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
@@ -20,7 +20,7 @@ module Rigor
20
20
  # fact published by `rigor-rails-routes` (ADR-9 cross-plugin API) and validates every implicit-self
21
21
  # `*_path` / `*_url` call inside files under `controller_search_paths` (default `app/controllers`).
22
22
  #
23
- # Tier 2 of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
23
+ # Tier 2 of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
24
24
  # Phase 1 (strong-parameters → AR column validation), Phase 2 (filter chains), and Phase 3 (render
25
25
  # targets) ship as separate slices; each phase composes additively under the same plugin id.
26
26
  #
@@ -37,14 +37,10 @@ module Rigor
37
37
  #
38
38
  # ## What it checks
39
39
  #
40
- # - **Helper existence** — every `*_path` / `*_url` call inside a controller file is looked up in
41
- # the helper table. Missing entries emit `unknown-helper` with a `DidYouMean` suggestion drawn
42
- # from the table.
43
- # - **Helper arity** — the call's positional-argument count is matched against the helper's
44
- # recorded arity (a trailing `KeywordHashNode` like `users_path(format: :json)` is excluded; same
45
- # convention `rigor-rails-routes` uses). Mismatches emit `wrong-helper-arity`.
46
- # - **Trace** — recognised helpers also emit a `helper-call` info diagnostic naming the action and
47
- # path, mirroring the trace shape of the upstream plugin.
40
+ # - **Trace** — recognised `*_path` / `*_url` helpers emit a `helper-call` info diagnostic naming
41
+ # the action and path. Unknown helpers (`unknown-helper`) and arity mismatches (`wrong-arity`)
42
+ # are reported by `rigor-rails-routes`, which owns the helper table, so this plugin does not
43
+ # repeat them.
48
44
  #
49
45
  # ## Limitations
50
46
  #
@@ -88,7 +84,9 @@ module Rigor
88
84
  # the engine reads as "a template exists here and has no unit". The bump is belt-and-braces: what
89
85
  # actually invalidates is the claim itself, through the glob row in `TemplateUnits#digest` and the
90
86
  # `claimed_globs` equality the carry checks. A manifest version is in no cache key of its own.
91
- version: "1.5.0",
87
+ # Bumped 2026-10-27 (#2430) — `params[...]` answers the gradual `Dynamic[Parameters | nil]` rather than
88
+ # the bare union, so a cached 1.5.0 run carries the non-gradual answer at every `params[:k]` argument.
89
+ version: "1.6.0",
92
90
  description: "Validates Action Pack route-helper calls and filter chains inside controllers, and types the request-context readers (`params` / `session` / `request` / `flash`) and their chains.",
93
91
  config_schema: {
94
92
  "controller_search_paths" => { kind: :array, default: ["app/controllers"] },
@@ -475,7 +473,7 @@ module Rigor
475
473
  # `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
476
474
  # `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
477
475
  # so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
478
- # `.each`) to unprotected. These three methods return another `Parameters`, so gate on a
476
+ # `.each`) to unprotected. These methods return another `Parameters`, so gate on a
479
477
  # `Parameters` receiver and re-type the result as the same lenient nominal — the chain stays a
480
478
  # concrete receiver end-to-end and `coverage --protection` counts the sites as protected.
481
479
  #
@@ -497,9 +495,9 @@ module Rigor
497
495
  # entry FP-safe, because the RBS-less nominal buys leniency on the *method surface* only:
498
496
  #
499
497
  # - Method-surface rules DO decline on it. `call.undefined-method` bails at
500
- # `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb:720) because Rigor
501
- # ships no RBS for Parameters, and the argument / arity rules gate on the same predicate
502
- # (:1151, :2126). So `params.slice(:a).whatever_rails_adds` never fires, and a scalar-shaped
498
+ # `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb) because Rigor
499
+ # ships no RBS for Parameters, and the argument / arity rules gate on the same predicate. So
500
+ # `params.slice(:a).whatever_rails_adds` never fires, and a scalar-shaped
503
501
  # use of a Parameters-typed value (`.to_i`, `.strip`, string interpolation) resolves lenient
504
502
  # -to-Dynamic rather than to a diagnostic.
505
503
  # - **Flow rules do NOT.** Truthiness folding, ternary arm selection and `.nil?` folding key on
@@ -542,7 +540,7 @@ module Rigor
542
540
  # and still carries the chain — `params[:user][:name]` types `Parameters?` and
543
541
  # `params[:user].permit(:name)` types `Parameters`, because the dispatcher strips the nil arm
544
542
  # for the receiver gate. Idiomatic *chained* reads stay silent too: `call.possible-nil-receiver`
545
- # restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`'s local-read restriction), so
543
+ # restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`), so
546
544
  # `params[:q].strip` cannot fire. What it does fire on is the assigned form — `q = params[:q];
547
545
  # q.strip` — at **error** severity, once per unguarded use. Guarded forms (`return if q.nil?`,
548
546
  # `if q`, `q&.strip`, `|| ""`) all narrow correctly and stay silent, so this is not a broken
@@ -564,7 +562,8 @@ module Rigor
564
562
  # The control specs stay, and they still bite: they pin the HAZARD (no truthiness fold, no `bot`
565
563
  # branch, no possible-nil firing), not the Dynamic answer, so they go red if either typing's
566
564
  # hazard ever returns — and they now carry a positive assertion that `params[:k]` types
567
- # `Parameters?`, so a silent regression to `Dynamic` or to the non-nil nominal fails them too.
565
+ # `Dynamic[ActionController::Parameters?]` (#2430), so a silent regression to bare `Dynamic[top]`
566
+ # or to the non-nil nominal fails them too.
568
567
  STRONG_PARAMS_CHAIN_METHODS = %i[
569
568
  require permit permit! expect slice
570
569
  except without extract! slice!
@@ -578,6 +577,16 @@ module Rigor
578
577
  Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
579
578
  end
580
579
 
580
+ # Issue #2430 — the chain on a `params[...]` carrier (`params[:user].permit(:name)`). The result stays
581
+ # a `Dynamic` of the same nominal, so it crosses a declared boundary as the carrier does.
582
+ dynamic_return methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, scope|
583
+ next nil unless call_node.is_a?(Prism::CallNode) && call_node.receiver
584
+ next nil unless controller_scope?(scope)
585
+ next nil unless params_carrier_facet?(scope.type_of(call_node.receiver))
586
+
587
+ Rigor::Type::Combinator.dynamic(Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]))
588
+ end
589
+
581
590
  # Issue #534 item 1, reopened by #574. `#[]` is the single largest named-receiver pair on both survey
