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
@@ -3,6 +3,7 @@
3
3
  require "rigor/plugin"
4
4
 
5
5
  require_relative "activesupport_core_ext/effects"
6
+ require_relative "activesupport_core_ext/symbolize_keys"
6
7
 
7
8
  module Rigor
8
9
  module Plugin
@@ -43,12 +44,15 @@ module Rigor
43
44
  manifest(
44
45
  id: "activesupport-core-ext",
45
46
  target_gems: ["activesupport"],
47
+ # Bumped 2026-10-27 (#2433) — `Hash#symbolize_keys` / `#to_options` map each key member through a
48
+ # `dynamic_return` hook instead of claiming `Hash[Symbol, V]`.
49
+ #
46
50
  # Bumped 2026-09-10 (#534 item 7) — `ActiveSupport::Concern` is declared, so `extend
47
51
  # ActiveSupport::Concern` resolves instead of reading as an unresolved constant.
48
52
  #
49
53
  # Bumped 2026-09-02 (#632) — `ActiveSupport::Duration`'s reader surface (`ago`/`to_i`/`iso8601`/…)
50
54
  # is now declared, open_receivers-protected; see the class comment above.
51
- version: "0.5.0",
55
+ version: "0.6.0",
52
56
  description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions, " \
53
57
  "plus the `ActiveSupport::Duration` type of the numeric time multipliers and its " \
54
58
  "reader surface.",
@@ -190,11 +194,9 @@ module Rigor
190
194
  argument = duration_arithmetic_argument(call_node)
191
195
  next nil if argument.nil?
192
196
 
193
- # The ARGUMENT is checked before the receiver: it is the cheaper of the two on the overwhelmingly
194
- # common shapes (`i + 1`, `n * 2`), and a call with no Duration operand must cost as little as
195
- # possible — this rule is consulted on every `+` / `-` / `*` in the project. The early exit is
196
- # what makes the ordering worth anything: an argument that is no Duration operand at all
197
- # (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
197
+ # The ARGUMENT is checked before the receiver: it is the cheaper test on the common shapes (`i + 1`,
198
+ # `n * 2`), and this rule is consulted on every `+` / `-` / `*`. An argument that is no Duration
199
+ # operand (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
198
200
  argument_kind = duration_operand_kind(scope&.type_of(argument))
199
201
  next nil if argument_kind.nil?
200
202
 
@@ -204,6 +206,17 @@ module Rigor
204
206
  duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
205
207
  end
206
208
 
209
+ # #2433 — `symbolize_keys` / `to_options` keep a key without `to_sym`, so the key type is mapped per
210
+ # member; see {SymbolizeKeys}. The RBS rows answer `Hash[untyped, V]` wherever this declines.
211
+ dynamic_return methods: %i[symbolize_keys to_options] do |call_node, scope|
212
+ next nil unless call_node.is_a?(Prism::CallNode)
213
+ next nil unless call_node.receiver
214
+ next nil unless call_node.arguments.nil?
215
+
216
+ # A block is ignored by `symbolize_keys`, so it does not decline.
217
+ SymbolizeKeys.result(scope&.type_of(call_node.receiver), scope)
218
+ end
219
+
207
220
  # ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
208
221
  # project file, so a project enabling this plugin stays incremental-capable.
209
222
  def incremental_state_fingerprint
@@ -1565,28 +1565,35 @@ class Hash[unchecked out K, unchecked out V]
1565
1565
  # non-bang half is `%a{pure}`. `assert_valid_keys` raises rather than
1566
1566
  # mutating (raising is not an effect label).
1567
1567
  %a{pure}
1568
- def symbolize_keys: () -> Hash[Symbol, V]
1568
+ # `key.to_sym rescue key` keeps a key without `to_sym`, so no one key type is right: `Hash[untyped, V]` here,
1569
+ # and the plugin's `dynamic_return` hook (#2433) maps each key member where it can.
1570
+ def symbolize_keys: () -> Hash[untyped, V]
1571
+ # `symbolize_keys!` keeps `-> self`: `key.to_sym rescue key` leaves a key without `to_sym` as it is, so
1572
+ # `Hash[Symbol, V]` is wrong for an Integer-keyed receiver (#2424 review). The other bang forms below
1573
+ # answer the re-keyed or re-valued hash.
1569
1574
  def symbolize_keys!: () -> self
1570
1575
  %a{pure}
1571
1576
  def deep_symbolize_keys: () -> Hash[untyped, untyped]
1572
- def deep_symbolize_keys!: () -> self
1577
+ def deep_symbolize_keys!: () -> Hash[untyped, untyped]
1573
1578
  %a{pure}
1574
1579
  def stringify_keys: () -> Hash[String, V]
1575
- def stringify_keys!: () -> self
1580
+ def stringify_keys!: () -> Hash[String, V]
1576
1581
  %a{pure}
1577
1582
  def deep_stringify_keys: () -> Hash[untyped, untyped]
1578
- def deep_stringify_keys!: () -> self
1583
+ def deep_stringify_keys!: () -> Hash[untyped, untyped]
1579
1584
  %a{pure}
1580
1585
  def assert_valid_keys: (*K | Array[K]) -> self
1586
+ # The block's result is the new key at every level, so the keys are its type, not `K`: `{ a: 1 }
1587
+ # .deep_transform_keys(&:to_s)` has String keys. Nested values are re-keyed too, so they read `untyped`.
1581
1588
  %a{pure}
1582
- def deep_transform_keys: () { (K) -> K } -> Hash[K, untyped]
1583
- def deep_transform_keys!: () { (K) -> K } -> self
1589
+ def deep_transform_keys: [X] () { (K) -> X } -> Hash[X, untyped]
1590
+ def deep_transform_keys!: [X] () { (K) -> X } -> Hash[X, untyped]
1584
1591
  %a{pure}
1585
1592
  def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
1586
- def deep_transform_values!: () { (V) -> untyped } -> self
1593
+ def deep_transform_values!: () { (V) -> untyped } -> Hash[K, untyped]
1587
1594
  # `alias_method`s of `symbolize_keys` / `symbolize_keys!`.
1588
1595
  %a{pure}
1589
- def to_options: () -> Hash[Symbol, V]
1596
+ def to_options: () -> Hash[untyped, V]
1590
1597
  def to_options!: () -> self
1591
1598
 
1592
1599
  # `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
@@ -1600,11 +1607,17 @@ class Hash[unchecked out K, unchecked out V]
1600
1607
  # `core_ext/hash/deep_merge` — the block receives `(key, this_val,
1601
1608
  # other_val)` and returns the merged value; neither `deep_merge` nor its
1602
1609
  # block mutates either hash (containment covers the block itself).
1603
- %a{pure}
1604
- def deep_merge: (Hash[K, V]) -> Hash[K, V]
1605
- | (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
1606
- def deep_merge!: (Hash[K, V]) -> self
1607
- | (Hash[K, V]) { (K, V, V) -> V } -> self
1610
+ # The result carries the argument's keys and values as well as the
1611
+ # receiver's, and a key both hold a Hash under merges into a third Hash
1612
+ # that is neither side's value type (`{ n: { x: 1 } }.deep_merge(n: { y:
1613
+ # "s" })[:n]` is `{ x: 1, y: "s" }`), so the values read `untyped`.
1614
+ # `(Hash[K, V]) -> Hash[K, V]` typed `h.deep_merge(b: "x")[:b]` as the
1615
+ # receiver's value type (#2424 review). The bang form answers the receiver.
1616
+ %a{pure}
1617
+ def deep_merge: [A, B] (Hash[A, B] other_hash) -> Hash[K | A, untyped]
1618
+ | [A, B] (Hash[A, B] other_hash) { (K | A, untyped, untyped) -> untyped } -> Hash[K | A, untyped]
1619
+ def deep_merge!: [A, B] (Hash[A, B] other_hash) -> Hash[K | A, untyped]
1620
+ | [A, B] (Hash[A, B] other_hash) { (K | A, untyped, untyped) -> untyped } -> Hash[K | A, untyped]
1608
1621
  # `other.is_a?(Hash)` — the hook `ActiveSupport::DeepMergeable` asks before
1609
1622
  # recursing into a value. `:nodoc:`, but public.
1610
1623
  %a{pure}
@@ -37,9 +37,9 @@ module Rigor
37
37
  @classes.empty?
38
38
  end
39
39
 
40
- # The classes alba infers for the associations that name no resource, limited to
41
- # those that exist in the project. The block receives each association name and returns alba's
42
- # `classify` of it (the caller owns the inflector; ADR-39).
40
+ # The classes alba infers for the associations that name no resource, limited to those that exist in
41
+ # the project. The block receives each association name and returns alba's `classify` of it (the
42
+ # caller owns the inflector; ADR-39).
43
43
  def inferred_roots(&classify)
44
44
  roots = @associations.filter_map do |association|
45
45
  next unless resource?(association.owner)
@@ -212,9 +212,7 @@ module Rigor
212
212
  # ... end` row's own recursive call (see {#each_block_type_info}). It caps the recursion at ONE
213
213
  # level deep: with `nested: true`, {#walk_predicate_chain} declines a FURTHER `each do ... end`
214
214
  # rather than recursing again (issue #137 deliberately does not model doubly-nested arrays of
215
- # arrays). This also keeps `collect_schema_shape` from calling itself — {#each_block_type_info}
216
- # is the only caller that ever passes `nested: true}` — so the call graph has no cycle for the
217
- # engine's return-type inference to approximate through.
215
+ # arrays). The recursion through {#each_block_type_info} is therefore bounded and terminates.
218
216
  def collect_schema_shape(block_node, type_aliases, nested:)
219
217
  required = {}
220
218
  optional = {}
@@ -34,9 +34,9 @@ module Rigor
34
34
  #
35
35
  # The synthetic reader's return type is resolved via ADR-18's `returns_from_arg:` fact lookup: the
36
36
  # call-site's second argument (`Types::String` etc.) is looked up through the `:dry_type_aliases`
37
- # fact published by `rigor-dry-types`, yielding `Nominal[String]` for common cases. When the lookup
38
- # misses (e.g. inline method-chain argument whose chain-head isn't currently extracted), the row
39
- # falls back to `Dynamic[Top]` silently.
37
+ # fact published by `rigor-dry-types`, yielding the aliased class (`String`) for common cases. When the
38
+ # lookup misses (e.g. inline method-chain argument whose chain-head isn't currently extracted), the row
39
+ # falls back to the untyped floor silently.
40
40
  #
41
41
  # ## Scope (slice 2c minimum)
42
42
  #
@@ -58,8 +58,8 @@ module Rigor
58
58
  "by consuming `rigor-dry-types`'s `:dry_type_aliases` fact.",
59
59
  # ADR-9 consumption — the precision-promotion path below uses `:dry_type_aliases` published by
60
60
  # `rigor-dry-types`. The fact is optional: when the `rigor-dry-types` plugin isn't loaded, the
61
- # `returns_from_arg:` lookup misses and the synthetic readers fall back to `Dynamic[Top]` (the
62
- # pre-ADR-18 floor).
61
+ # `returns_from_arg:` lookup misses and the synthetic readers fall back to the untyped floor (the
62
+ # pre-ADR-18 behaviour).
63
63
  consumes: [{ plugin_id: "dry-types", name: :dry_type_aliases, optional: true }],
64
64
  heredoc_templates: [
65
65
  Rigor::Plugin::Macro::HeredocTemplate.new(
@@ -69,7 +69,7 @@ module Rigor
69
69
  # ADR-18 — the synthetic reader's return type comes from the call site's second argument
70
70
  # (`Types::String` etc.), resolved through the `:dry_type_aliases` fact. When the lookup
71
71
  # misses (e.g. inline `attribute :tag, Types::String.constrained(...)`, whose receiver chain
72
- # head isn't currently extracted), the row falls back to Dynamic[Top].
72
+ # head isn't currently extracted), the row falls back to the untyped floor.
73
73
  emit: [{
74
74
  name: "\#{name}",
75
75
  returns_from_arg: {
@@ -33,10 +33,9 @@ module Rigor
33
33
  # `Coercible::*` (everything-to-target coercion), `Strict::*` (no coercion; raise if mismatch),
34
34
  # `Params::*` (HTTP / query-string-style coercion, used by Hanami / Roda / dry-web in request
35
35
  # handling), `JSON::*` (JSON-shape coercion). Each category exposes the same set of names as the
36
- # canonical shortcuts above, plus a few additions that are category-specific (`Params::Nil`,
37
- # `JSON::Symbol`). For Rigor's purposes the underlying class is the same regardless of category —
38
- # coercion semantics are a runtime concern. We register every `<module>::<Category>::<Name>`
39
- # mapping the upstream gem publishes so call-site references work uniformly.
36
+ # canonical shortcuts above; only those names are registered here, so a category-specific extra
37
+ # is not covered. For Rigor's purposes the underlying class is the same regardless of category —
38
+ # coercion semantics are a runtime concern.
40
39
  NESTED_CATEGORIES = %w[Coercible Strict Params JSON].freeze
41
40
  private_constant :NESTED_CATEGORIES
42
41
 
@@ -229,8 +228,8 @@ module Rigor
229
228
 
230
229
  # `include Dry.Types()` at the top of the module body is the canonical alias declaration. We
231
230
  # accept the call anywhere in the body (some projects guard it with a `if defined?` check). The
232
- # argument list must be empty (or a kwargs-only `default: :nominal` style accepted by upstream;
233
- # we treat both as "alias-installing").
231
+ # `Dry.Types(...)` argument list is not inspected: kwargs such as `default: :nominal` install the
232
+ # aliases too.
234
233
  def contains_dry_types_include?(body)
235
234
  return false if body.nil?
236
235
 
@@ -21,7 +21,7 @@ module Rigor
21
21
  # cross-plugin fact (ADR-9). Other dry-rb adapter plugins consume this fact:
22
22
  #
23
23
  # - `rigor-dry-struct` reads it so `attribute :city, Types::String` promotes `address.city` from
24
- # `Dynamic[Top]` to `Nominal[String]` via ADR-18's `returns_from_arg:` fact lookup.
24
+ # from an untyped value to `String` via ADR-18's `returns_from_arg:` fact lookup.
25
25
  # - `rigor-dry-validation` / `rigor-dry-schema` read it for per-key type recognition in `schema { … }`
26
26
  # / `params { … }` blocks (separate plugin slice).
27
27
  #
@@ -44,9 +44,8 @@ module Rigor
44
44
  end
45
45
 
46
46
  # Renders `Foo` / `Foo::Bar` / `::Foo::Bar` as the `::`-joined String {ContractScanner}'s table is
47
- # keyed by. Duplicated from rigor-dry-schema's identical helper rather than shared — five lines,
48
- # no dependency on anything ELSE in that plugin, and reusing it would need a `registered_for`
49
- # guard here too even though this leaf never touches dry-schema's own type/alias machinery.
47
+ # keyed by. Duplicated from rigor-dry-schema rather than shared: this leaf must not depend on that
48
+ # plugin, and reusing the helper would need a `registered_for` guard here too.
50
49
  def constant_name(node)
51
50
  case node
52
51
  when Prism::ConstantReadNode then node.name.to_s
@@ -58,19 +58,16 @@ module Rigor
58
58
 
59
59
  # ADR-37 dynamic return — refines `NewUserContract.new.call(input).to_h` to the per-contract
60
60
  # `HashShape` when {#result_shape_for} finds one. Gated on the method name alone (mirrors
61
- # rigor-dry-schema's own `.to_h` rule): the RBS overlay types `Contract#call`'s return as the
62
- # generic `Result`, but that alone can't select a PER-CONTRACT shape, so the block does its own
63
- # syntactic chain match via {ParamsShape.contract_name} before touching the table. When
64
- # rigor-dry-schema isn't loaded, or the chain isn't a Contract call, this returns nil and the RBS
65
- # overlay's generic `Hash[Symbol, untyped]` stands.
61
+ # rigor-dry-schema's `.to_h` rule); the chain match is in {ParamsShape.contract_name}, because the
62
+ # RBS overlay's generic `Result` cannot select a per-contract shape. Otherwise returns nil and the
63
+ # overlay's generic hash type stands.
66
64
  dynamic_return methods: [:to_h] do |call_node, _scope|
67
65
  result_shape_for(call_node)
68
66
  end
69
67
 
70
68
  # ADR-37 slice 1c two-pass — the "collect" half of `dry-validation.rule-key-mismatch`. Reads the
71
- # SAME `:dry_type_aliases` fact `#prepare` does (a plain `services.fact_store.read`, not
72
- # `#read_fact`'s memoised wrapper — `services` at collect time is the run-wide instance, so an
73
- # in-block memo isn't needed here the way it is across many per-call `dynamic_return` invocations).
69
+ # same `:dry_type_aliases` fact as `#prepare`, straight from `services.fact_store` (no `#read_fact`
70
+ # memo: `services` here is run-wide, unlike the per-call `dynamic_return` blocks).
74
71
  node_file_context do |root, _scope|
75
72
  type_aliases = services.fact_store.read(plugin_id: "dry-types", name: :dry_type_aliases) || {}
76
73
  ContractScanner.rule_key_issues(root, type_aliases)
@@ -14,7 +14,6 @@ module Rigor
14
14
  signature_paths: ["sig"]
15
15
  )
16
16
 
17
- # Option catalogue and return types for Easy methods
18
17
  EASY_RETURN_TYPES = {
19
18
  perform: -> { Rigor::Type::Combinator.nominal_of("Integer") },
20
19
  response_code: -> { Rigor::Type::Combinator.nominal_of("Integer") },
@@ -115,7 +115,7 @@ module Rigor
115
115
  # 3. Inflected from the factory name — `:user` → `"User"`, `:admin_user` → `"AdminUser"`. The
116
116
  # factory name is already singular by FactoryBot convention, so we only need camelization.
117
117
  #
118
- # Returns a String (the canonical class name).
118
+ # Returns the canonical class name.
119
119
  def factory_model_class(call_node, factory_name)
120
120
  explicit = explicit_class_option(call_node)
121
121
  return explicit if explicit
@@ -84,27 +84,20 @@ module Rigor
84
84
  @factory_search_paths = Array(config.fetch("factory_search_paths")).map(&:to_s)
85
85
  end
86
86
 
87
- # ADR-102 WD3 / #350 — the model classes the factories build, published as `:reachability_references`
88
- # rather than as `:reachability_roots`.
87
+ # ADR-102 WD3 / #350 — the factories' model classes are published as `:reachability_references`,
88
+ # not `:reachability_roots`.
89
89
  #
90
- # Both halves of that sentence are load-bearing.
91
- #
92
- # It is a CONTRIBUTION at all because a factory names its class by a mechanism the constant scan cannot
93
- # follow: `factory :user, class: "Admin::User"` is a string, and a bare `factory :user` is FactoryBot's
94
- # own constantization of the factory name. Neither leaves a constant node anywhere in the project.
95
- #
96
- # It is a REFERENCE rather than a root because factories live in the test tree. Rooting them would make
97
- # every factoried class production-reachable and erase ADR-102 WD8's "reachable only from tests"
98
- # category for exactly the classes it is most likely to be about — a model kept alive by its factory
99
- # and its spec and nothing else is the archetype of dead production code with a live test, and that is
100
- # the single most actionable row this report produces. Carrying the `:test` role keeps the finding.
90
+ # It is a contribution because a factory names its class by a mechanism the constant scan cannot
91
+ # follow (`factory :user, class: "Admin::User"` is a string; a bare `factory :user` is constantized by
92
+ # FactoryBot). It is a reference rather than a root because factories live in the test tree: rooting
93
+ # them would make every factoried class production-reachable and erase ADR-102 WD8's "reachable only
94
+ # from tests" category. The `:test` role keeps that finding.
101
95
  def prepare(services)
102
96
  index = producer_value(:factory_index)
103
97
  return if index.nil? || index.empty?
104
98
 
105
- # The producer value and the ADR-9 fact are different channels (#921) — `producer_value` is this
106
- # plugin's own cached computation, read back via `producer_value` above; `fact_store.publish` is
107
- # what makes it visible to another plugin's `read_fact`, which is how rigor-rspec binds
99
+ # The producer value and the ADR-9 fact are different channels (#921): `fact_store.publish` is what
100
+ # makes the index visible to another plugin's `read_fact`, as rigor-rspec uses it to bind
108
101
  # `create(:name)` to the factory's model class.
109
102
  services.fact_store.publish(plugin_id: manifest.id, name: :factory_index, value: index)
110
103
 
@@ -118,8 +111,7 @@ module Rigor
118
111
 
119
112
  # ADR-37 — per-call factory/attribute validation over the engine-owned walk. Each violation carries
120
113
  # its own location (the call's message_loc, or the offending attribute key), so it is positioned
121
- # via `diagnostic(node, location:)`. No file-level diagnostic remains, so there is no
122
- # `diagnostics_for_file`.
114
+ # via `diagnostic(node, location:)`.
123
115
  node_rule Prism::CallNode do |node, _scope, path|
124
116
  index = producer_value(:factory_index)
125
117
  next [] if index.nil? || index.empty?
@@ -40,7 +40,6 @@ module Rigor
40
40
  all_args = call_node.arguments&.arguments || []
41
41
  return nil if all_args.empty?
42
42
 
43
- # Strip trailing keyword options hash if present
44
43
  pos_args = all_args.grep_v(Prism::KeywordHashNode)
45
44
  return nil if pos_args.empty?
46
45
 
@@ -149,7 +149,6 @@ module Rigor
149
149
  end
150
150
  end
151
151
  when :callback
152
- # Issue 4 fix: extract callback definitions
153
152
  args = node.arguments&.arguments || []
154
153
  if args.size >= 3
155
154
  cb_name = Analyzer.extract_symbol(args[0])
@@ -193,7 +192,6 @@ module Rigor
193
192
  end
194
193
  end
195
194
  else
196
- # Issue 5 fix: pass mod_name to sub-plugin recognizers
197
195
  FFI.binding_recognizers.each do |recognizer|
198
196
  facts = recognizer.recognize(node, mod_name)
199
197
  Array(facts).each do |fact|
@@ -38,11 +38,10 @@ module Rigor
38
38
  # Dynamic return rule for attached FFI functions
39
39
  # Gated on known FFI library receivers and attached function names
40
40
  #
41
- # Issue #701 — both receiver kinds are declared because the block below already resolved
42
- # `Nominal` and `Singleton` alike: `attach_function` installs the binding on the library module
43
- # itself (`MyLib.strlen`) and `include`ing that module makes the same name an instance call, so
44
- # neither kind is an accident. The kind-aware gate makes that intent explicit rather than relying
45
- # on a `receivers:` entry answering for both.
41
+ # Issue #701 — both receiver kinds are declared because the block below already resolves `Nominal`
42
+ # and `Singleton` alike: `attach_function` installs the binding on the library module itself
43
+ # (`MyLib.strlen`), and `include`ing that module makes the same name an instance call. The
44
+ # kind-aware gate makes that intent explicit rather than relying on one `receivers:` entry for both.
46
45
  dynamic_return receivers: -> { both_receiver_kinds(producer_value(:ffi_catalog)&.libraries) },
47
46
  methods: -> { producer_value(:ffi_catalog)&.function_method_names || [] } do |call_node, scope|
48
47
  catalog = producer_value(:ffi_catalog)
@@ -85,7 +85,6 @@ module Rigor
85
85
  method_names: NAMESPACE_METHODS,
86
86
  self_type: "singleton(Grape::API::Instance)"
87
87
  ),
88
- # `params do ... end` — body `instance_eval`s on a `ParamsScope` instance.
89
88
  Rigor::Plugin::Macro::BlockAsMethod.new(
90
89
  receiver_constraint: "Grape::API",
91
90
  method_names: %i[params],
@@ -121,7 +120,6 @@ module Rigor
121
120
  method_names: %i[desc],
122
121
  self_type: "Grape::DSL::Desc::ConfigContext"
123
122
  ),
124
- # Verb bodies run inside `Grape::Endpoint` instances.
125
123
  Rigor::Plugin::Macro::BlockAsMethod.new(
126
124
  receiver_constraint: "Grape::API",
127
125
  method_names: VERB_METHODS,
@@ -23,9 +23,8 @@ module Rigor
23
23
  }.freeze
24
24
 
25
25
  # The base class name a Schema::Object subclass MUST inherit from to be recognised. Match is on
26
- # the rightmost segment of the superclass constant chain so both `GraphQL::Schema::Object` and
27
- # the locally-aliased `BaseObject = GraphQL::Schema::Object` shape work when the alias's RHS is
28
- # the canonical path.
26
+ # the last two segments of the superclass constant chain (see {#schema_subclass?}), so a locally
27
+ # aliased base such as `BaseObject = GraphQL::Schema::Object` is not followed.
29
28
  SCHEMA_OBJECT_TAIL = "Object"
30
29
  SCHEMA_ENUM_TAIL = "Enum"
31
30
  SCHEMA_INPUT_OBJECT_TAIL = "InputObject"
@@ -183,7 +182,7 @@ module Rigor
183
182
  private_class_method :statement_nodes
184
183
 
185
184
  # Walks every top-level `value "..."` call inside an enum subclass body and returns the value
186
- # names as an Array<String>. Both shapes graphql-ruby accepts work:
185
+ # names. Both shapes graphql-ruby accepts work:
187
186
  #
188
187
  # value "ACTIVE"
189
188
  # value "DISABLED", value: :off, description: "..."
@@ -299,7 +298,7 @@ module Rigor
299
298
  end
300
299
  private_class_method :parse_field_call
301
300
 
302
- # Resolves the `Type` positional argument to a `{type: "ClassName", list: bool}` tuple.
301
+ # Resolves the `Type` positional argument to a `type:` class name and a `list:` flag.
303
302
  # ArrayNode forms (`[String]` / `[Types::User]`) unwrap the single element and mark `list: true`.
304
303
  # Bare constant refs are not lists. Returns nil for unrecognised shapes (string types `"User"`,
305
304
  # Proc lazy types, etc.) so callers drop the field.
@@ -350,8 +349,8 @@ module Rigor
350
349
  end
351
350
  private_class_method :extract_nullability
352
351
 
353
- # Returns the constant chain as an Array of String segments (`["GraphQL", "Schema", "Object"]`).
354
- # Empty array for unrecognised node kinds.
352
+ # Returns the constant chain's segments, e.g. `["GraphQL", "Schema", "Object"]`. Empty for
353
+ # unrecognised node kinds.
355
354
  def constant_path_segments(node)
356
355
  case node
357
356
  when Prism::ConstantReadNode then [node.name.to_s]
@@ -20,10 +20,8 @@ module Rigor
20
20
  @contracts = contracts
21
21
  end
22
22
 
23
- # Per-`ClassNode` check over the engine-owned walk (ADR-37): called once per class node the
24
- # `node_rule` in `hanami.rb` dispatches, against every contract whose `path_glob` matches the
25
- # file. No cross-class-node state is needed, so the checker ships no `class_nodes` traversal of
26
- # its own.
23
+ # Per-`ClassNode` check over the engine-owned walk (ADR-37): called once per class node by the
24
+ # `node_rule` in `hanami.rb`, against every contract whose `path_glob` matches the file.
27
25
  def check_class(class_node, path:)
28
26
  @contracts.filter_map do |contract|
29
27
  next unless path_matches?(contract.path_glob, path)
@@ -87,11 +87,8 @@ module Rigor
87
87
  @protocol_contracts || manifest.protocol_contracts
88
88
  end
89
89
 
90
- # ADR-37 — per-class-node validation over the engine-owned walk. Each `Prism::ClassNode` is checked
91
- # against every contract independently, so the plugin no longer ships its own `class_nodes`
92
- # traversal; `ActionChecker#check_class` keeps the per-class contract logic. (A per-class contract
93
- # check is exactly what `node_rule` is for — the return type is void, so no `scope` query is
94
- # needed.)
90
+ # ADR-37 — per-class-node validation over the engine-owned walk; `ActionChecker#check_class` holds the
91
+ # per-class contract logic. The return type is void, so no `scope` query is needed.
95
92
  node_rule Prism::ClassNode do |node, _scope, path|
96
93
  contracts = protocol_contracts
97
94
  next [] if contracts.empty?
@@ -51,8 +51,8 @@ module Rigor
51
51
  #
52
52
  # - `is_a?(Result::Ok)` / `Some` / `None` exhaustive narrowing — core control-flow analysis over a sealed
53
53
  # hierarchy, not a plugin surface.
54
- # - The `variants do variant Const, Type end` Enum DSL is handled via ADR-36 `nested_class_templates:` in
55
- # this plugin's manifest (Slice A — see `nested_class_templates:` block below).
54
+ # - The `variants do variant Const, Type end` Enum DSL is not part of the unwrap contribution; it is
55
+ # synthesised through the ADR-36 `nested_class_templates:` in this plugin's manifest (Slice A).
56
56
  class Mangrove < Rigor::Plugin::Base
57
57
  manifest(
58
58
  id: "mangrove",
@@ -17,8 +17,8 @@ module Rigor
17
17
  #
18
18
  # assert_kind_of(T, x) → narrow x to T
19
19
  # assert_instance_of(T, x) → narrow x to T
20
- # assert_nil(x) → narrow x to Constant<nil>
21
- # assert_equal(literal, x) → meet x with Constant<literal> (see "Equality is not identity")
20
+ # assert_nil(x) → narrow x to nil
21
+ # assert_equal(literal, x) → meet x with the literal (see "Equality is not identity")
22
22
  # assert_match(regex, x) → narrow x to String
23
23
  #
24
24
  # ### (2) Minitest / Test::Unit `refute_*` / `assert_not_*` (negative) ###
@@ -26,7 +26,7 @@ module Rigor
26
26
  # refute_kind_of(T, x) → narrow x AWAY from T
27
27
  # refute_instance_of(T, x) → narrow x AWAY from T
28
28
  # refute_nil(x) → narrow x AWAY from nil
29
- # refute_equal(literal, x) → negative Constant<literal> fact (the engine keeps x unchanged)
29
+ # refute_equal(literal, x) → negative fact on the literal (the engine keeps x unchanged)
30
30
  #
31
31
  # Test::Unit's `assert_not_nil(x)` / `assert_not_equal(...)` / `assert_not_kind_of(...)` /
32
32
  # `assert_not_instance_of(...)` share the recognizer with `refute_*` (they're aliases).
@@ -35,8 +35,8 @@ module Rigor
35
35
  #
36
36
  # _(x).must_be_kind_of(T) → narrow x to T
37
37
  # _(x).must_be_instance_of(T) → narrow x to T
38
- # _(x).must_be_nil → narrow x to Constant<nil>
39
- # _(x).must_equal(literal) → meet x with Constant<literal> (see "Equality is not identity")
38
+ # _(x).must_be_nil → narrow x to nil
39
+ # _(x).must_equal(literal) → meet x with the literal (see "Equality is not identity")
40
40
  # _(x).must_match(regex) → narrow x to String
41
41
  #
42
42
  # The legacy bare `x.must_be_kind_of(T)` form (Minitest < 6.0 monkey-patched onto Object) is
@@ -48,7 +48,7 @@ module Rigor
48
48
  #
49
49
  # _(x).wont_be_kind_of(T) → narrow x AWAY from T
50
50
  # _(x).wont_be_nil → narrow x AWAY from nil
51
- # _(x).wont_equal(literal) → negative Constant<literal> fact (the engine keeps x unchanged)
51
+ # _(x).wont_equal(literal) → negative fact on the literal (the engine keeps x unchanged)
52
52
  #
53
53
  # ## Equality is not identity
54
54
  #
@@ -10,7 +10,8 @@ module Rigor
10
10
  module Plugin
11
11
  class Pundit < Rigor::Plugin::Base
12
12
  # Walks the configured policy-search paths via the plugin's `IoBoundary`, parses each `.rb` file with
13
- # Prism, and collects classes whose immediate superclass is one of the configured base classes.
13
+ # Prism, and collects classes whose immediate superclass is one of the configured base classes and
14
+ # whose name ends in `Policy`.
14
15
  #
15
16
  # For each discovered policy class, the discoverer collects every instance-side `def name?` predicate
16
17
  # method. Non-predicate methods (`initialize`, `resolve`, helper methods) are ignored — Pundit's
@@ -192,15 +192,10 @@ module Rigor
192
192
  first.unescaped
193
193
  end
194
194
 
195
- # Pulls the interpolation hash from the call's arguments. The trailing `Hash` argument (or
195
+ # Pulls the interpolation hash from the call's arguments. The trailing hash argument (or
196
196
  # `Prism::KeywordHashNode`) carries both reserved I18n options (`default:`, `scope:`, …) and
197
- # interpolation variables. Returns:
198
- # {
199
- # :has_default => bool,
200
- # :all_keys => Set<Symbol> (every assoc key in the hash),
201
- # :non_reserved => Set<Symbol> (keys NOT in RESERVED_OPTION_KEYS),
202
- # :hash_node => Prism::Node (or nil)
203
- # }
197
+ # interpolation variables. Returns `:has_default`, `:all_keys` (every assoc key), `:non_reserved`
198
+ # (keys outside RESERVED_OPTION_KEYS) and `:hash_node` (nil when there is no hash argument).
204
199
  #
205
200
  # Note: a reserved option key (e.g. `count:`) can also serve as an interpolation value when the
206
201
  # locale's leaf string has `%{count}`. The analyzer therefore checks the missing-placeholder set
@@ -11,9 +11,8 @@ module Rigor
11
11
  # placeholder names. The actual translated values are not retained — the analyzer doesn't need them
12
12
  # and keeping them would bloat the cache slice.
13
13
  class LocaleIndex
14
- # `placeholders` is a Hash: `locale_name => Set<String>`. `array_value` is true when at least one
15
- # locale's leaf is an Array (used by `l(time, format:)` and similar). `value_kinds` is a Hash:
16
- # `locale_name => Symbol` (`:string` / `:array` / `:hash`).
14
+ # `placeholders` maps each locale name to the `%{var}` placeholder names of its leaf. `value_kinds`
15
+ # maps each locale name to the leaf's kind (`:string` / `:array` / `:hash` / `:scalar`).
17
16
  Entry = Data.define(:dotted_key, :placeholders, :value_kinds) do
18
17
  def locales
19
18
  placeholders.keys
@@ -36,8 +36,8 @@ module Rigor
36
36
  end
37
37
 
38
38
  def load
39
- per_key = {} # dotted_key => { locale => Set<String> }
40
- per_key_kinds = {} # dotted_key => { locale => :string|:array|:hash }
39
+ per_key = {} # dotted_key => { locale => placeholder names }
40
+ per_key_kinds = {} # dotted_key => { locale => leaf kind }
41
41
  locales = Set.new
42
42
 
43
43
  locale_files.each do |path|
@@ -98,12 +98,9 @@ module Rigor
98
98
  # leaves are *not* recorded as entries themselves — only their descendants — but every leaf scalar
99
99
  # / array IS recorded.
100
100
  #
101
- # `breadcrumbs` is a single mutable stack reused across the whole walk (push before recursing, pop
102
- # after): for the 530-file / 14 MB Mastodon locale corpus the old
103
- # `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape allocated a fresh breadcrumb Array at
104
- # every node plus an intermediate result Array at every level — millions of short-lived objects and
105
- # the run's top allocation site. The dotted key is still materialised once per leaf (it has to
106
- # be); everything else is now allocation-free traversal.
101
+ # `breadcrumbs` is one mutable stack reused across the walk (push before recursing, pop after). A
102
+ # per-node Array allocation was the run's top allocation site on the 530-file Mastodon corpus; only
103
+ # the dotted key is materialised, once per leaf.
107
104
  def each_flattened(node, breadcrumbs, &)
108
105
  if node.is_a?(Hash)
109
106
  node.each do |k, v|
@@ -124,7 +124,6 @@ module Rigor
124
124
  if name
125
125
  names << name
126
126
  elsif call_node.name == :subject && call_node.arguments.nil?
127
- # Bare `subject { ... }` defines `:subject` itself.
128
127
  names << "subject"
129
128
  end
130
129
  end
@@ -201,8 +200,7 @@ module Rigor
201
200
  end
202
201
 
203
202
  def unknown_helper_violation(name, helper_table)
204
- # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester replaces the hand-rolled
205
- # Levenshtein this module used to carry.
203
+ # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester.
206
204
  suggestion = Rigor::Plugin::Base.suggest(name, helper_table.names)
207
205
  message = "no route helper `#{name}`"
208
206
  message += " (did you mean `#{suggestion}`?)" if suggestion