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
@@ -4,40 +4,34 @@ require "rigor/plugin"
4
4
 
5
5
  require_relative "activerecord/schema_table"
6
6
  require_relative "activerecord/schema_parser"
7
+ require_relative "activerecord/structure_sql_parser"
7
8
  require_relative "activerecord/model_index"
8
9
  require_relative "activerecord/model_discoverer"
9
10
  require_relative "activerecord/analyzer"
10
11
 
11
12
  module Rigor
12
13
  module Plugin
13
- # rigor-activerecord — types ActiveRecord finder + relation
14
- # calls against the project's `db/schema.rb` and discovered
15
- # AR model classes.
14
+ # rigor-activerecord — types ActiveRecord finder + relation calls against the project's `db/schema.rb`
15
+ # and discovered AR model classes.
16
16
  #
17
17
  # ## Architecture
18
18
  #
19
19
  # Two cached producers per plugin run:
20
20
  #
21
- # 1. `:schema_table` reads `db/schema.rb` via the `IoBoundary`
22
- # and parses it through {SchemaParser} into a
23
- # {SchemaTable} mapping `table_name { column_name
24
- # Column }`.
25
- # 2. `:model_index` walks every `.rb` file under the
26
- # configured `model_search_paths`, finds class declarations
27
- # whose direct superclass is in `model_base_classes`, and
28
- # composes them with the schema table into a {ModelIndex}.
21
+ # 1. `:schema_table` reads `db/schema.rb` via the `IoBoundary` and parses it through {SchemaParser} into
22
+ # a {SchemaTable} mapping `table_name { column_name → Column }`. When `db/schema.rb` is absent it
23
+ # falls back to a PostgreSQL `db/structure.sql` (`schema_format = :sql` apps) via {StructureSqlParser}.
24
+ # 2. `:model_index` walks every `.rb` file under the configured `model_search_paths`, finds class
25
+ # declarations whose direct superclass is in `model_base_classes`, and composes them with the schema
26
+ # table into a {ModelIndex}.
29
27
  #
30
- # Both producers ride `Plugin::Base#cache_for` (ADR-60 WD3
31
- # record-and-validate): each producer's in-block boundary reads
32
- # are captured into its dependency descriptor after the block
33
- # runs, and `model_index`'s `watch:` covers model-file additions,
34
- # so editing `db/schema.rb`, editing any model, or adding a new
35
- # model file invalidates exactly the right cache entry.
28
+ # Both producers ride `Plugin::Base#cache_for` (ADR-60 WD3 record-and-validate): each producer's
29
+ # in-block boundary reads are captured into its dependency descriptor after the block runs, and
30
+ # `model_index`'s `watch:` covers model-file additions, so editing `db/schema.rb`, editing any model, or
31
+ # adding a new model file invalidates exactly the right cache entry.
36
32
  #
37
- # The per-file `#diagnostics_for_file` hook delegates to
38
- # {Analyzer}, which walks Prism and emits diagnostics for
39
- # `Model.find` / `Model.find_by` / `Model.where` calls
40
- # against the index.
33
+ # The per-file `#diagnostics_for_file` hook delegates to {Analyzer}, which walks Prism and emits
34
+ # diagnostics for `Model.find` / `Model.find_by` / `Model.where` calls against the index.
41
35
  #
42
36
  # ## Configuration
43
37
  #
@@ -48,75 +42,73 @@ module Rigor
48
42
  # model_search_paths: ["app/models"]
49
43
  # model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"]
50
44
  #
51
- # All three keys default to the values shown above. The class
52
- # name `Rigor::Plugin::Activerecord` (single capital R) is
53
- # intentional keeps the constant lookup distinct from
54
- # `::ActiveRecord` even though the gem name is hyphenated.
45
+ # All three keys default to the values shown above. The class name `Rigor::Plugin::Activerecord` (single
46
+ # capital R) is intentional — keeps the constant lookup distinct from `::ActiveRecord` even though the
47
+ # gem name is hyphenated.
55
48
  #
56
- # Note: this plugin is the seventh worked example. It does NOT
57
- # require `active_record` at runtime it only reads project
58
- # source, the same way the other examples do. Rigor stays
59
- # decoupled from Rails.
49
+ # Note: this plugin is the seventh worked example. It does NOT require `active_record` at runtime — it
50
+ # only reads project source, the same way the other examples do. Rigor stays decoupled from Rails.
60
51
  class Activerecord < Rigor::Plugin::Base