582
591
  # apps (redmine 581, mastodon ~475). The honest contract is `Parameters | nil` — a missing key really
583
592
  # returns nil — and the block comment above records why that typing was REJECTED in 2026-09-01: it put
@@ -593,15 +602,53 @@ module Rigor
593
602
  #
594
603
  # The non-nil `-> Parameters` typing stays rejected on its own merits, unchanged: it makes every
595
604
  # `params[:k]` condition always-truthy and folds a ternary over one to its true arm.
605
+ #
606
+ # Issue #2430 — the answer is a GRADUAL carrier, `Dynamic[Parameters | nil]`, not the bare union.
607
+ # A declared boundary (a `String` parameter, a `-> String` return) rejects the bare union, because
608
+ # `Acceptance` passes only a `Dynamic` through every boundary, and a scalar param is a `String` at
609
+ # runtime. The static facet keeps the strong-params chain typed: the carrier-gated rules below
610
+ # dispatch on the facet, so `params[:user][:name]` and `params[:user].permit(:name)` keep resolving.
611
+ # The control hazards (no truthiness fold, no `bot`, no possible-nil on `q = params[:q]`) are
612
+ # unchanged, because the facet is the same `Parameters | nil` union.
596
613
  dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: %i[[]] do |call_node, _scope|
597
614
  next nil unless call_node.is_a?(Prism::CallNode)
598
615
 
