rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -161,9 +161,8 @@ module Rigor
161
161
  end
162
162
 
163
163
  def project_defines_object?(serializer_name, scope)
164
- return false unless scope.respond_to?(:user_def_through_ancestors)
165
-
166
- !scope.user_def_through_ancestors(serializer_name, :object).first.nil?
164
+ Rigor::Reflection.project_method_defined(serializer_name, :object, kind: :instance, source: :def,
165
+ scope: scope).yes?
167
166
  end
168
167
 
169
168
  # The model a serializer serializes, or nil where nothing corroborates it. Memoised per serializer
@@ -237,13 +236,11 @@ module Rigor
237
236
  # the check would decline nearly every real serializer. On the serializer side it is a base
238
237
  # serializer's own rendering method, which is not a read of the resource at all.
239
238
  def project_defines?(class_name, method_name, scope)
240
- return false unless scope.respond_to?(:user_def_through_ancestors)
241
-
242
239
  # A name no Symbol can hold cannot be a method the project defines (#1831): decline, do not raise.
243
240
  method = Rigor::Source::SymbolName.of(method_name.to_s)
244
241
  return false if method.nil?
245
242
 
246
- !scope.user_def_through_ancestors(class_name, method).first.nil?
243
+ Rigor::Reflection.project_method_defined(class_name, method, kind: :instance, source: :def, scope: scope).yes?
247
244
  end
248
245
 
249
246
  # `REST::AccountSerializer` offers two readings — the namespaced `REST::Account` and the
@@ -55,8 +55,24 @@ module Rigor
55
55
  )
56
56
  end
57
57
 
58
+ # Ruby binds a trailing `k: v` / `**opts` group to keyword parameters, never to positional ones, so
59
+ # when `#perform` takes keywords the group is not counted (the message's "got N" is then the
60
+ # positional count too). A job without keyword parameters keeps counting the group as one positional
61
+ # argument — `NoArgsJob.perform_later(name: 1)` raises at runtime. A `Hash` literal / local stays
62
+ # positional either way (Ruby 3 does not convert it).
63
+ #
64
+ # The call is declined (no report) when its shape is unknown: a `*splat` or `...` argument may supply
65
+ # any number of positionals (or a trailing keyword group), and a keyword group made only of `**opts`
66
+ # arguments sent to a job without keywords may be empty — declined only when the verdict differs
67
+ # between "empty" and "one positional hash" (`NoArgsJob.perform_later(**opts)`).
58
68
  def arity_violation(call_node, entry)
59
- actual = (call_node.arguments&.arguments || []).size
69
+ args = call_node.arguments&.arguments || []
70
+ return nil if args.any? { |arg| arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode) }
71
+
72
+ keyword_group = args.last.is_a?(Prism::KeywordHashNode) ? args.last : nil
73
+ return nil if keyword_group && !entry.accepts_keywords && empty_group_decides?(args, keyword_group, entry)
74
+
75
+ actual = entry.accepts_keywords && keyword_group ? args.size - 1 : args.size
60
76
  return nil if entry.accepts?(actual)
61
77
 
62
78
  Violation.new(
@@ -66,6 +82,14 @@ module Rigor
66
82
  )
67
83
  end
68
84
 
85
+ # True when the group is only `**splat` arguments (so it may be empty) AND the entry's verdict changes
86
+ # between counting it and not counting it.
87
+ def empty_group_decides?(args, keyword_group, entry)
88
+ return false unless keyword_group.elements.all?(Prism::AssocSplatNode)
89
+
90
+ entry.accepts?(args.size - 1) != entry.accepts?(args.size)
91
+ end
92
+
69
93
  # Renders a constant-path receiver as a String. Mirrors the helpers in rigor-activerecord /
70
94
  # rigor-rails-routes for parity.
71
95
  def constant_receiver_name(node)
