rigortype 0.4.6 → 0.4.8

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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "rbs"
5
5
 
6
6
  require_relative "classification"
7
+ require_relative "generic_header_scan"
7
8
  require_relative "layout_index"
8
9
  require_relative "superclass_spelling"
9
10
  require_relative "write_result"
@@ -15,7 +16,8 @@ module Rigor
15
16
  #
16
17
  # ADR-14 slice 2: the writer parses the target with `RBS::Parser` to find the matching class declaration and
17
18
  # inserts new method declarations just before the class's closing `end` keyword. Existing declarations are
18
- # NEVER touched unless `--overwrite` is set AND the candidate's classification is `tighter-return`.
19
+ # NEVER touched unless `--overwrite` is set and the candidate is eligible for replacement (see
20
+ # {#eligible_for_replacement?}).
19
21
  #
20
22
  # The slice does NOT re-render the whole file through `RBS::Writer`. That would lose comments / blank-line
21
23
  # formatting per upstream design; the byte-range insertion approach taken here preserves untouched
@@ -32,9 +34,33 @@ module Rigor
32
34
 
33
35
  # Per-`update_existing` accumulator. The merge_class helper mutates `source` / `decls` / `applied` /
34
36
  # `skipped` in place as each class is processed so the next class sees the latest byte positions.
35
- MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, keyword_init: true)
37
+ MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, :repaired_headers,
38
+ :target, keyword_init: true)
36
39
  private_constant :MergeState
37
40
 
41
+ # #2576 — a target the writer cannot write (a permission denial, a directory sitting where the
42
+ # `.rbs` belongs, a parent path that is a regular file) is a bad `--write` invocation, not a
43
+ # crash: the command
44
+ # reports it as the same usage error `rigor init --path` gives and exits 64. Carries the target
45
+ # and the stripped Errno reason so the CLI prints `cannot write PATH: REASON` without
46
+ # re-parsing the message.
47
+ class WriteError < StandardError
48
+ attr_reader :target_path
49
+
50
+ def initialize(target_path, error)
51
+ @target_path = target_path.to_s
52
+ super("cannot write #{@target_path}: #{error.message.sub(/ @ .*\z/m, '')}")
53
+ end
54
+ end
55
+
56
+ # The target routing this writer applies; the generator maps candidates through it to ask {#accepts_target?}.
57
+ attr_reader :path_mapper
58
+
59
+ # #2605 — the generic classes whose bare headers the last {#write_all} (or {#write}) left bare, because a
60
+ # body names one of the parameters: `{ class:, targets:, blocking: [{ target:, names: }] }` each
61
+ # ({GenericHeaderScan#left_bare}).
62
+ attr_reader :left_bare
63
+
38
64
  # @param dry_run — assemble and validate every target exactly as a write would, and report it, without
39
65
  # creating or changing a file. `sig-gen --check` (ADR-112 WD4) is this, so it can only ever disagree
40
66
  # with `--write` about the disk, never about the content.
@@ -47,6 +73,15 @@ module Rigor
47
73
  @global_namespace_kinds = {}
48
74
  # Run-level qualified-class-name => superclass-token view, same lifecycle as `@global_namespace_kinds`.
49
75
  @global_superclasses = {}
76
+ # #2436 — the target being rendered's view of {MethodCandidate#class_type_params}, set per target.
77
+ @type_params = {}
78
+ # #2603/#2605 — the run-wide repair decision {#write_all} reads from the sig root before writing: the
79
+ # classes cleared for repair with their spelling, and the classes held back. {#write} leaves both empty and
80
+ # decides per file.
81
+ @scan_repairs = {}
82
+ @held_back = []
83
+ @left_bare = []
84
+ @repair_targets = []
50
85
  end
51
86
 
52
87
  # Process the full candidate list by resolving each candidate's target sig file via the path mapper (which
@@ -56,31 +91,94 @@ module Rigor
56
91
  # ADR-14 follow-up: this is the consolidated-layout entry point. The legacy `write(source_path,
57
92
  # candidates)` below assumes all candidates share a target and remains for spec convenience.
58
93
  #
94
+ # @param header_spelling — called with a class name, returns the header spelling of a generic class the run
95
+ # does not stamp (#2603: a bare header in a file no candidate routes to); the generator's
96
+ # {Generator#generic_header_spelling}. Without it only the run's stamp counts.
59
97
  # @return one per target sig file.