599
- Rigor::Type::Combinator.union(
600
- Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
601
- Rigor::Type::Combinator.constant_of(nil)
616
+ params_read_type
617
+ end
618
+
619
+ # Issue #2430 — `Dynamic` is not a `Nominal`, so the receiver-gated rules never see a carrier
620
+ # receiver. These two rules gate on the carrier's facet instead, and only inside a controller: a
621
+ # carrier is minted by the `params` reader, which is controller-only, so no other scope can hold one.
622
+ dynamic_return methods: %i[[]] do |call_node, scope|
623
+ next nil unless call_node.is_a?(Prism::CallNode) && call_node.receiver
624
+ next nil unless controller_scope?(scope)
625
+ next nil unless params_carrier_facet?(scope.type_of(call_node.receiver))
626
+
627
+ params_read_type
628
+ end
629
+
630
+ # The `params` carrier's static facet as a `Dynamic`: `Parameters | nil`, gradual at every boundary.
631
+ def params_read_type
632
+ Rigor::Type::Combinator.dynamic(
633
+ Rigor::Type::Combinator.union(
634
+ Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
635
+ Rigor::Type::Combinator.constant_of(nil)
636
+ )
602
637
  )
603
638
  end
604
639
 
640
+ # True when `type` is a `Dynamic` whose facet is `Parameters` or `Parameters | nil`: the carrier a
641
+ # `params[...]` read answers, and the value a chained `[]` / strong-params call receives.
642
+ def params_carrier_facet?(type)
643
+ return false unless type.is_a?(Rigor::Type::Dynamic)
644
+
645
+ facet = type.static_facet
646
+ members = facet.is_a?(Rigor::Type::Union) ? facet.members : [facet]
647
+ non_nil = members.reject { |member| member.is_a?(Rigor::Type::Constant) && member.value.nil? }
648
+ non_nil.size == 1 && non_nil.first.is_a?(Rigor::Type::Nominal) &&
649
+ non_nil.first.class_name == REQUEST_CONTEXT_READER_TYPES[:params]
650
+ end
651
+
605
652
  # Phase 5c (2026-09-02, issue #534 "same lane") — the rest of the request-context surface the
606
653
  # corpus sweep measured after the #578 / #585 batch: `request.post?` 28 and `request.xhr?` 27 on
607
654
  # the two apps, `flash.now` inside the redmine 147 / mastodon 29 flash cluster. Each is typed from
@@ -35,7 +35,7 @@ module Rigor
35
35
 
36
36
  # Where a Rails app declares its adapter. Per-environment files are read too, because an app that
37
37
  # sets the adapter only in `config/environments/production.rb` is common; agreement across every
38
- # file that names one is what licenses the narrowing (see {.transport_for}).
38
+ # file that names one is what licenses the narrowing (see {.detect_adapter}).
39
39
  CONFIG_PATHS = ["config/application.rb", "config/environments"].freeze
40
40
 
41
41
  SETTING = /queue_adapter\s*=\s*[:"']([a-z_]+)["']?/
@@ -13,7 +13,7 @@ module Rigor
13
13
  # rigor-activejob — validates `Job.perform_later(...)` / `.perform_now(...)` / `.perform(...)`
14
14
  # argument arity against the discovered `#perform` definitions.
15
15
  #
16
- # Tier 1D of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
16
+ # Tier 1D of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
17
  # Statically discovers ActiveJob subclasses by walking the configured `job_search_paths` and
18
18
  # parsing each file with Prism — no `active_job` runtime dependency.
19
19
  #
@@ -98,9 +98,10 @@ module Rigor
98
98
  # Cached separately from `:job_index` because the two invalidate on different files: editing a
99
99
  # schedule changes which jobs are reached without touching `app/jobs` at all. The `watch:` roots the
100
100
  # glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
101
- # configured path is escaped to a literal name, so `recurring[1].yml` is watched as that file.
101
+ # configured path is escaped to a literal name, so `recurring[1].yml` is watched as that file, and an
102
+ # absolute path is watched under its own directory (`RootedGlob.split_file`, #2222).
102
103
  producer :recurring_jobs,
103
- watch: -> { [[".", *@recurring_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
104
+ watch: -> { @recurring_paths.map { |path| Rigor::RootedGlob.split_file(path) } } do |_params|
104
105
  RecurringScan.new(
105
106
  io_boundary: io_boundary,
106
107
  recurring_paths: @recurring_paths
@@ -20,8 +20,9 @@ module Rigor
20
20
  #
21
21
  # Declined — no root, never a guess: a polymorphic association (no single target), a non-literal
22
22
  # `class_name:` / `**opts`, `anonymous_class:`, a `with_options` group with non-literal options, and a
23
- # `through:` association without a literal `class_name:` / `source_type:`. Without them a `through:` target is the SOURCE association's class on the through model, and
24
- # that association (declared there, hence in this same index) roots it itself.
23
+ # `through:` association without a literal `class_name:` / `source_type:`. Without them a `through:`
24
+ # target is the SOURCE association's class on the through model, and that association (declared
25
+ # there, hence in this same index) roots it itself.
25
26
  #
26
27
  # A computed name equal to the owner's own demodulized name declines too (Rails 7.2+ tries `::Name`
27
28
  # first; the target is then the owner itself or a top-level class).
@@ -28,8 +28,8 @@ module Rigor
28
28
  #
29
29
  # An `ActiveRecord::Base` row reaches `User.find` through the project's own
30
30
  # `User < ApplicationRecord < ActiveRecord::Base` lines, so one row covers every model in the app.
31
- # That is the whole economy of the framework layer: thirty-odd rows here colour the several thousand
32
- # database touches a Rails app performs, and none of them needed a line of application code.
31
+ # That is the whole economy of the framework layer: a fixed set of rows here colours the several
32
+ # thousand database touches a Rails app performs, and none of them needed a line of application code.
33
33
  module Effects
34
34
  BASE = "ActiveRecord::Base"
35
35
  RELATION = "ActiveRecord::Relation"
@@ -186,27 +186,20 @@ module Rigor
186
186
  merge_redeclarations(rows)
187
187
  end
188
188
 
189
- # Collapses the rows that declare the SAME constant into one, so {ModelIndex.build} — which keys
190
- # its entries by `class_name` — receives at most one row per model.
189
+ # Collapses the rows that declare the SAME constant into one, so {ModelIndex.build} (keyed by
190
+ # `class_name`) receives at most one row per model.
191
191
  #
192
- # A model is routinely declared more than once: `app/models/user.rb` holds `class User <
193
- # ApplicationRecord` and a second file reopens it (`class ::User`, `class User`, or a full
194
- # `class User < ApplicationRecord` redeclaration) to add a method. Ruby's own semantics are
195
- # ADDITIVE — a reopen contributes what it spells and leaves the rest of the class alone — so the
196
- # rows are UNIONed here. Taking the last row instead dropped the real declaration's associations,
197
- # scopes, enums, validations, callbacks and `alias_attribute`s whenever the reopen sorted later in
198
- # the glob, and `where(<a declared alias>: …)` then surfaced a false `unknown-column` on correct
199
- # code; taking the first dropped whatever the reopen added. Neither order loses anything now.
192
+ # A model is routinely declared more than once (`app/models/user.rb` plus a reopening file). Ruby's
193
+ # semantics are ADDITIVE, so the rows are UNIONed. Taking the last row dropped the real declaration's
194
+ # associations, scopes, enums, validations, callbacks and `alias_attribute`s whenever the reopen sorted
195
+ # later, producing false `unknown-column`s; taking the first dropped what the reopen added.
200
196
  #
201
- # Merge is by field: the first non-nil `superclass_name` wins (a second declaration is the same
202
- # thing, or invalid Ruby); `self.table_name =` is an ASSIGNMENT, so the LATER declaration's
203
- # `table_name_override` wins as it does at load time, and when the two declarations disagree the
204
- # resolved name is marked computed so no value is pinned — the glob order is not the load order for
205
- # two full declarations, and a pinned wrong literal folds `Model.table_name == "…"` on correct code;
206
- # `table_name_computed` is otherwise an OR (any computed name in the class makes the resolved one
207
- # inexact); name-keyed rows (associations, enums, aliases) let the LAST declaration override an
208
- # earlier same-name row exactly as {ModelIndex.merge_named_rows} does across an STI chain, and the
209
- # plain lists union.
197
+ # Merge is by field: the first non-nil `superclass_name` wins. `self.table_name =` is an ASSIGNMENT, so
198
+ # the later `table_name_override` wins, but when two declarations disagree the name is marked computed
199
+ # so no value is pinned: glob order is not load order, and a pinned wrong literal would fold
200
+ # `Model.table_name == "…"` on correct code. `table_name_computed` is otherwise an OR. Name-keyed rows
201
+ # let the LAST declaration override an earlier same-name row, as {ModelIndex.merge_named_rows} does
202
+ # across an STI chain; plain lists union.
210
203
  def merge_redeclarations(rows)
211
204
  return rows if rows.length < 2
212
205
 
@@ -459,7 +452,6 @@ module Rigor
459
452
  macro_methods: lookup_macro_methods(node.body)
460
453
  })
461
454
 
462
- # Recurse into the body in case nested classes exist.
463
455
  walk_for_classes(node.body, [full_name], &) if node.body
464
456
  end
465
457
 
@@ -869,8 +861,8 @@ module Rigor
869
861
 
870
862
  # Walks the namespace body's TOP-LEVEL statements only — a decorator declared inside a nested
871
863
  # `module`/`class` belongs to THAT namespace, not this one, and must not be attributed here.
872
- # Returns `Hash<:prefix|:suffix => { literal: } | { computed: true }>`, only for the names this body
873
- # actually mentions. A later statement overrides an earlier one within the SAME body — real Ruby
864
+ # Returns the decorators keyed by `:prefix` / `:suffix`, only for the names this body actually mentions.
865
+ # A later statement overrides an earlier one within the SAME body — real Ruby
874
866
  # assignment order, not a guess: `mattr_accessor :table_name_prefix` (no `default:`, so `computed:
875
867
  # true`) followed two lines down by a real `self.table_name_prefix = "blog_"` folds to the literal.
876
868
  #
@@ -1234,8 +1226,8 @@ module Rigor
1234
1226
  end
1235
1227
 
1236
1228
  # Recognised single-instance and collection association DSL methods. The kind drives the eventual
1237
- # return-type contribution: singular associations narrow to `Nominal[Target] | nil`, plural ones
1238
- # narrow to `ActiveRecord::Relation[Target]`.
1229
+ # return-type contribution: singular associations narrow to the target or nil, plural ones to a
1230
+ # relation of the target.
1239
1231
  #
1240
1232
  # `composed_of` value-object aggregations and `delegated_type` roles are folded in here too — both
1241
1233
  # accept the association name as a `where` / `find_by` query key, so omitting them turns every
@@ -1336,7 +1328,7 @@ module Rigor
1336
1328
  association_option(args, "polymorphic") == true
1337
1329
 
1338
1330
  # A polymorphic association has no single static target class — `target` is nil and the flow
1339
- # contribution declines to narrow rather than inventing a wrong `Nominal[<classified-name>]`.
1331
+ # contribution declines to narrow rather than inventing a wrong class-name target.
1340
1332
  if polymorphic
1341
1333
  target = nil
1342
1334
  else
@@ -1460,8 +1452,7 @@ module Rigor
1460
1452
  end
1461
1453
 
1462
1454
  # `enum status: { active: 0, archived: 1 }` (Rails ≤6) and `enum :status, [:active, :archived]`
1463
- # (Rails 7+). Returns `Hash<column_name => Array<Symbol>>`. Non-literal forms decline rather than
1464
- # guess.
1455
+ # (Rails 7+). Non-literal forms decline rather than guess.
1465
1456
  def lookup_enums(body)
1466
1457
  return {} if body.nil?
1467
1458
 
@@ -1516,7 +1507,7 @@ module Rigor
1516
1507
  end
1517
1508
 
1518
1509
  # `scope :active, -> { ... }`. Records the scope name only (the body is intentionally NOT
1519
- # introspected — the caller contributes `ActiveRecord::Relation[Model]` based on the name alone
1510
+ # introspected — the caller contributes a relation of the model based on the name alone
1520
1511
  # via `class_scope_return_type`).
1521
1512
  def lookup_scopes(body)
1522
1513
  return [] if body.nil?
@@ -1585,8 +1576,7 @@ module Rigor
1585
1576
 
1586
1577
  # `alias_attribute :new_name, :old_name`. Records the mapping so the analyzer accepts the alias
1587
1578
  # as a query key — without it every `where(<alias>: ...)` / `find_by(<alias>: ...)` call surfaces
1588
- # as a false `unknown-column`. Returns `Hash<alias => target>`; non-Symbol-literal forms decline
1589
- # rather than guess.
1579
+ # as a false `unknown-column`. Non-Symbol-literal forms decline rather than guess.
1590
1580
  def lookup_aliases(body)
1591
1581
  return {} if body.nil?
1592
1582
 
@@ -1626,7 +1616,7 @@ module Rigor
1626
1616
  # macro and `images`, `images=`, `images_attachments`, `images_blobs` for the collection one — the
1627
1617
  # `has_one_attached` / `has_many_attached` generated-methods surface, none of it column-backed.
1628
1618
  # This is name MEMBERSHIP only, and does not overlap `rigor-activestorage`, which contributes the
1629
- # accessor's TYPE (`ActiveStorage::Attached::One`) and publishes no fact: a consumer that reads
1619
+ # accessor's type and publishes no fact: a consumer that reads
1630
1620
  # `:model_index` to ask "does this model answer `avatar`?" gets nothing from that plugin, and gets
1631
1621
  # nothing at all when the project does not load it.
1632
1622
  ATTACHMENT_MACROS = {
@@ -29,14 +29,12 @@ module Rigor
29
29
  # {Entry} resolves its table from the root model and inherits the chain's declared associations /
30
30
  # enums / aliases / scopes / validations / callbacks / macro methods.
31
31
  class ModelIndex
32
- # `associations` is a frozen `Array<Hash>` where each row carries `{ name:, kind:, target:,
33
- # nullable: }`:
32
+ # `associations` is a frozen list of rows `{ name:, kind:, target:, nullable: }`:
34
33
  #
35
- # - `name` — String, the association method name as the user invokes it (`"posts"`).
34
+ # - `name` — the association method name as the user invokes it (`"posts"`).
36
35
  # - `kind` — `:singular` (`belongs_to` / `has_one`) or `:collection` (`has_many`).
37
- # - `target` — String, the target class name resolved either from an explicit `class_name:`
38
- # option or via {Inflector.classify}.
39
- # - `nullable` — Boolean; whether a `:singular` accessor can return `nil`. `has_one` → `true`;
36
+ # - `target` — the target class name, from an explicit `class_name:` option or via {Inflector.classify}.
37
+ # - `nullable` — whether a `:singular` accessor can return `nil`. `has_one` → `true`;
40
38
  # `belongs_to` → `false` (required by default since Rails 5) unless `optional: true`
41
39
  # / `required: false`. Meaningless for `:collection` rows.
42
40
  #
@@ -71,27 +69,23 @@ module Rigor
71
69
 
72
70
  def association_names = associations.map { |a| a[:name] }
73
71
 
74
- # `enums` is `Hash<column_name => Array<value_name>>`.
72
+ # `enums` maps a column name to its value names.
75
73
  def enum?(name) = enums.key?(name.to_s)
76
74
  def enum_values(name) = enums.fetch(name.to_s, [])
77
75
 
78
- # `scopes` is `Array<scope_name>`.
79
76
  def scope?(name) = scopes.include?(name.to_s)
80
77
 
81
- # `validations` is `Array<attribute_name>` covering both `validates :name, ...` and the
82
- # `validates_*_of :name, ...` shorthand families.
78
+ # `validations` covers both `validates :name, ...` and the `validates_*_of :name, ...` shorthand families.
83
79
  def validation?(name) = validations.include?(name.to_s)
84
80
  def validated_attributes = validations
85
81
 
86
- # `callbacks` is `Array<{ name:, callback: }>`.
87
82
  def callback_targets = callbacks.map { |c| c[:name] }
88
83
 
89
- # `aliases` is `Hash<alias_name => target_attribute>` populated from `alias_attribute`
90
- # declarations.
84
+ # `aliases` maps an alias name to its target attribute, from `alias_attribute` declarations.
91
85
  def alias?(name) = aliases.key?(name.to_s)
92
86
  def resolve_alias(name) = aliases[name.to_s]
93
87
 
94
- # `macro_methods` is `Array<method_name>` — the instance methods a declaration macro installs that
88
+ # `macro_methods` lists the instance methods a declaration macro installs that
95
89
  # none of the sets above already carries (#1049): `delegate`'s names, the Paperclip / Active
96
90
  # Storage attachment readers, and an `enum`'s per-value predicates. Names only, with no type: a
97
91
  # consumer asks "does this model answer `followers_count`?", which is what a fail-closed check
@@ -312,7 +306,7 @@ module Rigor
312
306
  seen.values.map(&:freeze).freeze
313
307
  end
314
308
 
315
- # `Hash<column => Array<value>>` merged across the STI chain; a child redeclaration of the same
309
+ # Enum value lists merged across the STI chain; a child redeclaration of the same
316
310
  # enum column overrides the ancestor's value list.
317
311
  def self.merge_enums(chain)
318
312
  chain.each_with_object({}) do |row, acc|
@@ -320,7 +314,7 @@ module Rigor
320
314
  end.freeze
321
315
  end
322
316
 
323
- # `Hash<alias => target>` merged across the STI chain.
317
+ # Alias map merged across the STI chain.
324
318
  def self.merge_aliases(chain)
325
319
  chain.each_with_object({}) do |row, acc|
326
320
  (row[:aliases] || {}).each { |name, target| acc[name.to_s] = target.to_s }
@@ -108,9 +108,8 @@ module Rigor
108
108
  false
109
109
  end
110
110
 
111
- # Walks the block body collecting `t.<method>(...)` calls. Skips nested blocks (e.g. inside
112
- # `if`-conditioned columns) only at the top level — for richer schema constructs the parser falls
113
- # back silently.
111
+ # Walks the block body collecting `t.<method>(...)` calls. It descends through nested constructs
112
+ # (e.g. `if` branches) but does not recurse into a matched call.
114
113
  def collect_column_calls(node, &)
115
114
  return if node.nil?
116
115
 
@@ -21,7 +21,6 @@ module Rigor
21
21
  end
22
22
  end
23
23
 
24
- # Map ActiveRecord column types → Ruby class names.
25
24
  RUBY_TYPE_MAPPING = {
26
25
  string: "String",
27
26
  text: "String",
@@ -59,8 +59,8 @@ module Rigor
59
59
  # capital R) is intentional — keeps the constant lookup distinct from `::ActiveRecord` even though the
60
60
  # gem name is hyphenated.
61
61
  #
62
- # Note: this plugin is the seventh worked example. It does NOT require `active_record` at runtime — it
63
- # only reads project source, the same way the other examples do. Rigor stays decoupled from Rails.
62
+ # Note: this plugin does NOT require `active_record` at runtime — it only reads project source, the
63
+ # same way the other examples do. Rigor stays decoupled from Rails.
64
64
  class Activerecord < Rigor::Plugin::Base
65
65
  manifest(
66
66
  id: "activerecord",
@@ -553,8 +553,9 @@ module Rigor
553
553
  end
554
554
 
555
555
  # Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
556
- # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model; `find_by` adds the `nil` arm; `where` /
557
- # `all` / `order` / `limit` / `none` open a relation. The relation then carries its element type
556
+ # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model;
557
+ # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
558
+ # relation then carries its element type
558
559
  # through any further chained query method via the bundled `ActiveRecord::Relation` RBS.
559
560
  def finder_return_type(call_node, entry, scope)
560
561
  case call_node.name
@@ -105,10 +105,9 @@ module Rigor
105
105
  walk(node.body, inner_path, &) if node.body
106
106
  end
107
107
 
108
- # The full constant name a `class` / `module` declaration defines, given the rendered local name
109
- # and the enclosing lexical path. A ROOTED local name (`class ::User`) names the top-level constant
110
- # whatever the nesting, so the lexical path is dropped and the `::` with it; otherwise the name is
111
- # appended to the path (`class User` inside `module Admin` → `Admin::User`).
108
+ # The full constant name a `class` / `module` declaration defines. A ROOTED local name (`class ::User`)
109
+ # names the top-level constant whatever the nesting, so the lexical path is dropped; otherwise the
110
+ # name is appended to it (`class User` inside `module Admin` → `Admin::User`).
112
111
  def declared_constant_name(local_name, lexical_path)
113
112
  return local_name.delete_prefix("::") if local_name.start_with?("::")
114
113
 
@@ -8,7 +8,7 @@ module Rigor
8
8
  #
9
9
  # Each row is `{ name:, kind: }`:
10
10
  #
11
- # - `name` — String, the attachment method name as the user invokes it (`"avatar"`).
11
+ # - `name` — the attachment method name as the user invokes it (`"avatar"`).
12
12
  # - `kind` — `:singular` (`has_one_attached`) or `:collection` (`has_many_attached`).
13
13
  class AttachmentIndex
14
14
  attr_reader :entries
@@ -137,19 +137,14 @@ module Rigor
137
137
  nil
138
138
  end
139
139
 
140
- # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not about the
141
- # file being analysed, so both outcomes are registered rather than returned: see
142
- # {Plugin::Base#disclose_once} for the channel and the position. Returned from the per-file hook they
143
- # carried no once-guard at all — the `.uniq` collapsed repeats within one instance's list, but the
144
- # whole list was re-emitted on every analysed file.
140
+ # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not the file
141
+ # being analysed, so it is registered via {Plugin::Base#disclose_once} rather than returned from the
142
+ # per-file hook (which had no once-guard and re-emitted the list on every file).
145
143
  #
146
- # The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
147
- # (#1051), and a refused read must still precede a discovery failure the way `@load_errors` records
148
- # them — `@load_errors` is now keyed by that same string, which also caps it: `#attachment_index`
149
- # re-attempts the load per call site, and the old Array appended a fresh interpolated string every
150
- # time (the unbounded-growth shape #569 measured at 4.2 M retained strings on Redmine). The key, not
151
- # the message, is the identity: a second refusal naming a different path collapses into the first row
152
- # rather than adding one — the notice is "the index did not load", said once.
144
+ # The `key`s carry an ordinal prefix because the engine emits disclosures in KEY order (#1051), and
145
+ # a refused read must precede a discovery failure. `@load_errors` is keyed by the same string, which
146
+ # caps it: `#attachment_index` re-attempts the load per call site (#569). The key, not the message,
147
+ # is the identity, so a second refusal collapses into the first row.
153
148
  def disclose_load_errors
154
149
  @load_errors.each do |key, message|
155
150
  disclose_once(key, message: message, severity: :warning, rule: "load-error")
@@ -3,4 +3,4 @@
3
3
  # Entry point matching the `rigor-<id>` gem-name convention. The Plugin::Loader's `require_gem!` step
4
4
  # calls `require "rigor-activestorage"`, and this file loads the plugin's actual implementation under
5
5
  # the `rigor/plugin/` namespace so the require / register pair fires.
6
- require "rigor/plugin/activestorage"
6
+ require_relative "rigor/plugin/activestorage"
@@ -12,7 +12,7 @@ module Rigor
12
12
  # the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
13
13
  # #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
14
14
  # to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
15
- # One row below (`in_time_zone_row`) is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
15
+ # The `in_time_zone_rows` group is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
16
16
  # through a default argument, a gap the RBS sweep found but can only fix here.
17
17
  #
18
18
  # ## The clock
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/type"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class ActivesupportCoreExt < Rigor::Plugin::Base
8
+ # #2433 — the result type of `Hash#symbolize_keys` / `#to_options`.
9
+ #
10
+ # ActiveSupport implements them as `transform_keys { |key| key.to_sym rescue key }`, so a String or
11
+ # Symbol key becomes a Symbol and a key without `to_sym` (an Integer, a Float) survives unchanged. One
12
+ # RBS return type cannot say that: `Hash[Symbol, V]` reads an Integer key as a Symbol, and
13
+ # `Hash[Symbol | K, V]` makes a String-keyed hash fail a declared `Hash[Symbol, untyped]` parameter.
14
+ # So the RBS rows answer `Hash[untyped, V]` and this module maps each key member where it can.
15
+ #
16
+ # A key that is not a String or Symbol (an Integer, a project class) reads `Dynamic[top]`: no Rigor fact proves
17
+ # a class has no `to_sym` (a mixin, a `pre_eval` file or a reopening can add one), so a kept key could claim
18
+ # a type the program does not have. It declines (`nil`, the RBS row answers) when a receiver member is
19
+ # neither a `Hash[K, V]` nor `nil`. A `nil` member is dropped: `nil.symbolize_keys` raises, and the engine
20
+ # adds `nil` back for `&.`.
21
+ module SymbolizeKeys
22
+ HASH = "Hash"
23
+ SYMBOLIZED_CLASSES = %w[String Symbol].freeze
24
+
25
+ module_function
26
+
27
+ # Answers `Hash[K', V]` (the union of such over the receiver's Hash members), or nil to decline.
28
+ def result(receiver_type, scope)
29
+ members = receiver_type.is_a?(Rigor::Type::Union) ? receiver_type.members : [receiver_type]
30
+ answers = members.reject { |member| nil_member?(member) }.map { |member| member_answer(member, scope) }
31
+ return nil if answers.empty? || answers.any?(:decline)
32
+
33
+ hashes = answers.reject { |answer| answer == :empty }
34
+ hashes.empty? ? nil : Rigor::Type::Combinator.union(*hashes)
35
+ end
36
+
37
+ # An empty closed shape is `{}`: it adds no key, so it drops out of the union rather than declining it.
38
+ def member_answer(member, scope)
39
+ return :empty if member.is_a?(Rigor::Type::HashShape) && member.closed? && member.pairs.empty?
40
+
41
+ key_type, value_type = hash_arguments(member)
42
+ return :decline if key_type.nil?
43
+
44
+ Rigor::Type::Combinator.nominal_of(HASH, type_args: [map_key(key_type, scope), value_type])
45
+ end
46
+
47
+ def nil_member?(type)
48
+ (type.is_a?(Rigor::Type::Constant) && type.value.nil?) ||
49
+ (type.is_a?(Rigor::Type::Nominal) && type.class_name == "NilClass")
50
+ end
51
+
52
+ def hash_arguments(type)
53
+ case type
54
+ when Rigor::Type::Nominal
55
+ type.class_name == HASH && type.type_args.size == 2 ? type.type_args : nil
56
+ when Rigor::Type::Difference
57
+ hash_arguments(type.base)
58
+ when Rigor::Type::HashShape
59
+ shape_arguments(type)
60
+ end
61
+ end
62
+
63
+ # A closed, non-empty shape reads as `Hash[<its keys>, <union of its values>]`.
64
+ def shape_arguments(shape)
65
+ return nil if shape.open? || shape.pairs.empty?
66
+
67
+ keys = shape.pairs.keys.map { |key| Rigor::Type::Combinator.constant_of(key) }
68
+ [Rigor::Type::Combinator.union(*keys), Rigor::Type::Combinator.union(*shape.pairs.values)]
69
+ end
70
+
71
+ def map_key(type, scope)
72
+ members = type.is_a?(Rigor::Type::Union) ? type.members : [type]
73
+ Rigor::Type::Combinator.union(*members.map { |member| map_member(member, scope) })
74
+ end
75
+
76
+ def map_member(member, scope)
77
+ case member
78
+ when Rigor::Type::Constant
79
+ member.value.is_a?(String) || member.value.is_a?(Symbol) ? symbol : Rigor::Type::Combinator.untyped
80
+ when Rigor::Type::Nominal
81
+ symbolized_class?(member.class_name, scope) ? symbol : Rigor::Type::Combinator.untyped
82
+ else Rigor::Type::Combinator.untyped
83
+ end
84
+ end
85
+
86
+ def symbol
87
+ Rigor::Type::Combinator.nominal_of("Symbol")
88
+ end
89
+
90
+ # String, Symbol and their subclasses go through `to_sym`.
91
+ def symbolized_class?(name, scope)
92
+ SYMBOLIZED_CLASSES.include?(name) ||
93
+ SYMBOLIZED_CLASSES.any? { |base| %i[equal subclass].include?(ordering(name, base, scope)) }
94
+ end
95
+
96
+ def ordering(name, base, scope)
97
+ Rigor::Reflection.class_ordering(name, base, scope: scope)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end