rigortype 0.2.7 → 0.2.9

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 (433) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/08-understanding-errors.md +3 -3
  8. data/docs/manual/01-installation.md +1 -0
  9. data/docs/manual/02-cli-reference.md +16 -7
  10. data/docs/manual/04-diagnostics.md +7 -4
  11. data/docs/manual/06-baseline.md +5 -2
  12. data/docs/manual/07-plugins.md +1 -1
  13. data/docs/manual/14-rails-quickstart.md +4 -2
  14. data/docs/manual/15-type-protection-coverage.md +27 -4
  15. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  16. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  17. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  18. data/lib/rigor/analysis/baseline.rb +56 -96
  19. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  20. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  21. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  23. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  24. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  25. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  26. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  27. data/lib/rigor/analysis/check_rules.rb +174 -274
  28. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  29. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  30. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  31. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  32. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  33. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  34. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  35. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  36. data/lib/rigor/analysis/diagnostic.rb +48 -75
  37. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  38. data/lib/rigor/analysis/fact_store.rb +12 -22
  39. data/lib/rigor/analysis/incremental.rb +47 -63
  40. data/lib/rigor/analysis/incremental_session.rb +74 -100
  41. data/lib/rigor/analysis/project_scan.rb +12 -23
  42. data/lib/rigor/analysis/result.rb +3 -5
  43. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  44. data/lib/rigor/analysis/run_stats.rb +38 -61
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  48. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  49. data/lib/rigor/analysis/runner.rb +220 -333
  50. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  51. data/lib/rigor/analysis/worker_session.rb +64 -102
  52. data/lib/rigor/ast/type_node.rb +6 -8
  53. data/lib/rigor/ast.rb +6 -10
  54. data/lib/rigor/bleeding_edge.rb +27 -35
  55. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  56. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  57. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  58. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  59. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  60. data/lib/rigor/cache/descriptor.rb +58 -94
  61. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  62. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  63. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  64. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  65. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  66. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  67. data/lib/rigor/cache/rbs_environment.rb +11 -19
  68. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  69. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  70. data/lib/rigor/cache/store.rb +248 -186
  71. data/lib/rigor/cli/annotate_command.rb +51 -88
  72. data/lib/rigor/cli/baseline_command.rb +10 -17
  73. data/lib/rigor/cli/check_command.rb +121 -199
  74. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  75. data/lib/rigor/cli/ci_detector.rb +9 -14
  76. data/lib/rigor/cli/command.rb +8 -14
  77. data/lib/rigor/cli/coverage_command.rb +62 -48
  78. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  79. data/lib/rigor/cli/coverage_report.rb +0 -2
  80. data/lib/rigor/cli/coverage_scan.rb +7 -11
  81. data/lib/rigor/cli/diagnostic_formats.rb +46 -62
  82. data/lib/rigor/cli/diff_command.rb +10 -20
  83. data/lib/rigor/cli/docs_command.rb +26 -40
  84. data/lib/rigor/cli/doctor_command.rb +8 -9
  85. data/lib/rigor/cli/explain_command.rb +4 -7
  86. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  87. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  88. data/lib/rigor/cli/lsp_command.rb +11 -19
  89. data/lib/rigor/cli/mcp_command.rb +4 -6
  90. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  91. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  92. data/lib/rigor/cli/options.rb +11 -19
  93. data/lib/rigor/cli/plugin_command.rb +18 -30
  94. data/lib/rigor/cli/plugins_command.rb +29 -51
  95. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  96. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  97. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  98. data/lib/rigor/cli/protection_renderer.rb +5 -7
  99. data/lib/rigor/cli/protection_report.rb +34 -18
  100. data/lib/rigor/cli/renderable.rb +4 -6
  101. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  102. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  103. data/lib/rigor/cli/skill_command.rb +30 -47
  104. data/lib/rigor/cli/skill_describe.rb +40 -64
  105. data/lib/rigor/cli/trace_command.rb +9 -16
  106. data/lib/rigor/cli/trace_renderer.rb +35 -51
  107. data/lib/rigor/cli/triage_command.rb +6 -10
  108. data/lib/rigor/cli/triage_renderer.rb +1 -2
  109. data/lib/rigor/cli/type_of_command.rb +14 -23
  110. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  111. data/lib/rigor/cli/type_scan_command.rb +6 -10
  112. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  113. data/lib/rigor/cli/type_scan_report.rb +2 -3
  114. data/lib/rigor/cli/upgrade_command.rb +2 -3
  115. data/lib/rigor/cli.rb +16 -28
  116. data/lib/rigor/config_audit.rb +35 -44
  117. data/lib/rigor/configuration/dependencies.rb +41 -70
  118. data/lib/rigor/configuration/severity_profile.rb +28 -42
  119. data/lib/rigor/configuration.rb +150 -241
  120. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  121. data/lib/rigor/environment/class_registry.rb +19 -26
  122. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  123. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  124. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  125. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  126. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  127. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  128. data/lib/rigor/environment/rbs_loader.rb +420 -518
  129. data/lib/rigor/environment/reflection.rb +28 -53
  130. data/lib/rigor/environment/reporters.rb +13 -24
  131. data/lib/rigor/environment.rb +225 -304
  132. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  133. data/lib/rigor/flow_contribution/element.rb +7 -12
  134. data/lib/rigor/flow_contribution/fact.rb +33 -57
  135. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  136. data/lib/rigor/flow_contribution/merger.rb +32 -47
  137. data/lib/rigor/flow_contribution.rb +37 -55
  138. data/lib/rigor/inference/acceptance.rb +133 -219
  139. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  140. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  141. data/lib/rigor/inference/budget_trace.rb +43 -63
  142. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  143. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  144. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  145. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  146. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  147. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  148. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  149. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  150. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  151. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  152. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  153. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  154. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  155. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  156. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  157. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  158. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  159. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  160. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  161. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  162. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  163. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  164. data/lib/rigor/inference/def_return_typer.rb +8 -14
  165. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  166. data/lib/rigor/inference/expression_typer.rb +794 -1154
  167. data/lib/rigor/inference/fallback.rb +8 -12
  168. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  169. data/lib/rigor/inference/flow_tracer.rb +25 -36
  170. data/lib/rigor/inference/fork_map.rb +87 -0
  171. data/lib/rigor/inference/hkt_body.rb +45 -68
  172. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  173. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  174. data/lib/rigor/inference/hkt_registry.rb +50 -76
  175. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  176. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  177. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  178. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  179. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  180. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  181. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  182. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  183. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  184. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  185. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  186. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  187. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  188. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  189. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  190. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  191. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  192. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  193. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  194. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  195. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  196. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  197. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  198. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  199. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  200. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  201. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  202. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  203. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  204. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  205. data/lib/rigor/inference/mutation_widening.rb +115 -158
  206. data/lib/rigor/inference/narrowing.rb +627 -727
  207. data/lib/rigor/inference/origin_lookup.rb +38 -0
  208. data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
  209. data/lib/rigor/inference/precision_scanner.rb +26 -35
  210. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  211. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  212. data/lib/rigor/inference/protection_scanner.rb +16 -18
  213. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  214. data/lib/rigor/inference/scope_indexer.rb +473 -821
  215. data/lib/rigor/inference/statement_evaluator.rb +754 -1114
  216. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  217. data/lib/rigor/inference/synthetic_method.rb +9 -16
  218. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  219. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  220. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  221. data/lib/rigor/language_server/buffer_table.rb +11 -18
  222. data/lib/rigor/language_server/completion_provider.rb +69 -116
  223. data/lib/rigor/language_server/debouncer.rb +16 -25
  224. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  225. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  226. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  227. data/lib/rigor/language_server/hover_provider.rb +8 -11
  228. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  229. data/lib/rigor/language_server/loop.rb +14 -19
  230. data/lib/rigor/language_server/project_context.rb +46 -74
  231. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  232. data/lib/rigor/language_server/server.rb +55 -83
  233. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  234. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  235. data/lib/rigor/language_server/uri.rb +8 -13
  236. data/lib/rigor/language_server.rb +4 -6
  237. data/lib/rigor/mcp/loop.rb +2 -3
  238. data/lib/rigor/mcp/server.rb +4 -7
  239. data/lib/rigor/mcp.rb +3 -6
  240. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  241. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  242. data/lib/rigor/plugin/base.rb +335 -518
  243. data/lib/rigor/plugin/blueprint.rb +14 -23
  244. data/lib/rigor/plugin/box.rb +18 -29
  245. data/lib/rigor/plugin/fact_store.rb +16 -26
  246. data/lib/rigor/plugin/inflector.rb +37 -53
  247. data/lib/rigor/plugin/io_boundary.rb +33 -56
  248. data/lib/rigor/plugin/isolation.rb +42 -55
  249. data/lib/rigor/plugin/load_error.rb +10 -15
  250. data/lib/rigor/plugin/loader.rb +30 -49
  251. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  252. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  253. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  254. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  255. data/lib/rigor/plugin/macro.rb +10 -15
  256. data/lib/rigor/plugin/manifest.rb +85 -144
  257. data/lib/rigor/plugin/node_context.rb +14 -22
  258. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  259. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  260. data/lib/rigor/plugin/registry.rb +132 -205
  261. data/lib/rigor/plugin/services.rb +21 -33
  262. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  263. data/lib/rigor/plugin/trust_policy.rb +24 -38
  264. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  265. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  266. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  267. data/lib/rigor/protection/mutator.rb +50 -70
  268. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  269. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  270. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  271. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  272. data/lib/rigor/rbs_extended.rb +107 -197
  273. data/lib/rigor/reflection.rb +68 -86
  274. data/lib/rigor/scope/discovery_index.rb +14 -19
  275. data/lib/rigor/scope.rb +255 -310
  276. data/lib/rigor/sig_gen/classification.rb +6 -10
  277. data/lib/rigor/sig_gen/generator.rb +197 -323
  278. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  279. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  280. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  281. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  282. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  283. data/lib/rigor/sig_gen/renderer.rb +7 -13
  284. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  285. data/lib/rigor/sig_gen/write_result.rb +8 -16
  286. data/lib/rigor/sig_gen/writer.rb +95 -174
  287. data/lib/rigor/sig_gen.rb +3 -6
  288. data/lib/rigor/signature_path_audit.rb +24 -30
  289. data/lib/rigor/source/constant_path.rb +10 -14
  290. data/lib/rigor/source/literals.rb +31 -45
  291. data/lib/rigor/source/node_locator.rb +9 -11
  292. data/lib/rigor/source/node_walker.rb +9 -13
  293. data/lib/rigor/source.rb +3 -4
  294. data/lib/rigor/testing.rb +16 -20
  295. data/lib/rigor/triage/catalogue.rb +38 -62
  296. data/lib/rigor/triage.rb +31 -52
  297. data/lib/rigor/trinary.rb +9 -13
  298. data/lib/rigor/type/acceptance_router.rb +4 -6
  299. data/lib/rigor/type/accepts_result.rb +10 -14
  300. data/lib/rigor/type/app.rb +19 -27
  301. data/lib/rigor/type/bot.rb +4 -6
  302. data/lib/rigor/type/bound_method.rb +10 -15
  303. data/lib/rigor/type/combinator.rb +165 -257
  304. data/lib/rigor/type/constant.rb +23 -34
  305. data/lib/rigor/type/data_class.rb +10 -15
  306. data/lib/rigor/type/data_instance.rb +14 -20
  307. data/lib/rigor/type/difference.rb +21 -32
  308. data/lib/rigor/type/dynamic.rb +3 -5
  309. data/lib/rigor/type/hash_shape.rb +32 -18
  310. data/lib/rigor/type/integer_range.rb +11 -16
  311. data/lib/rigor/type/intersection.rb +27 -42
  312. data/lib/rigor/type/nominal.rb +10 -15
  313. data/lib/rigor/type/plain_lattice.rb +9 -13
  314. data/lib/rigor/type/refined.rb +67 -114
  315. data/lib/rigor/type/singleton.rb +4 -6
  316. data/lib/rigor/type/struct_class.rb +11 -16
  317. data/lib/rigor/type/struct_instance.rb +15 -21
  318. data/lib/rigor/type/tuple.rb +14 -19
  319. data/lib/rigor/type/union.rb +30 -42
  320. data/lib/rigor/type_node/generic.rb +14 -26
  321. data/lib/rigor/type_node/identifier.rb +12 -19
  322. data/lib/rigor/type_node.rb +3 -12
  323. data/lib/rigor/value_semantics.rb +16 -21
  324. data/lib/rigor/version.rb +1 -1
  325. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
  337. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  340. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
  348. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  350. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  351. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  353. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  354. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  355. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  356. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  357. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  358. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  359. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  361. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  363. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  365. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  366. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  367. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  368. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  369. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  372. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  373. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  376. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  377. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  378. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  379. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  380. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  381. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  385. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  386. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  389. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  390. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
  398. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
  399. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  400. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  406. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  408. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  409. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  411. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  416. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  417. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  426. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  427. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  428. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  429. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  430. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  431. data/sig/rigor/environment.rbs +1 -0
  432. data/sig/rigor/scope.rbs +6 -0
  433. metadata +9 -1