@@ -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_]+)["']?/
@@ -76,7 +76,8 @@ module Rigor
76
76
  base.with(
77
77
  min_arity: [base.min_arity, addition.min_arity].min,
78
78
  max_arity: [base.max_arity, addition.max_arity].max,
79
- keyword_required: base.keyword_required & addition.keyword_required
79
+ keyword_required: base.keyword_required & addition.keyword_required,
80
+ accepts_keywords: base.accepts_keywords || addition.accepts_keywords
80
81
  )
81
82
  end
82
83
 
@@ -187,7 +188,7 @@ module Rigor
187
188
  if perform_def.nil?
188
189
  return JobIndex::Entry.new(
189
190
  class_name: class_name, min_arity: 0,
190
- max_arity: Float::INFINITY, keyword_required: []
191
+ max_arity: Float::INFINITY, keyword_required: [], accepts_keywords: true
191
192
  )
192
193
  end
193
194
 
@@ -195,22 +196,31 @@ module Rigor
195
196
  if parameters.nil?
196
197
  return JobIndex::Entry.new(
197
198
  class_name: class_name, min_arity: 0,
198
- max_arity: 0, keyword_required: []
199
+ max_arity: 0, keyword_required: [], accepts_keywords: false
199
200
  )
200
201
  end
201
202
 
202
- required_count = (parameters.requireds || []).size
203
+ # Parameters after an optional or rest one (`def perform(a, b = 1, c)`) are required too.
204
+ required_count = (parameters.requireds || []).size + (parameters.posts || []).size
203
205
  optional_count = (parameters.optionals || []).size
204
206
  rest_present = !parameters.rest.nil?
205
207
  keyword_required = (parameters.keywords || []).filter_map do |kw|
206
208
  kw.name if kw.is_a?(Prism::RequiredKeywordParameterNode)
207
209
  end
208
210
 
211
+ keyword_rest = parameters.keyword_rest
212
+ # `...` lives in `keyword_rest` and forwards every argument, so it widens the envelope; `**nil`
213
+ # also lives there but declares that NO keywords are taken (ActiveJob then hands them over as a
214
+ # positional hash, exactly like a method with no keyword parameters).
215
+ forwards_all = keyword_rest.is_a?(Prism::ForwardingParameterNode)
216
+ takes_keywords = !(parameters.keywords || []).empty? || keyword_rest.is_a?(Prism::KeywordRestParameterNode)
217
+
209
218
  JobIndex::Entry.new(
210
219
  class_name: class_name,
211
220
  min_arity: required_count,
212
- max_arity: rest_present ? Float::INFINITY : required_count + optional_count,
213
- keyword_required: keyword_required.map(&:to_sym)
221
+ max_arity: rest_present || forwards_all ? Float::INFINITY : required_count + optional_count,
222
+ keyword_required: keyword_required.map(&:to_sym),
223
+ accepts_keywords: takes_keywords || forwards_all
214
224
  )
215
225
  end
216
226
  end
@@ -10,9 +10,11 @@ module Rigor
10
10
  # `min_arity` / `max_arity` form a closed range (`Float::INFINITY` for the upper bound when
11
11
  # `*args` is present). `keyword_required` lists any required keyword arguments — Active Job
12
12
  # supports keyword args but they're rare in user code, so the analyzer validates positional arity
13
- # only (keyword arity validation is deferred).
13
+ # only (keyword name validation is deferred). `accepts_keywords` records whether `#perform` binds
14
+ # keywords at all (`k:`, `k: 1` or `**opts`), so the analyzer can keep a call's keyword arguments out
15
+ # of the positional count.
14
16
  class JobIndex
15
- Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required) do
17
+ Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required, :accepts_keywords) do
16
18
  # Flexible-friendly textual form of the arity for error messages: `1`, `1..2`, `2+`.
17
19
  def arity_label
18
20
  return "#{min_arity}+" if max_arity == Float::INFINITY
@@ -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
@@ -110,7 +110,8 @@ module Rigor
110
110
  # The predicate `Activerecord#finder_return_type` declines on, so the note and the type agree in a