61
52
  manifest(
62
53
  id: "activerecord",
63
- # Bumped 2026-05-28 — implicit-self class-side AR call
64
- # resolution: `select(:uri).group(:uri)` inside a scope
65
- # lambda body / class-method body now contributes
66
- # `Relation[Model]` via `scope.self_type` instead of
67
- # falling through to `Kernel#select` (the IO multiplexer,
68
- # `Array[String]` return). Plus `:select` added to the
69
- # relation-entry-point list.
54
+ # Bumped 2026-05-28 — implicit-self class-side AR call resolution: `select(:uri).group(:uri)` inside
55
+ # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
56
+ # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
57
+ # `:select` added to the relation-entry-point list.
70
58
  version: "0.5.0",
71
59
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
72
60
  config_schema: {
73
61
  "schema_file" => { kind: :string, default: "db/schema.rb" },
62
+ # `schema_format = :sql` projects (GitLab-class apps) commit a PostgreSQL `db/structure.sql`
63
+ # instead of `db/schema.rb`. Used as the fallback schema source when `schema_file` is absent.
64
+ "structure_sql_file" => { kind: :string, default: "db/structure.sql" },
74
65
  "model_search_paths" => { kind: :array, default: ["app/models"] },
75
66
  "model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
76
67
  },
77
68
  produces: [:model_index],
78
- # ADR-25 — the bundled `ActiveRecord::Relation` RBS that
79
- # relation-typed call sites (`has_many` accessors,
80
- # `Model.where`, scopes) dispatch against.
69
+ # ADR-25 — the bundled `ActiveRecord::Relation` RBS that relation-typed call sites (`has_many`
70
+ # accessors, `Model.where`, scopes) dispatch against.
81
71
  signature_paths: ["sig"],
82
- # ADR-26 — `ActiveRecord::Relation` is an "open" receiver:
83
- # it delegates an unbounded set of user-defined scopes /
84
- # class methods to its model, so `call.undefined-method`
85
- # must not fire for it. `CheckRules` reads this manifest
86
- # field and skips the rule for the class.
72
+ # ADR-26 — `ActiveRecord::Relation` is an "open" receiver: it delegates an unbounded set of
73
+ # user-defined scopes / class methods to its model, so `call.undefined-method` must not fire for
74
+ # it. `CheckRules` reads this manifest field and skips the rule for the class.
87
75
  open_receivers: ["ActiveRecord::Relation"]
88
76
  )
89
77
 
90
- # The class the bundled `sig/active_record/relation.rbs`
91
- # describes; `dynamic_return` contributes
92
- # `ActiveRecord::Relation[Model]` for relation-returning
93
- # call sites (`has_many` accessors, `Model.where`, scopes).
78
+ # The class the bundled `sig/active_record/relation.rbs` describes; `dynamic_return` contributes
79
+ # `ActiveRecord::Relation[Model]` for relation-returning call sites (`has_many` accessors,
80
+ # `Model.where`, scopes).
94
81
  RELATION_CLASS_NAME = "ActiveRecord::Relation"
95
82
 
96
- # Cached: parsed schema table. The producer reads `@schema_file`
97
- # via `io_boundary.read_file` so the descriptor picks up the
98
- # digest, then parses through {SchemaParser}.
83
+ # Cached: parsed schema table. The producer reads `@schema_file` via `io_boundary.read_file` so the
84
+ # descriptor picks up the digest, then parses through {SchemaParser}. When `db/schema.rb` is absent
85
+ # the producer falls back to a PostgreSQL `db/structure.sql` (`schema_format = :sql` apps) parsed
86
+ # through {StructureSqlParser} — both reads are captured into the record-and-validate descriptor.
99
87
  producer :schema_table do |_params|
100
- contents = io_boundary.read_file(@schema_file)
101
- SchemaParser.parse(contents)
88
+ SchemaParser.parse(io_boundary.read_file(@schema_file))
89
+ rescue Errno::ENOENT
90
+ StructureSqlParser.parse(io_boundary.read_file(@structure_sql_file))
102
91
  end
103
92
 
104
- # Cached: model index. Walks every model file, then composes
105
- # the rows with the cached schema table. `watch:` (ADR-60 WD3)
106
- # covers model-file additions; the discoverer's in-block reads
107
- # are captured into the record-and-validate dependency
108
- # descriptor after the block runs.
93
+ # Cached: model index. Walks every model file, then composes the rows with the cached schema table.
94
+ # `watch:` (ADR-60 WD3) covers model-file additions; the discoverer's in-block reads are captured
95
+ # into the record-and-validate dependency descriptor after the block runs.
109
96
  producer :model_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