@@ -5,9 +5,8 @@ require "prism"
5
5
  module Rigor
6
6
  module Plugin
7
7
  class Activerecord < Rigor::Plugin::Base
8
- # Per-file AST walker. For each `Model.find(...)` /
9
- # `Model.find_by(...)` / `Model.where(...)` call where
10
- # `Model` is a name in the {ModelIndex}, emits diagnostics:
8
+ # Per-file AST walker. For each `Model.find(...)` / `Model.find_by(...)` / `Model.where(...)` call
9
+ # where `Model` is a name in the {ModelIndex}, emits diagnostics:
11
10
  #
12
11
  # | Method | Recognised arg shape | Validation |
13
12
  # | --- | --- | --- |
@@ -16,17 +15,13 @@ module Rigor
16
15
  # | `Model.where(col: v, ...)` | keyword args | each key must be a column |
17
16
  # | `Model.where(string)` | String literal | parser-side; not validated |
18
17
  #
19
- # Successful matches surface as `:info` diagnostics naming
20
- # the resolved table; unknown columns surface as `:error`.
21
- # Calls whose receiver is not a model in the index, and
22
- # calls with non-keyword arguments to `where` / `find_by`,
23
- # stay silent.
18
+ # Successful matches surface as `:info` diagnostics naming the resolved table; unknown columns
19
+ # surface as `:error`. Calls whose receiver is not a model in the index, and calls with
20
+ # non-keyword arguments to `where` / `find_by`, stay silent.
24
21
  class Analyzer