60
- def write_all(candidates)
98
+ def write_all(candidates, header_spelling: nil)
61
99
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
62
- return [] if emittable.empty?
100
+ scan_generic_headers(candidates, header_spelling)
63
101
 
64
102
  @global_namespace_kinds = build_namespace_kinds(candidates)
65
103
  @global_superclasses = build_superclasses(candidates)
66
- grouped = emittable.group_by do |c|
67
- @path_mapper.target_for(c.path, class_name: c.class_name, method_name: c.method_name, kind: c.rbs_kind)
104
+ grouped = emittable.group_by { |c| target_of(c) }
105
+ results = grouped.map { |target, group| write_target(target, group) }
106
+ results + repair_only_results(candidates, grouped.keys.map { |target| target.expand_path.to_s })
107
+ end
108
+
109
+ def target_of(candidate)
110
+ @path_mapper.target_for(candidate.path, class_name: candidate.class_name,
111
+ method_name: candidate.method_name, kind: candidate.rbs_kind)
112
+ end
113
+ private :target_of
114
+
115
+ # Issues #2603 and #2605 — reads every file under the signature paths for bare openings of generic classes
116
+ # before anything is written ({GenericHeaderScan}). A class is repaired in every file or in none: one bare
117
+ # opening rbs still rejects keeps the class failing, so repairing its other openings would be churn. Only files
118
+ # under the sig root are rewritten; a bare opening anywhere else holds its class back.
119
+ def scan_generic_headers(candidates, header_spelling)
120
+ stamp = merged_type_params(candidates)
121
+ scan = GenericHeaderScan.scan(sig_root: @path_mapper.sig_root_dir, signature_dirs: @path_mapper.signature_dirs,
122
+ spelling_for: ->(name) { stamp[name] || header_spelling&.call(name) })
123
+ @scan_repairs = scan.repairs
124
+ @held_back = scan.left_bare.map { |entry| entry.fetch(:class) }
125
+ @left_bare = scan.left_bare
126
+ @repair_targets = scan.repair_targets
127
+ end
128
+ private :scan_generic_headers
129
+
130
+ # The files with a bare header to repair that no emitted member routes to: each gets the repair and nothing
131
+ # else (#2580), whether a skipped candidate routes to it or none does (#2603). The row names a candidate's
132
+ # source when one routes there. A file the writer would not open (not UTF-8, unparseable) is never listed.
133
+ def repair_only_results(candidates, written)
134
+ sources = candidates.each_with_object({}) do |candidate, acc|
135
+ acc[target_of(candidate).expand_path.to_s] ||= candidate.path if candidate.path
136
+ end
137
+ @repair_targets.filter_map do |path|
138
+ target = Pathname(path)
139
+ next if written.include?(target.expand_path.to_s) || !inside_sig_root?(target)
140
+
141
+ result = update_existing(sources[target.expand_path.to_s], target, [], repair_only: true)
142
+ result if %i[updated skipped_invalid_rbs].include?(result.action)
68
143
  end
69
- grouped.map { |target, group| write_target(target, group) }
70
144
  end
145
+ private :repair_only_results
71
146
 
72
- # @param candidates — only
73
- # emittable classifications (new-method /
74
- # tighter-return) are honoured; the caller is
75
- # responsible for filtering.
147
+ # @param candidates — only emittable classifications are honoured; `write` filters on `EMITTABLE` itself.
76
148
  def write(source_path, candidates)
77
149
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
78
150
  return WriteResult.new(source_path: source_path, target_path: nil, action: :noop) if emittable.empty?
79
151
 
152
+ @scan_repairs = {}
153
+ @held_back = []
154
+ @left_bare = []
80
155
  target = @path_mapper.target_for(source_path, class_name: emittable.first.class_name)
81
156
  write_target(target, emittable, source_path: source_path)
82
157
  end
83
158
 