111
111
  # CLI run. In an editor run this scope lacks the file's own methods (#1329).
112
112
  def own_find?(entry)
113
- @scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
113
+ !Rigor::Reflection.project_method_defined(entry.class_name, :find, kind: :singleton, source: :discovered,
114
+ scope: @scope).no?
114
115
  end
115
116
 
116
117
  def validate_column_hash_call(node, entry)
@@ -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 }
@@ -31,10 +31,17 @@ module Rigor
31
31
  # `Object` per `SchemaTable.ruby_type_for` rather than being dropped. Only the structural calls in
32
32
  # {NON_COLUMN_METHODS} (indexes, constraints, foreign keys) are skipped.
33
33
  #
34
+ # `create_table "x", primary_key: "name"` adds that column, typed from the `id:` option when the call is
35
+ # read completely and untyped otherwise. `record.id` reads the primary key column's value, so the `id`
36
+ # column takes the same type; a composite or unreadable `primary_key:` leaves it untyped. `id: false` or
37
+ # `id: nil` removes it and adds neither.
38
+ #
34
39
  # Designed for the Prism interpretation pattern from rigor-lisp-eval — recursive descent on the AST,
35
40
  # no eval.
36
41
  class SchemaParser
37
42
  TIMESTAMPS_COLUMNS = %w[created_at updated_at].freeze
43
+ DEFAULT_PRIMARY_KEY_TYPE = :bigint
44
+ UNKNOWN_PRIMARY_KEY_TYPE = :unknown
38
45
 
39
46
  # Structural `t.<method>` calls inside a `create_table` block that declare indexes / constraints
40
47
  # rather than columns. Everything NOT in this set that carries a literal column name is treated as
@@ -75,8 +82,10 @@ module Rigor
75
82
  return [nil, nil] if table_name.nil?
76
83
 
77
84
  block_node = call_node.block
78
- columns = { "id" => SchemaTable::Column.new(name: "id", type: :integer, ruby_type: "Integer") }
85
+ columns = { "id" => implicit_id_column(call_node) }
79
86
  columns.delete("id") if id_disabled?(call_node)
87
+ primary_key = named_primary_key_column(call_node)
88
+ columns[primary_key.name] = primary_key if primary_key
80
89
 
81
90
  if block_node.is_a?(Prism::BlockNode) && block_node.body
82
91
  collect_column_calls(block_node.body) do |column_call|
@@ -92,25 +101,133 @@ module Rigor
92
101
  [table_name, columns.freeze]
93
102
  end
94
103
 
104
+ # `record.id` reads the primary key column's value, so the `id` reader takes the key's type — the
105
+ # same shape the `id:` option gives the column `primary_key:` names or the implicit `id` column.
106
+ # A composite `primary_key:` makes `id` an Array this parser does not type, and a `primary_key:`
107
+ # it cannot read leaves `id` untyped as well.
108
+ def implicit_id_column(call_node)
109
+ unless single_primary_key?(call_node)
110
+ return SchemaTable::Column.new(name: "id", type: UNKNOWN_PRIMARY_KEY_TYPE, ruby_type: "Object")
111
+ end
112
+
113
+ SchemaTable::Column.new(name: "id", **primary_key_shape(call_node))
114
+ end
115
+
116
+ # `primary_key:` names one key column when it is a String or Symbol literal; `nil` or `false` falls
117
+ # back to the default `id` column the way `primary_key || ...` does in Rails.
118
+ def single_primary_key?(call_node)
119
+ primary_key = keyword_value(call_node, :primary_key)
120
+ primary_key.nil? || primary_key.is_a?(Prism::NilNode) || primary_key.is_a?(Prism::FalseNode) ||
121
+ !literal_name(primary_key).nil?
122
+ end
123
+
95
124
  def id_disabled?(call_node)