25
- # Methods that take a column → value Hash and need each key
26
- # validated against the receiver's column set. The bang
27
- # variants (`find_by!` raises instead of returning nil;
28
- # `find_or_create_by!` raises on a validation failure) and
29
- # `create_or_find_by` / `create_or_find_by!` take the
22
+ # Methods that take a column → value Hash and need each key validated against the receiver's
23
+ # column set. The bang variants (`find_by!` raises instead of returning nil; `find_or_create_by!`
24
+ # raises on a validation failure) and `create_or_find_by` / `create_or_find_by!` take the
30
25
  # identical column-hash argument shape.
31
26
  COLUMN_HASH_METHODS = %i[
32
27
  where
@@ -88,17 +83,14 @@ module Rigor
88
83
  keyword_pairs = keyword_argument_pairs(node)
89
84
  return push_recognised(node, entry) if keyword_pairs.empty?
90
85
 
91
- # Models with no schema-side columns are virtual — backed
92
- # by a database VIEW (Mastodon's `Instance` model wraps a
93
- # SQL view that isn't in `db/schema.rb`), seeded from an
94
- # external source, or otherwise opaque to our schema
95
- # parser. Without a column set we cannot meaningfully
96
- # check query keys; surface the call as recognised and
97
- # skip column validation entirely rather than firing a
98
- # false `unknown-column` against every key.
86
+ # Models with no schema-side columns are virtual — backed by a database VIEW (Mastodon's
87
+ # `Instance` model wraps a SQL view that isn't in `db/schema.rb`), seeded from an external
88
+ # source, or otherwise opaque to our schema parser. Without a column set we cannot meaningfully
89
+ # check query keys; surface the call as recognised and skip column validation entirely rather
90
+ # than firing a false `unknown-column` against every key.
99
91
  return push_recognised(node, entry, keyword_pairs.map { |p| p[:key] }) if entry.column_names.empty?
100
92
 
101
- unknown = keyword_pairs.reject { |pair| valid_query_key?(entry, pair[:key]) }
93
+ unknown = keyword_pairs.reject { |pair| nested_condition?(pair[:value]) || valid_query_key?(entry, pair[:key]) }
102
94
  if unknown.empty?
103
95
  keyword_pairs.each { |pair| validate_enum_value(node, entry, pair) }
104
96
  push_recognised(node, entry, keyword_pairs.map { |p| p[:key] })
@@ -114,23 +106,20 @@ module Rigor
114
106
  end
115
107
  end
116
108
 
117
- # ActiveRecord's `where` / `find_by` / `find_or_initialize_by`
118
- # accept either a column name *or* a singular association
119
- # name (belongs_to / has_one). The latter resolves to the
120
- # association's FK column behind the scenes:
109
+ # ActiveRecord's `where` / `find_by` / `find_or_initialize_by` accept either a column name *or* a
110
+ # singular association name (belongs_to / has_one). The latter resolves to the association's FK
111
+ # column behind the scenes:
121
112
  #
122
113
  # AccountPin.find_by(account: x)
123
114
  # # → SELECT … WHERE account_id = x.id
124
115
  #
125
- # Without this allowance, every `find_by(<assoc>:)` call
126
- # surfaces as an `unknown-column` false positive Mastodon
127
- # saw ~100 such hits across the API controllers, all of
128
- # which were the canonical Rails idiom rather than typos.
116
+ # Without this allowance, every `find_by(<assoc>:)` call surfaces as an `unknown-column` false
117
+ # positive Mastodon saw ~100 such hits across the API controllers, all of which were the
118
+ # canonical Rails idiom rather than typos.
129
119
  #
130
- # `alias_attribute` names resolve to their target before
131
- # the column / association check querying by an aliased
132
- # attribute is a valid Rails idiom and must not surface as
133
- # an `unknown-column`.
120
+ # `alias_attribute` names resolve to their target before the column / association check —
121
+ # querying by an aliased attribute is a valid Rails idiom and must not surface as an
122
+ # `unknown-column`.
134
123
  def valid_query_key?(entry, key)
135
124
  key = entry.resolve_alias(key) if entry.alias?(key)
136
125
 
@@ -140,13 +129,19 @@ module Rigor
140
129
  assoc && assoc[:kind] == :singular
141
130
  end
142
131
 
143
- # When the column is an enum-bearing column AND the
144
- # passed value is a Symbol literal, the value MUST be
145
- # one of the declared enum values. Non-Symbol values
146
- # (variables, expressions) decline so dynamic call
147
- # sites stay silent. Sequences (`status: [:active,
148
- # :archived]`) are intentionally NOT walked here —
149
- # the relation-shape check belongs in a future track.
132
+ # A keyword pair with a Hash value is a nested / joined-table condition (`Approval.where(approvals:
133
+ # { user_id: 1 })`, `where(users: { name: 'x' })`), where the key names an association or a joined
134
+ # table, not a column on the receiver. Rails resolves it through the join, so it must not be checked
135
+ # against the receiver's own columns. (An Array value — `where(status: [:a, :b])` is an IN query on
136
+ # a real column, not nested, so only a Hash is skipped.)
137
+ def nested_condition?(value_node)
138
+ value_node.is_a?(Prism::HashNode) || value_node.is_a?(Prism::KeywordHashNode)
139
+ end
140
+
141
+ # When the column is an enum-bearing column AND the passed value is a Symbol literal, the value
142
+ # MUST be one of the declared enum values. Non-Symbol values (variables, expressions) decline so
143
+ # dynamic call sites stay silent. Sequences (`status: [:active, :archived]`) are intentionally NOT
144
+ # walked here — the relation-shape check belongs in a future track.
150
145
  def validate_enum_value(node, entry, pair)
151
146
  return unless entry.enum?(pair[:key])
152
147
  return unless pair[:value].is_a?(Prism::SymbolNode)
@@ -199,13 +194,10 @@ module Rigor
199
194
  node.arguments.arguments.size
200
195
  end
201
196
 
202
- # Returns the symbol-keyed pairs of any KeywordHashNode
203
- # argument as `{ key:, value: }` rows. Plain hash-literal
204
- # arguments (`Model.where({a: 1})`) are NOT walked
205
- # Rails accepts both, but the keyword form is the
206
- # idiomatic one. The `value` Prism node is preserved so
207
- # the enum check downstream can recognise Symbol
208
- # literals without re-walking the call.
197
+ # Returns the symbol-keyed pairs of any KeywordHashNode argument as `{ key:, value: }` rows. Plain
198
+ # hash-literal arguments (`Model.where({a: 1})`) are NOT walked — Rails accepts both, but the
199
+ # keyword form is the idiomatic one. The `value` Prism node is preserved so the enum check
200
+ # downstream can recognise Symbol literals without re-walking the call.
209
201
  def keyword_argument_pairs(node)
210
202
  return [] if node.arguments.nil?
211
203
 
@@ -6,20 +6,15 @@ require "rigor/source/literals"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class Activerecord < Rigor::Plugin::Base
9
- # Walks the configured model search paths via the plugin's
10
- # `IoBoundary`, parses each `.rb` file with Prism, and
11
- # collects class declarations that resolve to ActiveRecord
12
- # models.
9
+ # Walks the configured model search paths via the plugin's `IoBoundary`, parses each `.rb` file with
10
+ # Prism, and collects class declarations that resolve to ActiveRecord models.
13
11
  #
14
- # Discovery is a two-step pass. First every class declaration
15
- # is captured as a *candidate* (its name, its superclass
16
- # name, and its DSL metadata). Then a fixpoint marks a
17
- # candidate as a model when its superclass is a configured
18
- # base class OR (transitively) the class name of another
19
- # model this is what makes single-table-inheritance
20
- # subclasses (`class Admin < User`) discoverable. Each STI
21
- # child carries an `sti_parent:` pointer the {ModelIndex}
22
- # uses to inherit the root model's table and DSL surface.
12
+ # Discovery is a two-step pass. First every class declaration is captured as a *candidate* (its
13
+ # name, its superclass name, and its DSL metadata). Then a fixpoint marks a candidate as a model
14
+ # when its superclass is a configured base class OR (transitively) the class name of another model
15
+ # this is what makes single-table-inheritance subclasses (`class Admin < User`) discoverable.
16
+ # Each STI child carries an `sti_parent:` pointer the {ModelIndex} uses to inherit the root model's
17
+ # table and DSL surface.
23
18
  #
24
19
  # Returns rows the {ModelIndex} consumes:
25
20
  #
@@ -28,27 +23,35 @@ module Rigor
28
23
  #
29
24
  # Limitations (intentional for v0.1.0 of the plugin):
30
25
  #
31
- # - `self.table_name = "..."` recognised only when the RHS
32
- # is a String literal. Computed names
26
+ # - `self.table_name = "..."` recognised only when the RHS is a String literal. Computed names
33
27
  # (`self.table_name = "#{tenant}_users"`) are skipped.
34
- # - Modules (`class Admin::User < ApplicationRecord`) are
35
- # recognised; the resulting class name is the lexical
36
- # path (`Admin::User`).
37
- # - The STI fixpoint matches a superclass name against model
38
- # class names tolerating a leading `::`; richer constant
39
- # resolution (relative namespacing) is not modelled.
28
+ # - Modules (`class Admin::User < ApplicationRecord`) are recognised; the resulting class name is
29
+ # the lexical path (`Admin::User`).
30
+ # - The STI fixpoint matches a superclass name against model class names tolerating a leading `::`;
31
+ # richer constant resolution (relative namespacing) is not modelled.
40
32
  class ModelDiscoverer
41
33
  # @param io_boundary [Rigor::Plugin::IoBoundary]
42
- # @param search_paths [Array<String>] absolute or
43
- # project-relative paths.
44
- # @param base_classes [Array<String>] superclass names that
45
- # identify a class as an AR model.
34
+ # @param search_paths [Array<String>] absolute or project-relative paths.
35
+ # @param base_classes [Array<String>] superclass names that identify a class as an AR model.
36
+ # Declaration macros whose column's runtime value is a rich object, not the SQL scalar. Their column
37
+ # must NOT be narrowed to the schema type (see {ModelIndex.build}'s type-override remap).
38
+ TYPE_OVERRIDE_METHODS = %i[serialize mount_uploader mount_uploaders].freeze
39
+
46
40
  def initialize(io_boundary:, search_paths:, base_classes:)
47
41
  @io_boundary = io_boundary
48
42
  @search_paths = search_paths
49
43
  @base_classes = base_classes.to_set
44
+ # Global set of column names whose declared runtime type overrides the schema scalar
45
+ # (`serialize` / `mount_uploader` / `attribute :x, CustomType`). Collected across every class AND
46
+ # concern module walked — a serialize inside a concern's `included do … end` is invisible to the
47
+ # per-model view (the discoverer doesn't follow `include`), so overrides are tracked globally by
48
+ # column name and applied wherever that column appears. Over-suppresses a same-named scalar
49
+ # column elsewhere (a precision cost, never a false positive).
50
+ @type_override_columns = Set.new
50
51
  end
51
52
 
53
+ attr_reader :type_override_columns
54
+
52
55
  # @return [Array<Hash>] rows of { class_name:, table_name_override:, sti_parent:, ... }
53
56
  def discover
54
57
  candidates = []
@@ -64,17 +67,13 @@ module Rigor
64
67
 
65
68
  private
66
69
 
67
- # Fixpoint over the captured class candidates: a candidate
68
- # is a model when its superclass is a configured base
69
- # class, or transitively the class name of an
70
- # already-known model. The second arm is what discovers
71
- # STI subclasses; the matched parent name is stamped onto
72
- # the row as `sti_parent:` so the {ModelIndex} can inherit
73
- # the root model's table and association surface.
70
+ # Fixpoint over the captured class candidates: a candidate is a model when its superclass is a
71
+ # configured base class, or transitively the class name of an already-known model. The
72
+ # second arm is what discovers STI subclasses; the matched parent name is stamped onto the row as
73
+ # `sti_parent:` so the {ModelIndex} can inherit the root model's table and association surface.
74
74
  #
75
- # Non-model classes (POROs, service objects that happen to
76
- # live under `app/models/`) never enter `model_names` and
77
- # are dropped.
75
+ # Non-model classes (POROs, service objects that happen to live under `app/models/`) never enter
76
+ # `model_names` and are dropped.
78
77
  def resolve_models(candidates)
79
78
  model_names = {}
80
79
  sti_parent = {}
@@ -108,9 +107,8 @@ module Rigor
108
107
  end
109
108
  end
110
109
 
111
- # Resolves a superclass NAME against the set of known
112
- # model class names, tolerating a leading `::`. Returns
113
- # the matched model class name, or nil.
110
+ # Resolves a superclass NAME against the set of known model class names, tolerating a leading
111
+ # `::`. Returns the matched model class name, or nil.
114
112
  def model_match(superclass_name, model_names)
115
113
  return superclass_name if model_names.key?(superclass_name)
116
114
 
@@ -146,11 +144,9 @@ module Rigor
146
144
  end
147
145
  end
148
146
 
149
- # Captures EVERY class declaration as a candidate — the
150
- # `resolve_models` fixpoint decides afterwards which ones
151
- # are models. The DSL metadata is extracted eagerly; for a
152
- # non-model class it is simply discarded when the candidate
153
- # is dropped.
147
+ # Captures EVERY class declaration as a candidate — the `resolve_models` fixpoint decides
148
+ # afterwards which ones are models. The DSL metadata is extracted eagerly; for a non-model class
149
+ # it is simply discarded when the candidate is dropped.
154
150
  def visit_class(node, lexical_path, &)
155
151
  class_local_name = constant_path_name(node.constant_path)
156
152
  return if class_local_name.nil?
@@ -158,6 +154,8 @@ module Rigor
158
154
  full_name = (lexical_path + [class_local_name]).join("::")
159
155
  superclass = constant_path_name(node.superclass) if node.superclass
160
156
 
157
+ collect_type_overrides(node.body)
158
+
161
159
  yield({
162
160
  class_name: full_name,
163
161
  superclass_name: superclass,
@@ -179,12 +177,57 @@ module Rigor
179
177
  module_local_name = constant_path_name(node.constant_path)
180
178
  return if module_local_name.nil?
181
179
 
180
+ # Concerns (`module DiffPositionableNote`) carry `serialize` / `mount_uploader` inside an
181
+ # `included do … end` block; collect their overrides even though the module itself is not a model.
182
+ collect_type_overrides(node.body)
183
+
182
184
  inner_path = lexical_path + [module_local_name]
183
185
  walk_for_classes(node.body, inner_path, &) if node.body
184
186
  end
185
187
 
186
- # Renders a constant-path node (`Admin::User`,
187
- # `::ApplicationRecord`) as a String. Returns nil for
188
+ # Records the column name of every `serialize :col` / `mount_uploader(s) :col` / `attribute :col,
189
+ # CustomType` in `body` (descending into `with_options` and concern `included do` blocks) into the
190
+ # global {#type_override_columns} set. `attribute :col, :symbol_type` (a built-in scalar type) is
191
+ # NOT an override — only a custom type CONSTANT is.
192
+ def collect_type_overrides(body)
193
+ type_override_declaration_calls(body).each do |node|
194
+ next if node.receiver
195
+
196
+ column = Rigor::Source::Literals.symbol_name(node.arguments&.arguments&.first)
197
+ next if column.nil?
198
+
199
+ if TYPE_OVERRIDE_METHODS.include?(node.name)
200
+ @type_override_columns << column
201
+ elsif node.name == :attribute && custom_type_attribute?(node)
202
+ @type_override_columns << column
203
+ end
204
+ end
205
+ end
206
+
207
+ # `declaration_calls` variant that also descends into a concern's `included do … end` block (the
208
+ # ActiveSupport::Concern idiom where models' shared `serialize` declarations live).
209
+ def type_override_declaration_calls(body)
210
+ return [] if body.nil?
211
+
212
+ body.compact_child_nodes.flat_map do |node|
213
+ next [] unless node.is_a?(Prism::CallNode)
214
+
215
+ if %i[with_options included].include?(node.name) && node.block.is_a?(Prism::BlockNode)
216
+ type_override_declaration_calls(node.block.body)
217
+ else
218
+ [node]
219
+ end
220
+ end
221
+ end
222
+
223
+ # True when an `attribute :col, <type>` call's type argument is a custom type CLASS (a constant),
224
+ # whose runtime value is a rich object — not a built-in `:symbol` type, which stays a scalar.
225
+ def custom_type_attribute?(node)
226
+ type_arg = node.arguments&.arguments&.[](1)
227
+ type_arg.is_a?(Prism::ConstantReadNode) || type_arg.is_a?(Prism::ConstantPathNode)
228
+ end
229
+
230
+ # Renders a constant-path node (`Admin::User`, `::ApplicationRecord`) as a String. Returns nil for
188
231
  # shapes the discoverer chooses not to handle.
189
232
  def constant_path_name(node)
190
233
  return nil if node.nil?
@@ -208,9 +251,8 @@ module Rigor
208
251
  end
209
252
  end
210
253
 
211
- # Looks for `self.table_name = "..."` at the top level of
212
- # the class body. Returns the literal String when found,
213
- # nil otherwise.
254
+ # Looks for `self.table_name = "..."` at the top level of the class body. Returns the literal
255
+ # String when found, nil otherwise.
214
256
  def lookup_table_name_override(body)
215
257
  return nil if body.nil?
216
258
 
@@ -224,19 +266,14 @@ module Rigor
224
266
  nil
225
267
  end
226
268
 
227
- # Recognised single-instance and collection association
228
- # DSL methods. The kind drives the eventual return-type
229
- # contribution: singular associations narrow to
230
- # `Nominal[Target] | nil`, plural ones narrow to
231
- # `ActiveRecord::Relation[Target]`.
269
+ # Recognised single-instance and collection association DSL methods. The kind drives the eventual
270
+ # return-type contribution: singular associations narrow to `Nominal[Target] | nil`, plural ones
271
+ # narrow to `ActiveRecord::Relation[Target]`.
232
272
  #
233
- # `composed_of` value-object aggregations and
234
- # `delegated_type` roles are folded in here too both
235
- # accept the association name as a `where` / `find_by`
236
- # query key, so omitting them turns every such query into
237
- # a false `unknown-column`. `composed_of` resolves to its
238
- # value class (a real target); `delegated_type` is
239
- # polymorphic (no single target).
273
+ # `composed_of` value-object aggregations and `delegated_type` roles are folded in here too — both
274
+ # accept the association name as a `where` / `find_by` query key, so omitting them turns every
275
+ # such query into a false `unknown-column`. `composed_of` resolves to its value class (a real
276
+ # target); `delegated_type` is polymorphic (no single target).
240
277
  ASSOCIATION_METHODS = {
241
278
  belongs_to: :singular,
242
279
  has_one: :singular,
@@ -247,28 +284,21 @@ module Rigor
247
284
  }.freeze
248
285
  private_constant :ASSOCIATION_METHODS
249
286
 
250
- # Association DSL methods that are ALWAYS polymorphic —
251
- # the accessor has no single static target class.
252
- # `belongs_to` / `has_one` become polymorphic only with
253
- # an explicit `polymorphic: true` option.
287
+ # Association DSL methods that are ALWAYS polymorphic — the accessor has no single static target
288
+ # class. `belongs_to` / `has_one` become polymorphic only with an explicit `polymorphic: true`
289
+ # option.
254
290
  POLYMORPHIC_BY_DEFAULT = %i[delegated_type].freeze
255
291
  private_constant :POLYMORPHIC_BY_DEFAULT
256
292
 
257
- # Class-body declaration calls — the top-level `CallNode`s
258
- # PLUS those nested inside a `with_options(...) do … end`
259
- # block. `with_options` is Rails' idiom for sharing
260
- # options across a group of `belongs_to` / `validates` /
261
- # etc. declarations; without descending into it every
262
- # association / enum / validation declared inside is
263
- # invisible to the discoverer, turning `where(<assoc>:
264
- # ...)` into a false `unknown-column`. Nested
265
- # `with_options` blocks recurse.
293
+ # Class-body declaration calls — the top-level `CallNode`s PLUS those nested inside a
294
+ # `with_options(...) do … end` block. `with_options` is Rails' idiom for sharing options across a
295
+ # group of `belongs_to` / `validates` / etc. declarations; without descending into it every
296
+ # association / enum / validation declared inside is invisible to the discoverer, turning
297
+ # `where(<assoc>: ...)` into a false `unknown-column`. Nested `with_options` blocks recurse.
266
298
  #
267
- # The options the `with_options` call itself carries (e.g.
268
- # `with_options class_name: 'Account'`) are NOT merged into
269
- # the nested calls discovering the declaration name is
270
- # what clears the false positive; the merged-option target
271
- # precision is a separate refinement.
299
+ # The options the `with_options` call itself carries (e.g. `with_options class_name: 'Account'`)
300
+ # are NOT merged into the nested calls — discovering the declaration name is what clears the
301
+ # false positive; the merged-option target precision is a separate refinement.
272
302
  def declaration_calls(body)
273
303
  return [] if body.nil?
274
304
 
@@ -283,17 +313,13 @@ module Rigor
283
313
  end
284
314
  end
285
315
 
286
- # Walks the class body for association DSL calls and
287
- # returns a list of rows shaped:
316
+ # Walks the class body for association DSL calls and returns a list of rows shaped:
288
317
  #
289
318
  # { name: "user", kind: :singular, target: "User" }
290
319
  #
291
- # The `target` is resolved from an explicit
292
- # `class_name: "Foo"` option when supplied, otherwise
293
- # inferred from the association name via
294
- # {Inflector.classify}. Calls whose first arg is not a
295
- # Symbol literal (or whose `class_name:` is a non-literal
296
- # expression) decline rather than guess.
320
+ # The `target` is resolved from an explicit `class_name: "Foo"` option when supplied, otherwise
321
+ # inferred from the association name via {Inflector.classify}. Calls whose first arg is not a
322
+ # Symbol literal (or whose `class_name:` is a non-literal expression) decline rather than guess.
297
323
  def lookup_associations(body)
298
324
  return [] if body.nil?
299
325
 
@@ -319,10 +345,8 @@ module Rigor
319
345
  polymorphic = POLYMORPHIC_BY_DEFAULT.include?(node.name) ||
320
346
  association_option(args, "polymorphic") == true
321
347
 
322
- # A polymorphic association has no single static target
323
- # class `target` is nil and the flow contribution
324
- # declines to narrow rather than inventing a wrong
325
- # `Nominal[<classified-name>]`.
348
+ # A polymorphic association has no single static target class — `target` is nil and the flow
349
+ # contribution declines to narrow rather than inventing a wrong `Nominal[<classified-name>]`.
326
350
  if polymorphic
327
351
  target = nil
328
352
  else
@@ -334,15 +358,12 @@ module Rigor
334
358
  nullable: association_nullable?(node.name, args) }
335
359
  end
336
360
 
337
- # Whether a `:singular` association's accessor can return
338
- # `nil`. `has_one` genuinely can (no associated record
339
- # `nil`). `belongs_to` is **required (non-`nil`) by default
340
- # since Rails 5** (`belongs_to_required_by_default`); it
341
- # becomes nullable only when the call passes `optional: true`
342
- # or `required: false`. `composed_of` is non-nullable
343
- # unless `allow_nil: true`. `delegated_type` roles are
344
- # required. A non-literal option value declines to the
345
- # default rather than guessing.
361
+ # Whether a `:singular` association's accessor can return `nil`. `has_one` genuinely can (no
362
+ # associated record → `nil`). `belongs_to` is **required (non-`nil`) by default since Rails 5**
363
+ # (`belongs_to_required_by_default`); it becomes nullable only when the call passes
364
+ # `optional: true` or `required: false`. `composed_of` is non-nullable unless `allow_nil: true`.
365
+ # `delegated_type` roles are required. A non-literal option value declines to the default rather
366
+ # than guessing.
346
367
  def association_nullable?(method_name, args)
347
368
  case method_name
348
369
  when :has_one
@@ -357,9 +378,8 @@ module Rigor
357
378
  end
358
379
  end
359
380
 
360
- # Reads a literal boolean association option (`optional:` /
361
- # `required:`). Returns `true` / `false` for a literal, or
362
- # `nil` when the key is absent or its value is non-literal.
381
+ # Reads a literal boolean association option (`optional:` / `required:`). Returns `true` / `false`
382
+ # for a literal, or `nil` when the key is absent or its value is non-literal.
363
383
  def association_option(args, key)
364
384
  args.each do |arg|
365
385
  next unless arg.is_a?(Prism::KeywordHashNode)
@@ -388,10 +408,9 @@ module Rigor
388
408
  nil
389
409
  end
390
410
 
391
- # `enum status: { active: 0, archived: 1 }` (Rails ≤6)
392
- # and `enum :status, [:active, :archived]` (Rails 7+).
393
- # Returns `Hash<column_name => Array<Symbol>>`.
394
- # Non-literal forms decline rather than guess.
411
+ # `enum status: { active: 0, archived: 1 }` (Rails ≤6) and `enum :status, [:active, :archived]`
412
+ # (Rails 7+). Returns `Hash<column_name => Array<Symbol>>`. Non-literal forms decline rather than
413
+ # guess.
395
414
  def lookup_enums(body)
396
415
  return {} if body.nil?
397
416
 
@@ -445,10 +464,9 @@ module Rigor
445
464
  end
446
465
  end
447
466
 
448
- # `scope :active, -> { ... }`. Records the scope name
449
- # only (the body is intentionally NOT introspected
450
- # the caller contributes `ActiveRecord::Relation[Model]`
451
- # based on the name alone via `class_scope_return_type`).
467
+ # `scope :active, -> { ... }`. Records the scope name only (the body is intentionally NOT
468
+ # introspected the caller contributes `ActiveRecord::Relation[Model]` based on the name alone
469
+ # via `class_scope_return_type`).
452
470
  def lookup_scopes(body)
453
471
  return [] if body.nil?
454
472
 
@@ -468,11 +486,9 @@ module Rigor
468
486
  scopes.freeze
469
487
  end
470
488
 
471
- # `validates :name, presence: true, length: { maximum: 100 }`.
472
- # Records the attribute name (the validator option set
473
- # is ignored the value here is the diagnostic
474
- # `validates :unknown_attr` surfacing when the attribute
475
- # isn't a column on the table).
489
+ # `validates :name, presence: true, length: { maximum: 100 }`. Records the attribute name (the
490
+ # validator option set is ignored — the value here is the diagnostic `validates :unknown_attr`
491
+ # surfacing when the attribute isn't a column on the table).
476
492
  def lookup_validations(body)
477
493
  return [] if body.nil?
478
494
 
@@ -487,10 +503,9 @@ module Rigor
487
503
  attrs.uniq.freeze
488
504
  end
489
505
 
490
- # `before_save :foo`, `after_create :bar`, etc. Records
491
- # the referenced method name (a Symbol literal). The
492
- # diagnostic value is "did you forget to `def` this?".
493
- # Block callbacks (`before_save { ... }`) decline.
506
+ # `before_save :foo`, `after_create :bar`, etc. Records the referenced method name (a Symbol
507
+ # literal). The diagnostic value is "did you forget to `def` this?". Block callbacks
508
+ # (`before_save { ... }`) decline.
494
509
  CALLBACK_METHODS = %i[
495
510
  before_validation after_validation
496
511
  before_save after_save around_save
@@ -517,12 +532,10 @@ module Rigor
517
532
  targets.freeze
518
533
  end
519
534
 
520
- # `alias_attribute :new_name, :old_name`. Records the
521
- # mapping so the analyzer accepts the alias as a query
522
- # key without it every `where(<alias>: ...)` /
523
- # `find_by(<alias>: ...)` call surfaces as a false
524
- # `unknown-column`. Returns `Hash<alias => target>`;
525
- # non-Symbol-literal forms decline rather than guess.
535
+ # `alias_attribute :new_name, :old_name`. Records the mapping so the analyzer accepts the alias
536
+ # as a query key without it every `where(<alias>: ...)` / `find_by(<alias>: ...)` call surfaces
537
+ # as a false `unknown-column`. Returns `Hash<alias => target>`; non-Symbol-literal forms decline
538
+ # rather than guess.
526
539
  def lookup_aliases(body)
527
540
  return {} if body.nil?
528
541
 
@@ -543,10 +556,8 @@ module Rigor
543
556
  aliases.freeze
544
557
  end
545
558
 
546
- # Collects every Symbol-literal positional argument
547
- # from a CallNode. Used by both `lookup_validations`
548
- # and `lookup_callbacks` to extract the attribute /
549
- # method name list.
559
+ # Collects every Symbol-literal positional argument from a CallNode. Used by both
560
+ # `lookup_validations` and `lookup_callbacks` to extract the attribute / method name list.
550
561
  def symbol_args(node)
551
562
  args = node.arguments&.arguments
552
563
  return [] if args.nil?