159
+ # Whether the writer is sure to write `candidate`. A file the writer writes nothing to is left as it is, so a
160
+ # class shell riding on that file's candidates never reaches disk; the generator asks this before it counts
161
+ # a shell as declared (#2454). Only a new method is sure: without `--overwrite` the writer skips a tighter
162
+ # return or an inline overwrite whose method the target already declares. Counting too few only demotes a
163
+ # subclass, which is the output before #2454.
164
+ def self.writes?(candidate)
165
+ candidate.classification == Classification::NEW_METHOD
166
+ end
167
+
168
+ # Whether {#write_target} would write to `target` at all, asked before any candidate is rendered (#2461): a
169
+ # target inside the sig root that is missing (the create path), or that exists, is valid UTF-8 and parses.
170
+ # {#update_existing} refuses an unparseable or mis-encoded file and leaves it untouched, so a class shell on it
171
+ # never reaches disk. A spliced result {RbsValidity} rejects is a writer defect and is not predicted here.
172
+ def accepts_target?(target)
173
+ return false unless inside_sig_root?(target)
174
+ return true unless target.exist?
175
+
176
+ source = target.read
177
+ source.valid_encoding? && !parse_signature(source).nil?
178
+ rescue SystemCallError
179
+ false
180
+ end
181
+
84
182
  # Whether `proposed` has STRICTLY FEWER bare `untyped` tokens than `existing`. A count, not a slot-by-slot
85
183
  # comparison: it reads as "at least one more position is concrete", and does not prove that no concrete
86
184
  # position became `untyped` elsewhere. Word-boundary matching counts `untyped` only as a type token, not
@@ -103,6 +201,8 @@ module Rigor
103
201
  end
104
202
 
105
203
  target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
204
+ rescue SystemCallError => e
205
+ raise WriteError.new(target, e)
106
206
  end
107
207
 
108
208
  # Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
@@ -139,6 +239,8 @@ module Rigor
139
239
  end
140
240
  WriteResult.new(source_path: source_path, target_path: target,
141
241
  action: :created, applied: candidates)
242
+ rescue SystemCallError => e
243
+ raise WriteError.new(target, e)
142
244
  end
143
245
 
144
246
  # ADR-14 gap-#3 follow-up (c): when one candidate's `class_name` is a strict prefix of another's, emit a
@@ -157,10 +259,11 @@ module Rigor
157
259
  tree = build_namespace_tree(candidates, shells)
158
260
  kinds = merged_namespace_kinds(candidates)
159
261
  supers = merged_superclasses(candidates)
262
+ @type_params = merged_type_params(candidates)
160
263
  render_tree_nodes(tree, kinds, supers, 0)
161
264
  end
162
265
 
163
- # Drains `class_shells` from every candidate; the generator's walker populates the same set on every
266
+ # Unions `class_shells` across every candidate; the generator's walker populates the same set on every
164
267
  # candidate produced from a given file (gap-#3 (e)).
165
268
  def collect_class_shells(candidates)
166
269
  shells = Set.new
@@ -202,6 +305,13 @@ module Rigor
202
305
  merged
203
306
  end
204
307
 
308
+ # #2436 — every candidate carries the run's map, so folding the target's candidates is the whole view.
309
+ def merged_type_params(candidates)
310
+ candidates.each_with_object({}) do |c, acc|
311
+ acc.merge!(c.class_type_params) if c.respond_to?(:class_type_params)
312
+ end
313
+ end
314
+
205
315
  def build_superclasses(candidates)
206
316
  candidates.each_with_object({}) do |candidate, acc|
207
317
  next unless candidate.respond_to?(:class_superclasses)
@@ -257,7 +367,7 @@ module Rigor
257
367
  indent = INDENT * depth
258
368
  qualified = (prefix + [node[:name]]).join("::")
259
369
  keyword = node_keyword(node, kinds, qualified)
260
- header = "#{keyword} #{node[:name]}#{superclass_suffix(keyword, supers, qualified)}"
370
+ header = "#{keyword} #{node[:name]}#{@type_params[qualified]}#{superclass_suffix(keyword, supers, qualified)}"
261
371
  body = render_tree_node_body(node, kinds, supers, depth, prefix)
262
372
  "#{indent}#{header}\n#{body}#{indent}end\n"
263
373
  end
@@ -293,13 +403,12 @@ module Rigor
293
403
  :module
294
404
  end
295
405
 
296
- def update_existing(source_path, target, candidates)
406
+ def update_existing(source_path, target, candidates, repair_only: false)
297
407
  source = target.read
298
408
  # Pre-parser encoding guard, mirroring `Environment::RbsLoader.invalid_encoding?`: handed invalid
299
409
  # UTF-8, `RBS::Parser` raises a bare `ArgumentError` on rbs 4.1 (not the `ParsingError` that