96
- return false if call_node.arguments.nil?
125
+ id = keyword_value(call_node, :id)
126
+ (id.is_a?(Prism::FalseNode) || id.is_a?(Prism::NilNode)) && keywords_read?(call_keywords(call_node))
127
+ end
128
+
129
+ # `create_table "settings", primary_key: "name", id: { type: :string }` creates the column `name`
130
+ # without the block declaring it, so the option is the only place the schema names it.
131
+ def named_primary_key_column(call_node)
132
+ return nil if id_disabled?(call_node)
133
+
134
+ name = literal_name(keyword_value(call_node, :primary_key))
135
+ return nil if name.nil?
136
+
137
+ SchemaTable::Column.new(name: name, **primary_key_shape(call_node))
138
+ end
139
+
140
+ # The `id:` option describes the primary key column's type whether the key column is `id` itself or
141
+ # the column `primary_key:` names. An unreadable call still yields a shape — the column stays
142
+ # queryable, just untyped.
143
+ def primary_key_shape(call_node)
144
+ id = keyword_value(call_node, :id)
145
+ options = id.is_a?(Prism::HashNode) ? id.elements : []
146
+ array = keyword_in(options, :array).is_a?(Prism::TrueNode)
147
+ unless primary_key_type_stated?(call_node, options)
148
+ return { type: UNKNOWN_PRIMARY_KEY_TYPE, ruby_type: "Object", array: array }
149
+ end
150
+
151
+ type = primary_key_type(id, options)
152
+ { type: type, ruby_type: primary_key_ruby_type(type), array: array }
153
+ end
154
+
155
+ # `id: :string` or `id: { type: :string, ... }`. `id: true` asks for the default key, as does an
156
+ # `id:` hash that names no `type:`. A type this parser cannot read as a literal is left unknown
157
+ # rather than assumed to be bigint.
158
+ def primary_key_type(id, options)
159
+ return DEFAULT_PRIMARY_KEY_TYPE if id.is_a?(Prism::TrueNode)
160
+
161
+ declared = id.is_a?(Prism::HashNode) ? keyword_in(options, :type) : id
162
+ return DEFAULT_PRIMARY_KEY_TYPE if declared.nil?
163
+
164
+ literal_name(declared)&.to_sym || UNKNOWN_PRIMARY_KEY_TYPE
165
+ end
166
+
167
+ # The key is typed only from a call this parser reads completely. `as:` is excluded because the query
168
+ # may define the column, and a non-literal `array:` because it decides the reader's shape.
169
+ def primary_key_type_stated?(call_node, options)
170
+ array = keyword_in(options, :array)
171
+ keywords_read?(call_keywords(call_node)) && keywords_read?(options) &&
172
+ keyword_value(call_node, :as).nil? &&
173
+ (array.nil? || array.is_a?(Prism::TrueNode) || array.is_a?(Prism::FalseNode))
174
+ end
175
+
176
+ def call_keywords(call_node)
177
+ call_node.arguments.arguments.grep(Prism::KeywordHashNode).flat_map(&:elements)
178
+ end
179
+
180
+ # Every element has a static Symbol key and no key repeats, so `keyword_in` sees what Ruby would pass.
181
+ def keywords_read?(elements)
182
+ keys = elements.map { |element| symbol_key(element.key) if element.is_a?(Prism::AssocNode) }
183
+ keys.none?(&:nil?) && keys.uniq.size == keys.size
184
+ end
185
+
186
+ # A decimal key stays untyped, because its class depends on the scale: `Type::DecimalWithoutScale`
187
+ # at scale 0, `Type::Decimal` otherwise.
188
+ def primary_key_ruby_type(type)
189
+ type == :decimal ? "Object" : SchemaTable.ruby_type_for(type)
190
+ end
191
+
192
+ # A `decimal` column's class follows its literal `scale:`. Any other type takes its SchemaTable mapping.
193
+ # A call this parser does not read completely may still hold a different scale, so it reads untyped.
194
+ def column_ruby_type(type, call_node)
195
+ return SchemaTable.ruby_type_for(type) unless type == :decimal
196
+ return "Object" unless keywords_read?(call_keywords(call_node))
197
+
198
+ scale = keyword_value(call_node, :scale)
199
+ SchemaTable.decimal_ruby_type(scale.is_a?(Prism::IntegerNode) ? scale.value : nil)
200
+ end
201
+
202
+ def literal_name(node)
203
+ case node
204
+ when Prism::StringNode, Prism::SymbolNode then node.unescaped
205
+ end
206
+ end
207
+
208
+ def keyword_value(call_node, name)
209
+ return nil if call_node.arguments.nil?
97
210
 