110
- rows = ModelDiscoverer.new(
97
+ discoverer = ModelDiscoverer.new(
111
98
  io_boundary: io_boundary,
112
99
  search_paths: @model_search_paths,
113
100
  base_classes: @model_base_classes
114
- ).discover
115
- ModelIndex.build(model_rows: rows, schema_table: schema_table_or_nil)
101
+ )
102
+ rows = discoverer.discover
103
+ ModelIndex.build(
104
+ model_rows: rows, schema_table: schema_table_or_nil,
105
+ type_override_columns: discoverer.type_override_columns
106
+ )
116
107
  end
117
108
 
118
109
  def init(_services)
119
110
  @schema_file = config.fetch("schema_file")
111
+ @structure_sql_file = config.fetch("structure_sql_file")
120
112
  @model_search_paths = Array(config.fetch("model_search_paths")).map(&:to_s)
121
113
  @model_base_classes = Array(config.fetch("model_base_classes")).map(&:to_s)
122
114
  @schema_table = nil
@@ -125,15 +117,11 @@ module Rigor
125
117
  @load_errors = []
126
118
  end
127
119
 
128
- # ADR-9 cross-plugin publication. Builds the model index
129
- # eagerly during the per-run `prepare(services)` pass and
130
- # publishes a flat Hash form to the shared fact store so
131
- # downstream Tier-2 consumers (rigor-actionpack Phase 1
132
- # strong-parameter validation, rigor-factorybot Phase 1
133
- # (c) attribute → column cross-check, future plugins
134
- # that need to know "what columns does class `User`
135
- # expose?") can read it without coupling to this
136
- # plugin's carrier classes.
120
+ # ADR-9 cross-plugin publication. Builds the model index eagerly during the per-run
121
+ # `prepare(services)` pass and publishes a flat Hash form to the shared fact store so downstream
122
+ # Tier-2 consumers (rigor-actionpack Phase 1 strong-parameter validation, rigor-factorybot Phase 1
123
+ # (c) attribute column cross-check, future plugins that need to know "what columns does class
124
+ # `User` expose?") can read it without coupling to this plugin's carrier classes.
137
125
  #
138
126
  # The published shape:
139
127
  #
@@ -143,11 +131,9 @@ module Rigor
143
131
  # ...
144
132
  # }
145
133
  #
146
- # Consumers do `services.fact_store.read(plugin_id:
147
- # "activerecord", name: :model_index)` and look up by
148
- # class name. Discovery failures (missing schema,
149
- # unparseable models) leave the fact unpublished — the
150
- # consumer's own degrade path runs (typically a no-op).
134
+ # Consumers do `services.fact_store.read(plugin_id: "activerecord", name: :model_index)` and look up
135
+ # by class name. Discovery failures (missing schema, unparseable models) leave the fact unpublished —
136
+ # the consumer's own degrade path runs (typically a no-op).
151
137
  def prepare(services)
152
138
  index = model_index
153
139
  return if index.nil? || index.empty?
@@ -162,12 +148,10 @@ module Rigor
162
148
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
163
149
  index = model_index
164
150
  if index.nil?
165
- # Project-global error (missing `db/schema.rb`, parse
166
- # failure, etc.) emit once per run rather than once
167
- # per analyzed file. On a Redmine-shape project that
168
- # uses migrations only (no `schema.rb`), the old path
169
- # produced 346 identical load-errors; on a Solidus
170
- # monorepo (no top-level `schema.rb`), 999.
151
+ # Project-global error (missing `db/schema.rb`, parse failure, etc.) — emit once per run rather
152
+ # than once per analyzed file. On a Redmine-shape project that uses migrations only (no
153
+ # `schema.rb`), the old path produced 346 identical load-errors; on a Solidus monorepo (no
154
+ # top-level `schema.rb`), 999.
171
155
  return [] if @load_errors_emitted
172
156
 
173
157
  @load_errors_emitted = true
@@ -179,15 +163,11 @@ module Rigor
179
163
  Analyzer.new(path: path, model_index: index).analyze(root).diagnostics
180
164
  end
181
165
 