300
410
  # `parse_signature` below rescues) and could hang outright on the older releases the gemspec
301
- # supports. The refusal is loud — unlike an unparseable file (`:noop`, surfaced by `rigor check`'s
302
- # own quarantine warning), a mojibake file would otherwise fail with a stack trace naming neither
411
+ # supports. The refusal is loud — a mojibake file would otherwise fail with a stack trace naming neither
303
412
  # the file nor the fix.
304
413
  unless source.valid_encoding?
305
414
  return WriteResult.new(source_path: source_path, target_path: target,
@@ -307,16 +416,19 @@ module Rigor
307
416
  error: "#{target} is not valid UTF-8")
308
417
  end
309
418
 
310
- decls = parse_signature(source)
311
- return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
312
-
313
- state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
314
- replaced: [])
315
- merge_candidates(state, candidates)
419
+ # An existing file that does not parse is refused like a mis-encoded one, not skipped as `:noop` (#2480):
420
+ # `--check` must fail on it, because a `--write` that cannot touch the file is not an up-to-date `sig/`.
421
+ # `rigor check` quarantines such a file whole; this refusal names it at the sig-gen boundary too.
422
+ decls, parse_detail = parse_target_signature(target, source)
423
+ if decls.nil?
424
+ return WriteResult.new(source_path: source_path, target_path: target,
425
+ action: :skipped_unparseable,
426
+ error: "#{target} does not parse as RBS: #{parse_detail}")
427
+ end
316
428
 
317
- action = state.applied.empty? ? :noop : :updated
318
- unless action == :updated
319
- return WriteResult.new(source_path: source_path, target_path: target, action: action,
429
+ state = merged_state(source, decls, candidates, repair_only, target)
430
+ unless state_changed?(state)
431
+ return WriteResult.new(source_path: source_path, target_path: target, action: :noop,
320
432
  applied: state.applied, skipped: state.skipped,
321
433
  left_unreadable: state.left_unreadable)
322
434
  end
@@ -333,8 +445,11 @@ module Rigor
333
445
 
334
446
  target.write(state.source) unless @dry_run
335
447
  WriteResult.new(source_path: source_path, target_path: target,
336
- action: action, applied: state.applied, skipped: state.skipped,
337
- left_unreadable: state.left_unreadable, replaced: state.replaced)
448
+ action: :updated, applied: state.applied, skipped: state.skipped,
449
+ left_unreadable: state.left_unreadable, replaced: state.replaced,
450
+ repaired_headers: state.repaired_headers)
451
+ rescue SystemCallError => e
452
+ raise WriteError.new(target, e)
338
453
  end
339
454
 
340
455
  # Applies every class group, then every requested shell, then normalises the layout the three steps
@@ -343,12 +458,89 @@ module Rigor
343
458
  supers = merged_superclasses(candidates)
344
459
  kinds = merged_namespace_kinds(candidates)
345
460
  shells = collect_class_shells(candidates)
461
+ @type_params = merged_type_params(candidates)
346
462
  groups = candidates.group_by(&:class_name)
347
463
  # Ancestors first, so `Foo` is created (or found) before `Foo::Bar` looks for a parent to nest under.
348
464
  # A plain sort suffices: a name always sorts before every name it is a strict prefix of.
349
465
  groups.keys.sort.each { |name| merge_class(state, name, groups.fetch(name), kinds, supers) }
350
466
  merge_class_shells(state, shells, kinds, supers)
351
467
  collapse_nested_declarations(state, groups.keys + shells.to_a)