98
211
  call_node.arguments.arguments.each do |arg|
99
212
  next unless arg.is_a?(Prism::KeywordHashNode)
100
213
 
101
- arg.elements.each do |pair|
102
- next unless pair.is_a?(Prism::AssocNode)
214
+ value = keyword_in(arg.elements, name)
215
+ return value if value
216
+ end
217
+ nil
218
+ end
103
219
 
104
- key = symbol_key(pair.key)
105
- return true if key == :id && pair.value.is_a?(Prism::FalseNode)
106
- end
220
+ def keyword_in(elements, name)
221
+ elements.each do |pair|
222
+ next unless pair.is_a?(Prism::AssocNode)
223
+
224
+ return pair.value if symbol_key(pair.key) == name
107
225
  end
108
- false
226
+ nil
109
227
  end
110
228
 
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.
229
+ # Walks the block body collecting `t.<method>(...)` calls. It descends through nested constructs
230
+ # (e.g. `if` branches) but does not recurse into a matched call.
114
231
  def collect_column_calls(node, &)
115
232
  return if node.nil?
116
233
 
@@ -148,7 +265,7 @@ module Rigor
148
265
  SchemaTable::Column.new(
149
266
  name: name,
150
267
  type: type,
151
- ruby_type: SchemaTable.ruby_type_for(type),
268
+ ruby_type: column_ruby_type(type, call_node),
152
269
  array: keyword_true?(call_node, :array)
153
270
  )
154
271
  end
@@ -177,19 +294,7 @@ module Rigor
177
294
  # modifiers like `t.bigint "status_ids", array: true` (Postgres array column) and `t.references
178
295
  # "x", polymorphic: true`.
179
296
  def keyword_true?(call_node, name)
180
- return false if call_node.arguments.nil?
181
-
182
- call_node.arguments.arguments.each do |arg|
183
- next unless arg.is_a?(Prism::KeywordHashNode)
184
-
185
- arg.elements.each do |pair|
186
- next unless pair.is_a?(Prism::AssocNode)
187
- next unless symbol_key(pair.key) == name
188
-
189
- return pair.value.is_a?(Prism::TrueNode)
190
- end
191
- end
192
- false
297
+ keyword_value(call_node, name).is_a?(Prism::TrueNode)
193
298
  end
194
299
 
195
300
  # `t.column "name", "type"` / `t.column "name", :type` — the explicit generic column form. The
@@ -203,7 +308,7 @@ module Rigor
203
308
  SchemaTable::Column.new(
204
309
  name: name,
205
310
  type: type_sym,
206
- ruby_type: SchemaTable.ruby_type_for(type_sym),
311
+ ruby_type: column_ruby_type(type_sym, call_node),
207
312
  array: keyword_true?(call_node, :array)
208
313
  )
209
314
  end
@@ -217,13 +322,7 @@ module Rigor
217
322
  def string_argument(call_node, index)
218
323
  return nil if call_node.arguments.nil?
219
324
 
220
- arg = call_node.arguments.arguments[index]
221
- return nil if arg.nil?
222
-
223
- case arg
224
- when Prism::StringNode then arg.unescaped
225
- when Prism::SymbolNode then arg.unescaped
226
- end
325
+ literal_name(call_node.arguments.arguments[index])
227
326
  end
228
327
 
229
328
  def symbol_key(node)