182
- # Rails migration files (`db/migrate/<timestamp>_*.rb`)
183
- # and post-migration files (`db/post_migrate/`) reference
184
- # the EVOLVING schema at the time the migration was
185
- # written `User.where(admin: ...)` is valid when the
186
- # migration ran on a schema that still had the `admin`
187
- # column, even though the current `db/schema.rb` no
188
- # longer carries it. Validating these files against the
189
- # CURRENT schema is a category error; the column
190
- # diagnostics MUST stay silent.
166
+ # Rails migration files (`db/migrate/<timestamp>_*.rb`) and post-migration files
167
+ # (`db/post_migrate/`) reference the EVOLVING schema at the time the migration was written —
168
+ # `User.where(admin: ...)` is valid when the migration ran on a schema that still had the `admin`
169
+ # column, even though the current `db/schema.rb` no longer carries it. Validating these files
170
+ # against the CURRENT schema is a category error; the column diagnostics MUST stay silent.
191
171
  MIGRATION_PATH_PATTERNS = [
192
172
  %r{(\A|/)db/migrate/},
193
173
  %r{(\A|/)db/post_migrate/}
@@ -201,48 +181,36 @@ module Rigor
201
181
  MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
202
182
  end
203
183
 
204
- # The class-side finder / relation entry-point names
205
- # `finder_return_type` recognises. Static half of the
206
- # `dynamic_return` name gate; the run-time half comes from
207
- # the model index (scopes, associations, columns).
184
+ # The class-side finder / relation entry-point names `finder_return_type` recognises. Static half of
185
+ # the `dynamic_return` name gate; the run-time half comes from the model index (scopes,
186
+ # associations, columns).
208
187
  FINDER_METHOD_NAMES = %i[find find_by! find_by where all order limit none select].freeze
209
188
  private_constant :FINDER_METHOD_NAMES
210
189
 
211
- # Return-type contribution via the run-time `methods:` name
212
- # gate (ADR-52 slice 5b). `Model.find(id)` narrows the call
213
- # site's return type to `Nominal[Model]`, so chained calls
214
- # (`User.find(1).name`) resolve through the analyzer's
215
- # normal dispatch instead of the RBS-level untyped
216
- # fall-back; scopes, association accessors, and column
217
- # readers narrow per the paths below.
190
+ # Return-type contribution via the run-time `methods:` name gate (ADR-52 slice 5b). `Model.find(id)`
191
+ # narrows the call site's return type to `Nominal[Model]`, so chained calls (`User.find(1).name`)
192
+ # resolve through the analyzer's normal dispatch instead of the RBS-level untyped fall-back; scopes,
193
+ # association accessors, and column readers narrow per the paths below.
218
194
  #
219
- # WHY a method-name gate and not `receivers:` — the ADR-52
220
- # "rigor-activerecord blocker": a project model not in RBS
221
- # types its constant as `Dynamic[top]`, so a receiver-type
222
- # gate declines exactly the calls this plugin exists for. A
223
- # *name* gate never reads the receiver type; the block keeps
224
- # the plugin's own AST-constant / `self_type` / `type_of`
225
- # resolution, so the Dynamic-constant case still reaches it
226
- # (the same shape as rigor-sorbet's catalog path). The set
227
- # the static finder names every scope, association, and
228
- # column name (plus `column?` predicate forms) the model
229
- # index discovered — is exactly the union of names the four
230
- # resolution paths below can return a type for, so gating on
231
- # it is byte-identical to the old ungated hook. It is broad
232
- # (`name`, `id`, …), but membership is one Set probe and the
233
- # expensive block runs only on candidate hits.
195
+ # WHY a method-name gate and not `receivers:` — the ADR-52 "rigor-activerecord blocker": a project
196
+ # model not in RBS types its constant as `Dynamic[top]`, so a receiver-type gate declines exactly the
197
+ # calls this plugin exists for. A *name* gate never reads the receiver type; the block keeps the
198
+ # plugin's own AST-constant / `self_type` / `type_of` resolution, so the Dynamic-constant case still
199
+ # reaches it (the same shape as rigor-sorbet's catalog path). The set — the static finder names ∪
200
+ # every scope, association, and column name (plus `column?` predicate forms) the model index
201
+ # discovered is exactly the union of names the four resolution paths below can return a type for,
202
+ # so gating on it is byte-identical to the old ungated hook. It is broad (`name`, `id`, …), but
203
+ # membership is one Set probe and the expensive block runs only on candidate hits.
234
204
  dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
235
205
  contribution_return_type(call_node, scope)
236
206
  end
237
207
 
238
208
  private
239
209
 
240
- # The run-time name gate: finders ∪ scopes ∪ associations ∪
241
- # column readers (+ `?` predicates). Resolved lazily on first
242
- # dispatch (after `#prepare` built the index), memoised by the
243
- # engine. Returns [] when discovery found nothing — the gate
244
- # then declines every call, matching the old hook's
245
- # `index.nil? || index.empty?` early return.
210
+ # The run-time name gate: finders ∪ scopes ∪ associations ∪ column readers (+ `?` predicates).
211
+ # Resolved lazily on first dispatch (after `#prepare` built the index), memoised by the engine.
212
+ # Returns [] when discovery found nothing — the gate then declines every call, matching the old
213
+ # hook's `index.nil? || index.empty?` early return.
246
214
  def recognised_method_names
247
215
  index = model_index
248
216
  return [] if index.nil? || index.empty?
@@ -259,8 +227,8 @@ module Rigor
259
227
  names
260
228
  end
261
229
 
262
- # Resolution body for `dynamic_return` — same four-path
263
- # order, returning the bare type the contract expects.
230
+ # Resolution body for `dynamic_return` — same four-path order, returning the bare type the contract
231
+ # expects.
264
232
  def contribution_return_type(call_node, scope)
265
233
  return nil unless call_node.is_a?(Prism::CallNode)
266
234
 
@@ -287,24 +255,19 @@ module Rigor
287
255
  class_scope_return_type(call_node, entry)
288
256
  end
289
257
 
290
- # Implicit-self class-side call: `select(:uri)` /
291
- # `where(active: true)` inside a `def self.<method>` body,
292
- # a class body, or a scope lambda body (`scope :x, -> { ... }`).
293
- # The surrounding `self_type` is `Singleton[Model]` in all
294
- # three cases, so the same finder / scope / relation entry-
295
- # point resolution that handles `Model.where(...)` applies.
258
+ # Implicit-self class-side call: `select(:uri)` / `where(active: true)` inside a `def
259
+ # self.<method>` body, a class body, or a scope lambda body (`scope :x, -> { ... }`). The
260
+ # surrounding `self_type` is `Singleton[Model]` in all three cases, so the same finder / scope /
261
+ # relation entry-point resolution that handles `Model.where(...)` applies.
296
262
  #
297
- # Without this, `select(:uri)` inside a class body falls
298
- # through to RBS dispatch on `Singleton[Account]`, which
299
- # finds `Kernel#select` (the IO multiplexer) at
300
- # `core/kernel.rbs` — its `Array[String]` return masks the
301
- # AR class-side `select`'s relation return type, so the
302
- # canonical scope-body idiom
263
+ # Without this, `select(:uri)` inside a class body falls through to RBS dispatch on
264
+ # `Singleton[Account]`, which finds `Kernel#select` (the IO multiplexer) at `core/kernel.rbs` — its
265
+ # `Array[String]` return masks the AR class-side `select`'s relation return type, so the canonical
266
+ # scope-body idiom
303
267
  #
304
268
  # scope :duplicate_uris, -> { select(:uri).group(:uri) }
305
269
  #
306
- # types `select(:uri)` as `Array[String]` and the chained
307
- # `.group` as `undefined-method`.
270
+ # types `select(:uri)` as `Array[String]` and the chained `.group` as `undefined-method`.
308
271
  def implicit_self_class_call_return_type(call_node, scope, index)
309
272
  return nil if scope.nil?
310
273
 
@@ -318,12 +281,10 @@ module Rigor
318
281
  class_scope_return_type(call_node, entry)
319
282
  end
320
283
 
321
- # Class-side finders + the class-side relation entry points.
322
- # `find` / `find_by!` return the model; `find_by` adds the
323
- # `nil` arm; `where` / `all` / `order` / `limit` / `none`
324
- # open a relation. The relation then carries its element
325
- # type through any further chained query method via the
326
- # bundled `ActiveRecord::Relation` RBS.
284
+ # Class-side finders + the class-side relation entry points. `find` / `find_by!` return the model;
285
+ # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
286
+ # relation then carries its element type through any further chained query method via the bundled
287
+ # `ActiveRecord::Relation` RBS.
327
288
  def finder_return_type(call_node, entry)
328
289
  case call_node.name
329
290
  when :find
@@ -331,8 +292,8 @@ module Rigor
331
292
 
332
293
  Rigor::Type::Combinator.nominal_of(entry.class_name)
333
294
  when :find_by!
334
- # The bang variant raises `RecordNotFound` instead of
335
- # returning `nil`, so the result is non-nullable.
295
+ # The bang variant raises `RecordNotFound` instead of returning `nil`, so the result is
296
+ # non-nullable.
336
297
  Rigor::Type::Combinator.nominal_of(entry.class_name)
337
298
  when :find_by
338
299
  Rigor::Type::Combinator.union(
@@ -341,31 +302,26 @@ module Rigor
341
302
  )
342
303
  when :where, :all, :order, :limit, :none, :select
343
304
  # `:select` was added to close Mastodon's
344
- # `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }`
345
- # shape: the implicit-self `select(:uri)` inside the
346
- # scope lambda body had been resolving to `Kernel#select`
347
- # (IO multiplexer, return `Array[String]`), masking the
348
- # AR class-side relation entry point. The rest of the
349
- # query DSL chains through the bundled `ActiveRecord::Relation`
350
- # RBS once a relation is open.
305
+ # `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }` shape: the implicit-self
306
+ # `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select` (IO
307
+ # multiplexer, return `Array[String]`), masking the AR class-side relation entry point. The rest
308
+ # of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a relation is
309
+ # open.
351
310
  relation_of(entry.class_name)
352
311
  end
353
312
  end
354
313
 
355
- # `Post.published` / `Post.recent(5)` — a user-declared
356
- # `scope` returns a relation of the model regardless of the
357
- # arguments it takes.
314
+ # `Post.published` / `Post.recent(5)` — a user-declared `scope` returns a relation of the model
315
+ # regardless of the arguments it takes.
358
316
  def class_scope_return_type(call_node, entry)
359
317
  return nil unless entry.scope?(call_node.name)
360
318
 
361
319
  relation_of(entry.class_name)
362
320
  end
363
321
 
364
- # `ActiveRecord::Relation[Model]` — the type the bundled
365
- # `sig/active_record/relation.rbs` describes. The class is
366
- # declared `open_receivers` in the manifest, so a chained
367
- # scope call the bundled RBS cannot enumerate does not
368
- # surface as `call.undefined-method` (ADR-26).
322
+ # `ActiveRecord::Relation[Model]` — the type the bundled `sig/active_record/relation.rbs` describes.
323
+ # The class is declared `open_receivers` in the manifest, so a chained scope call the bundled RBS
324
+ # cannot enumerate does not surface as `call.undefined-method` (ADR-26).
369
325
  def relation_of(model_class_name)
370
326
  Rigor::Type::Combinator.nominal_of(
371
327
  RELATION_CLASS_NAME,
@@ -373,24 +329,17 @@ module Rigor
373
329
  )
374
330
  end
375
331
 
376
- # A scope invoked on an already-typed relation
377
- # (`User.where(active: true).published`) keeps the relation
378
- # type through the chain. The bundled `ActiveRecord::Relation`
379
- # RBS cannot enumerate user-defined scopes, so without this
380
- # the chain would lose its element type after the first
381
- # scope call. Non-scope methods decline — the RBS tier
382
- # resolves `where` / `order` / `each` / `first` precisely.
383
- # Scopes may take arguments (`relation.recent(5)`), so —
384
- # unlike `instance_call_return_type` — argument calls are
385
- # not skipped.
332
+ # A scope invoked on an already-typed relation (`User.where(active: true).published`) keeps the
333
+ # relation type through the chain. The bundled `ActiveRecord::Relation` RBS cannot enumerate
334
+ # user-defined scopes, so without this the chain would lose its element type after the first scope
335
+ # call. Non-scope methods decline — the RBS tier resolves `where` / `order` / `each` / `first`
336
+ # precisely. Scopes may take arguments (`relation.recent(5)`), so unlike `instance_call_return_type`
337
+ # argument calls are not skipped.
386
338
  #
387
- # The cheap `scope_name?` pre-check is load-bearing: it
388
- # gates the `scope.type_of(receiver)` call so the receiver
389
- # type is computed ONLY when the method name could be a
390
- # scope. `type_of` on a call receiver re-enters dispatch,
391
- # and calling it for every call node in a long method chain
392
- # is pathologically expensive — the pre-check keeps the
393
- # cost off the hot path.
339
+ # The cheap `scope_name?` pre-check is load-bearing: it gates the `scope.type_of(receiver)` call so
340
+ # the receiver type is computed ONLY when the method name could be a scope. `type_of` on a call
341
+ # receiver re-enters dispatch, and calling it for every call node in a long method chain is
342
+ # pathologically expensive the pre-check keeps the cost off the hot path.
394
343
  def relation_call_return_type(call_node, scope, index)
395
344
  return nil if call_node.receiver.nil?
396
345
  return nil unless scope_name?(call_node.name, index)
@@ -405,16 +354,15 @@ module Rigor
405
354
  relation_of(model_name)
406
355
  end
407
356
 
408
- # Whether `name` is a declared `scope` on ANY model in the
409
- # index. A run-lifetime memoised Set so the per-call check
410
- # in `relation_call_return_type` stays O(1).
357
+ # Whether `name` is a declared `scope` on ANY model in the index. A run-lifetime memoised Set so the
358
+ # per-call check in `relation_call_return_type` stays O(1).
411
359
  def scope_name?(name, index)
412
360
  @all_scope_names ||= index.entries.each_value.flat_map(&:scopes).to_set
413
361
  @all_scope_names.include?(name.to_s)
414
362
  end
415
363
 
416
- # When `type` is `ActiveRecord::Relation[Nominal[Model]]`,
417
- # returns the model class name; nil for any other type.
364
+ # When `type` is `ActiveRecord::Relation[Nominal[Model]]`, returns the model class name; nil for
365
+ # any other type.
418
366
  def relation_element_class_name(type)
419
367
  return nil unless type.is_a?(Rigor::Type::Nominal)
420
368
  return nil unless type.class_name == RELATION_CLASS_NAME
@@ -423,13 +371,10 @@ module Rigor
423
371
  element.class_name if element.is_a?(Rigor::Type::Nominal)
424
372
  end
425
373
 
426
- # Instance-side navigation: when the call's receiver
427
- # resolves to `Nominal[Model]` and the method name matches
428
- # a discovered association OR a table column, the call site
429
- # gets a precise return type. Calls with arguments are
430
- # skipped — accessor / association calls take no args, and
431
- # argument forms (`user.posts(limit: 10)`, `user.name = x`)
432
- # route through Rails APIs this slice does not model.
374
+ # Instance-side navigation: when the call's receiver resolves to `Nominal[Model]` and the method name
375
+ # matches a discovered association OR a table column, the call site gets a precise return type.
376
+ # Calls with arguments are skipped — accessor / association calls take no args, and argument forms
377
+ # (`user.posts(limit: 10)`, `user.name = x`) route through Rails APIs this slice does not model.
433
378
  def instance_call_return_type(call_node, scope, index)
434
379
  return nil unless call_node.arguments.nil?
435
380
 
@@ -444,16 +389,12 @@ module Rigor
444
389
  column_return_type(entry, call_node.name)
445
390
  end
446
391
 
447
- # The return type for an association accessor. A `belongs_to`
448
- # / `has_one` singular association narrows to the target
449
- # model `belongs_to` is required (non-`nil`) by default
450
- # since Rails 5 so it is `Nominal[Target]`, while `has_one`
451
- # (and an `optional: true` / `required: false` `belongs_to`)
452
- # adds the `nil` arm. A `has_many` / `has_and_belongs_to_many`
453
- # collection narrows to `ActiveRecord::Relation[Target]` so
454
- # chained query / iteration calls resolve. A polymorphic
455
- # association has no single static target and declines
456
- # rather than inventing a wrong type.
392
+ # The return type for an association accessor. A `belongs_to` / `has_one` singular association
393
+ # narrows to the target model — `belongs_to` is required (non-`nil`) by default since Rails 5 so it
394
+ # is `Nominal[Target]`, while `has_one` (and an `optional: true` / `required: false` `belongs_to`)
395
+ # adds the `nil` arm. A `has_many` / `has_and_belongs_to_many` collection narrows to
396
+ # `ActiveRecord::Relation[Target]` so chained query / iteration calls resolve. A polymorphic
397
+ # association has no single static target and declines rather than inventing a wrong type.
457
398
  def association_return_type(entry, method_name)
458
399
  association = entry.association(method_name)
459
400
  return nil if association.nil?
@@ -470,19 +411,14 @@ module Rigor
470
411
  end
471
412
  end
472
413
 
473
- # Instance-side column access. `user.name` on a
474
- # `Nominal[User]` receiver narrows to the column's value
475
- # type; `user.name?` (the ActiveRecord-generated predicate)
476
- # narrows to `bool`.
414
+ # Instance-side column access. `user.name` on a `Nominal[User]` receiver narrows to the column's
415
+ # value type; `user.name?` (the ActiveRecord-generated predicate) narrows to `bool`.
477
416
  #
478
- # The contributed type is deliberately NON-nullable even
479
- # though the DB column may permit `NULL`: Rails code calls
480
- # column accessors directly (`user.email.downcase`) as a
481
- # matter of course, and contributing `T | nil` would light
482
- # up that idiom with `possible-nil-receiver` across an
483
- # entire codebase. Under-reporting a nil column is a false
484
- # negative; over-reporting it is a false positive — and the
485
- # project ranks the latter as the worse failure.
417
+ # The contributed type is deliberately NON-nullable even though the DB column may permit `NULL`:
418
+ # Rails code calls column accessors directly (`user.email.downcase`) as a matter of course, and
419
+ # contributing `T | nil` would light up that idiom with `possible-nil-receiver` across an entire
420
+ # codebase. Under-reporting a nil column is a false negative; over-reporting it is a false positive
421
+ # and the project ranks the latter as the worse failure.
486
422
  def column_return_type(entry, method_name)
487
423
  name = method_name.to_s
488
424
  predicate = name.end_with?("?")
@@ -498,12 +434,10 @@ module Rigor
498
434
  column.array? ? Rigor::Type::Combinator.nominal_of("Array", type_args: [inner]) : inner
499
435
  end
500
436
 
501
- # Maps a `SchemaTable::Column#ruby_type` string to a Rigor
502
- # type. `"Object"` (json / jsonb / unrecognised column
503
- # types) declines `Nominal[Object]` would be NARROWER
504
- # than the RBS-erased envelope and could surface false
505
- # `call.undefined-method` on a value whose real shape the
506
- # plugin cannot model.
437
+ # Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb /
438
+ # unrecognised column types) declines — `Nominal[Object]` would be NARROWER than the RBS-erased
439
+ # envelope and could surface false `call.undefined-method` on a value whose real shape the plugin
440
+ # cannot model.
507
441
  def ruby_type_to_type(ruby_type)
508
442
  case ruby_type
509
443
  when "bool" then bool_type
@@ -546,12 +480,9 @@ module Rigor
546
480
  node.arguments.arguments.size
547
481
  end
548
482
 
549
- # Marshal-clean Hash form for the cross-plugin fact
550
- # store. Consumers (rigor-actionpack Phase 1,
551
- # rigor-factorybot Phase 1 (c), ...) get a flat
552
- # `class_name → { table:, columns: }` map without
553
- # depending on this plugin's `ModelIndex` /
554
- # `SchemaTable::Column` carrier classes.
483
+ # Marshal-clean Hash form for the cross-plugin fact store. Consumers (rigor-actionpack Phase 1,
484
+ # rigor-factorybot Phase 1 (c), ...) get a flat `class_name → { table:, columns: }` map without
485
+ # depending on this plugin's `ModelIndex` / `SchemaTable::Column` carrier classes.
555
486
  def index_to_published_hash(index)
556
487
  index.entries.transform_values do |entry|
557
488
  {
@@ -573,11 +504,9 @@ module Rigor
573
504
  table = schema_table_or_nil
574
505
  return nil if table.nil?
575
506
 
576
- # ADR-60 WD3 record-and-validate: the producer's own in-block
577
- # `ModelDiscoverer` reads are captured into the dependency
578
- # descriptor after the block runs, and the producer's `watch:`
579
- # covers model-file additions — so no priming walk is needed
580
- # (it used to run the discover twice).
507
+ # ADR-60 WD3 record-and-validate: the producer's own in-block `ModelDiscoverer` reads are
508
+ # captured into the dependency descriptor after the block runs, and the producer's `watch:` covers
509
+ # model-file additions so no priming walk is needed (it used to run the discover twice).
581
510
  @model_index = cache_for(:model_index, params: {}).call
582
511
  rescue StandardError => e
583
512
  @load_errors << "model index build failed: #{e.class}: #{e.message}"
@@ -586,26 +515,24 @@ module Rigor
586
515
 
587
516
  def schema_table_or_nil
588
517
  return @schema_table if @schema_table
589
- # Memoize the *failure*, not just the success: `model_index`
590
- # (and thus this method) is invoked per AR call site, so a
591
- # missing / unreadable schema file would otherwise re-attempt
592
- # the read and append a fresh interpolated error string to
593
- # `@load_errors` on every call. On a large Rails app that grew
594
- # `@load_errors` to millions of retained strings (measured: 4.2 M
595
- # strings / ~1.5 GB on Redmine). One attempt is enough.
518
+ # Memoize the *failure*, not just the success: `model_index` (and thus this method) is invoked per
519
+ # AR call site, so a missing / unreadable schema file would otherwise re-attempt the read and
520
+ # append a fresh interpolated error string to `@load_errors` on every call. On a large Rails app
521
+ # that grew `@load_errors` to millions of retained strings (measured: 4.2 M strings / ~1.5 GB on
522
+ # Redmine). One attempt is enough.
596
523
  return nil if @schema_load_attempted
597
524
 
598
525
  @schema_load_attempted = true
599
- # ADR-60 WD3 record-and-validate: the producer reads
600
- # `@schema_file` in-block, and that read is captured into the
601
- # dependency descriptor after the block runs — so no priming
602
- # read is needed here.
526
+ # ADR-60 WD3 record-and-validate: the producer reads `@schema_file` in-block, and that read is
527
+ # captured into the dependency descriptor after the block runs so no priming read is needed
528
+ # here.
603
529
  @schema_table = cache_for(:schema_table, params: {}).call
604
530
  rescue Plugin::AccessDeniedError => e
605
531
  @load_errors << "rigor-activerecord: #{e.message}"
606
532
  nil
607
533
  rescue Errno::ENOENT
608
- @load_errors << "rigor-activerecord: schema file `#{@schema_file}` not found; AR call checks skipped"
534
+ @load_errors << "rigor-activerecord: schema file `#{@schema_file}` (or `#{@structure_sql_file}`) " \
535
+ "not found; AR call checks skipped"
609
536
  nil
610
537
  rescue StandardError => e
611
538
  @load_errors << "rigor-activerecord: failed to parse `#{@schema_file}`: #{e.class}: #{e.message}"