468
+ # Last, so the members this run merged count towards {#shadowed_after_merge?}.
469
+ repair_generic_headers(state, state.target)
470
+ end
471
+
472
+ # Issue #2580 — a declaration this file already holds for a class the loaded RBS declares generic, opened
473
+ # with no type parameters at all (`class Array`, as sig-gen wrote it before #2436), is a guaranteed
474
+ # `RBS::GenericParameterMismatchError`: rbs requires every declaration of a class to repeat the first one's
475
+ # parameters, and the class, plus every class whose ancestry reaches it (`Object` through `Kernel`), then
476
+ # fails its definition build. Writing more members into that header would keep the file broken, so the
477
+ # header gets the same spelling a new header would ({Generator#stamp_generic_reopenings}: plain
478
+ # parameters only). A header that spells parameters of its own is the project's and is left alone, as is
479
+ # every class neither the run's stamp nor {#write_all}'s scan names.
480
+ #
481
+ # A declaration whose body names a type like one of the parameters (`def old: () -> E` for a project class
482
+ # `E`) is left bare: under `class Array[unchecked out E]` that `E` would bind to the parameter, silently
483
+ # changing what the member says. The body is matched as text, so a comment or a nested declaration that
484
+ # spells the name also keeps the header bare, which is the state the file was already in; a method type that
485
+ # declares the name as its own parameter (`def m: [E] (E) -> E`) does not (#2605).
486
+ #
487
+ # Under {#write_all} the class is repaired in every file under the sig root or in none ({GenericHeaderScan},
488
+ # #2605): a class held back there is not repaired here, and a class the run does not stamp is repaired with the
489
+ # scan's spelling (#2603). The check below runs again after the merge, so a member this run merged counts
490
+ # too; the generator anchors a member's type that a parameter would capture (#2436), so it holds back a class
491
+ # the scan cleared only for a member written without that anchor, and is then reported in {#left_bare}.
492
+ # `repaired_headers` gets one entry per declaration repaired.
493
+ def repair_generic_headers(state, target)
494
+ spellings = @scan_repairs.merge(@type_params).except(*@held_back)
495
+ spellings.keys.sort.each do |name|
496
+ spelling = spellings.fetch(name)
497
+ bare = find_class_decls(state.decls, name).select { |d| d.type_params.empty? && d.location&.[](:name) }
498
+ next if bare.empty? || shadowed_after_merge?(state.source, bare, name, spelling, target)
499
+
500
+ insert_type_params(state, bare, spelling)
501
+ bare.each { state.repaired_headers << "#{name}#{spelling}" }
502
+ end
503
+ end
504
+
505
+ # Whether any of `bare` names one of the parameters `spelling` declares ({GenericHeaderScan.shadowing_names});
506
+ # each such opening is recorded in {#left_bare}.
507
+ def shadowed_after_merge?(source, bare, name, spelling, target)
508
+ params = GenericHeaderScan.parameter_names(spelling)
509
+ return true if params.nil?
510
+
511
+ blocking = bare.filter_map do |decl|
512
+ names = GenericHeaderScan.shadowing_names(source, decl, params)
513
+ { target: target.to_s, reason: "names_parameter", names: names } unless names.empty?
514
+ end
515
+ return false if blocking.empty?
516
+
517
+ @left_bare << { class: name, targets: [target.to_s], blocking: blocking }
518
+ true
519
+ end
520
+
521
+ # Splices `spelling` right after each declaration's name, last one first so earlier offsets stay valid.
522
+ def insert_type_params(state, decls, spelling)
523
+ decls.map { |d| d.location[:name].end_pos }.sort.reverse_each do |pos|
524
+ state.source = state.source[0...pos] + spelling + state.source[pos..]
525
+ end
526
+ state.decls = parse_signature(state.source) || state.decls
527
+ end
528
+
529
+ def merged_state(source, decls, candidates, repair_only, target)
530
+ state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
531
+ replaced: [], repaired_headers: [], target: target)
532
+ repair_only ? repair_only_candidates(state, candidates) : merge_candidates(state, candidates)
533
+ state
534
+ end
535
+
536
+ def state_changed?(state)
537
+ !(state.applied.empty? && state.repaired_headers.empty?)
538
+ end
539
+
540
+ # {#repair_only_results}' merge step: the run's stamp, and the header repair alone.
541
+ def repair_only_candidates(state, candidates)
542
+ @type_params = merged_type_params(candidates)
543
+ repair_generic_headers(state, state.target)
352
544
  end
353
545
 
354
546
  # ADR-14 gap-#3 (e): for every requested class shell that isn't already declared in the target file,
@@ -401,7 +593,9 @@ module Rigor
401
593
  parent_name = segments[0...-1].join("::")
402
594
  parent = find_class_decl(state.decls, parent_name)
403
595
  decl = parent && find_class_decl(state.decls, qualified)
404
- return if decl.nil? || overlapping?(parent, decl)
596
+ # A rooted declaration (`module A; class ::B::C`) keeps the layout the user wrote: cutting it out of its
597
+ # enclosing module would leave that module empty, so the member merges in place instead.
598
+ return if decl.nil? || overlapping?(parent, decl) || decl.name.to_s.start_with?("::")
405
599
 
406
600
  region = decl_region(state.source, decl)
407
601
  block = region && relocated_block(state.source, decl, parent, segments.last, region)
@@ -560,6 +754,15 @@ module Rigor
560
754
  nil
561
755
  end
562
756
 
757
+ # Parses an EXISTING target, naming it as the buffer so the parse error carries `path:line:col` (a bare
758
+ # String would be named `a.rbs`). Returns `[decls, nil]`, or `[nil, first_line_of_message]` on a parse error.
759
+ def parse_target_signature(target, source)
760
+ _, _, decls = RBS::Parser.parse_signature(RBS::Buffer.new(name: target.to_s, content: source))
761
+ [decls, nil]
762
+ rescue RBS::ParsingError => e
763
+ [nil, e.message.to_s.lines.first.to_s.strip]
764
+ end
765
+
563
766
  def merge_class(state, class_name, methods, kinds, supers)
564
767
  decl = find_class_decl(state.decls, class_name)
565
768
  if decl.nil?
@@ -586,11 +789,10 @@ module Rigor
586
789
  decls.each do |decl|
587
790
  next unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
588
791
 
589
- local = decl.name.to_s.sub(/\A::/, "")
590
- full = prefix.empty? ? local : "#{prefix.join('::')}::#{local}"
591
- return decl if full == qualified_name
792
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
793
+ return decl if path.join("::") == qualified_name
592
794
 
593
- nested = find_class_decl_in(decl.members, prefix + [local], qualified_name)
795
+ nested = find_class_decl_in(decl.members, path, qualified_name)
594
796
  return nested if nested
595
797
  end
596
798
  nil
@@ -602,10 +804,9 @@ module Rigor
602
804
  decls.each do |decl|
603
805
  next unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
604
806
 
605
- local = decl.name.to_s.sub(/\A::/, "")
606
- full = prefix.empty? ? local : "#{prefix.join('::')}::#{local}"
607
- found << decl if full == qualified_name
608
- find_class_decls(decl.members, qualified_name, prefix + [local], found)
807
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
808
+ found << decl if path.join("::") == qualified_name
809
+ find_class_decls(decl.members, qualified_name, path, found)
609
810
  end
610
811
  found
611
812
  end
@@ -669,7 +870,7 @@ module Rigor
669
870
  # name, slices out the old declaration's source range and substitutes the new RBS one-liner. Members that
670
871
  # are not `MethodDefinition`s are left alone.
671
872
  #
672
- # Two candidate classifications are eligible for replacement under `--overwrite`:
873
+ # Under `--overwrite`, an inline overwrite is always eligible, and so are two other classifications:
673
874
  #
674
875
  # 1. `TIGHTER_RETURN` — the classifier already proved the new return type is a strict subtype of the
675
876
  # declared one (with lenience guards passed).
data/lib/rigor/sig_gen.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "sig_gen/classification"
5
5
  require_relative "sig_gen/effect_annotation"
6
6
  require_relative "sig_gen/inline_declarations"
7
7
  require_relative "sig_gen/method_candidate"
8
+ require_relative "sig_gen/platform_guarded_files"
8
9
  require_relative "sig_gen/observed_call"
9
10
  require_relative "sig_gen/meta_class_shape"
10
11
  require_relative "sig_gen/type_elaborator"
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "literals"
5
+
6
+ module Rigor
7
+ module Source
8
+ # Issue #2793 — the methods a `Forwardable` / `SingleForwardable` delegator macro defines, read off its syntax
9
+ # alone. Ruby 4.0's `forwardable.rb` is the oracle for each row:
10
+ #
11
+ # - `def_instance_delegator(acc, m, ali = m)` defines `ali` on the instance side;
12
+ # - `def_instance_delegators(acc, *ms)` defines each `m` but `__send__` and `__id__`, instance side (the two are
13
+ # recorded anyway: `BasicObject` declares both, so a recorded entry changes no answer);
14
+ # - `instance_delegate(ms => acc)` defines each key name (a name or an array of them), instance side;
15
+ # - `def_single_delegator(s)` and `single_delegate` are the same on the singleton side;
16
+ # - `def_delegator(s)` and `delegate` are the same on the side the `extend` decides.
17
+ #
18
+ # `def_delegator(s)` and `delegate` are `Forwardable`'s instance aliases and `SingleForwardable`'s singleton
19
+ # ones, so their side is {AMBIGUOUS} and the caller settles it from the `extend` it has seen. `def_delegator :@x,
20
+ # :last, :final` defines `final` and never `last`.
21
+ #
22
+ # A name the syntax does not spell (`def_delegators :@x, *NAMES`, `delegate names => :@x`) makes the delegation
23
+ # {Delegation#open?}: the macro defines methods no reader can enumerate. `delegate` takes Forwardable's
24
+ # single-hash form only, so ActiveSupport's `delegate :a, to: :b` (positional names before a `to:` key) is
25
+ # not read here.
26
+ module ForwardableDelegators
27
+ AMBIGUOUS = :ambiguous
28
+
29
+ # The names a delegator macro defines, on which side, and whether it also defines names its syntax hides.
30
+ Delegation = Data.define(:side, :names, :open) do
31
+ def open? = open
32
+ end
33
+
34
+ # `[side, shape]` per macro: `:alias` takes `(accessor, method, alias = method)`, `:list` takes `(accessor,
35
+ # *methods)`, `:hash` takes `(methods => accessor, …)`.
36
+ MACROS = Ractor.make_shareable(
37
+ {
38
+ def_instance_delegator: %i[instance alias], def_instance_delegators: %i[instance list],
39
+ instance_delegate: %i[instance hash],
40
+ def_single_delegator: %i[singleton alias], def_single_delegators: %i[singleton list],
41
+ single_delegate: %i[singleton hash],
42
+ def_delegator: [AMBIGUOUS, :alias], def_delegators: [AMBIGUOUS, :list], delegate: [AMBIGUOUS, :hash]
43
+ }
44
+ )
45
+
46
+ private_constant :MACROS
47
+
48
+ module_function
49
+
50
+ # The {Delegation} of an implicit-self (or `self.`) delegator macro call, or nil for any other call, and for
51
+ # one whose arguments are not a shape the macro accepts.
52
+ def of(call_node)
53
+ return nil unless call_node.is_a?(Prism::CallNode)
54
+ return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
55
+
56
+ side, shape = MACROS[call_node.name]
57
+ return nil if side.nil?
58
+
59
+ arguments = call_node.arguments&.arguments || []
60
+ return Delegation.new(side: side, names: [].freeze, open: true) if unplaceable_splat?(shape, arguments)
61
+
62
+ names, open = case shape
63
+ when :alias then alias_names(arguments)
64
+ when :list then list_names(arguments)
65
+ else hash_names(arguments)
66
+ end
67
+ return nil if names.nil?
68
+
69
+ Delegation.new(side: side, names: names.freeze, open: open)
70
+ end
71
+
72
+ # A splat (`def_delegator(*ARGS)`, `def_delegators(*ARGS)`, `delegate(*pairs)`) hides which argument is the
73
+ # accessor and which a name, so the macro may define any name. A splat after the accessor of the list form is
74
+ # only a hidden name, and {#list_names} reads the literal names beside it.
75
+ def unplaceable_splat?(shape, arguments)
76
+ splats = arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
77
+ return false if splats.empty?
78
+
79
+ shape != :list || splats.include?(0)
80
+ end
81
+
82
+ def alias_names(arguments)
83
+ return nil unless (2..3).cover?(arguments.size)
84
+
85
+ name = Literals.symbol_or_string(arguments.last)
86
+ name.nil? ? [[], true] : [[name], false]
87
+ end
88
+
89
+ def list_names(arguments)
90
+ return nil if arguments.empty?
91
+
92
+ rest = arguments.drop(1)
93
+ names = rest.filter_map { |argument| Literals.symbol_or_string(argument) }
94
+ [names, names.size != rest.size]
95
+ end
96
+
97
+ def hash_names(arguments)
98
+ return nil unless arguments.size == 1
99
+
100
+ hash = arguments.first
101
+ return nil unless hash.is_a?(Prism::KeywordHashNode) || hash.is_a?(Prism::HashNode)
102
+
103
+ names = []
104
+ open = false
105
+ hash.elements.each do |element|
106
+ return [[], true] unless element.is_a?(Prism::AssocNode)
107
+
108
+ keys = key_names(element.key)
109
+ keys.nil? ? (open = true) : names.concat(keys)
110
+ end
111
+ [names, open]
112
+ end
113
+
114
+ # The names a hash key spells — a literal name, or an array of them — or nil when any is computed.
115
+ def key_names(key)
116
+ elements = key.is_a?(Prism::ArrayNode) ? key.elements : [key]
117
+ names = elements.map { |element| Literals.symbol_or_string(element) }
118
+ names.include?(nil) ? nil : names
119
+ end
120
+ end
121
+ end
122
+ end
@@ -109,6 +109,19 @@ module Rigor
109
109
  def each_child(node, &)
110
110
  node.rigor_each_child(&) if node.is_a?(Prism::Node)
111
111
  end
112
+
113
+ # #2455 — whether `root` nests deeper than `bound` levels, measured without recursion (the question is asked after
114
+ # the recursive walkers ran out of stack). Stops as soon as one path passes the bound.
115
+ def self.nested_deeper_than?(root, bound)
116
+ stack = [[root, 1]]
117
+ until stack.empty?
118
+ node, depth = stack.pop
119
+ return true if depth > bound
120
+
121
+ node.rigor_each_child { |child| stack << [child, depth + 1] }
122
+ end
123
+ false
124
+ end
112
125
  end
113
126
  end
114
127
  end
@@ -6,8 +6,8 @@ module Rigor
6
6
  #
7
7
  # Every carrier's acceptance check is the same fixed forwarding on `self` —
8
8
  # `Inference::Acceptance.accepts(self, other, mode: mode)` — so it lived as an identical copy in
9
- # fourteen carriers. The one exception is `Type::App`, which forwards on its reduced `bound` type rather
10
- # than `self`; it keeps its own `accepts` and does not include this.
9
+ # every carrier that includes it. The one exception is `Type::App`, which forwards on its reduced `bound`
10
+ # type rather than `self`; it keeps its own `accepts` and does not include this.
11
11
  module AcceptanceRouter
12
12
  def accepts(other, mode: :gradual)
13
13
  Inference::Acceptance.accepts(self, other, mode: mode)
@@ -9,7 +9,7 @@ module Rigor
9
9
  # answer alongside the boundary mode the answer was computed under and an ordered list of textual
10
10
  # reasons describing which rules fired.
11
11
  #
12
- # AcceptsResult is the dual of `SubtypeResult` (Slice 5+). Acceptance answers "is `other` passable to
12
+ # AcceptsResult is the dual of the planned `SubtypeResult`. Acceptance answers "is `other` passable to
13
13
  # `self` at a method-parameter or assignment boundary?", consulting the gradual-typing rules in
14
14
  # docs/type-specification/value-lattice.md when `mode` is `:gradual`, and the strict subset relation
15
15
  # when `mode` is `:strict`. Phase 2c ships full `:gradual` semantics; `:strict` is reserved for later
@@ -17,13 +17,11 @@ module Rigor
17
17
  # (use a plain type alias instead).
18
18
  #
19
19
  # `bound` is a `Rigor::Type` representing the value Rigor MUST erase to when this `App` cannot be
20
- # reduced — registered at `%a{rigor:v1:hkt_register}` time, defaulting to
21
- # `Rigor::Type::Dynamic[Rigor::Type::Top]` per ADR-20 D5 / WD2. It also drives the lattice probes
22
- # (`top` / `bot` / `dynamic`) and the acceptance fallback while no reduction is wired (Slice 1).
20
+ # reduced — registered at `%a{rigor:v1:hkt_register}` time, defaulting to the untyped top per ADR-20 D5 /
21
+ # WD2. It also drives the lattice probes (`top` / `bot` / `dynamic`) and `accepts`.
23
22
  #
24
- # Slice 1 ships the carrier as **opaque**: every operation delegates to `bound` since no reduction
25
- # surface exists yet. Slice 2 introduces the conditional / indexed-access evaluator that reduces `App`
26
- # to its registered body before delegating; the carrier shape stays identical.
23
+ # The carrier is opaque: those operations delegate to `bound`, and {#reduce} is the one path that unfolds
24
+ # the application to its registered body first (ADR-20 Slice 2a).
27
25
  #
28
26
  # Display form per ADR-20 OQ5: bare RBS-style `uri[arg1, arg2]`, not the wrapped `App[uri, [arg1,
29
27
  # arg2]]` faithful form. Two `App` values are structurally equal iff their `uri`, `args`, AND `bound`