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
@@ -3,44 +3,32 @@
3
3
  module Rigor
4
4
  module Plugin
5
5
  class Activerecord < Rigor::Plugin::Base
6
- # Maps a discovered ActiveRecord model class name to its
7
- # resolved table name and the column set the schema attaches
8
- # to that table. Marshal-clean; the cache producer round-
9
- # trips it through the standard pair.
6
+ # Maps a discovered ActiveRecord model class name to its resolved table name and the column set the
7
+ # schema attaches to that table. Marshal-clean; the cache producer round-trips it through the
8
+ # standard pair.
10
9
  #
11
10
  # Construction is two-phase by design:
12
- # 1. {ModelDiscoverer} walks the project source for model
13
- # class declarations (direct base-class children plus
14
- # transitive STI subclasses) and yields a row per model.
15
- # 2. The plugin combines those rows with the parsed
16
- # {SchemaTable} to produce this index.
11
+ # 1. {ModelDiscoverer} walks the project source for model class declarations (direct base-class
12
+ # children plus transitive STI subclasses) and yields a row per model.
13
+ # 2. The plugin combines those rows with the parsed {SchemaTable} to produce this index.
17
14
  #
18
- # `table_name_override` is non-nil when the source contained
19
- # `self.table_name = "..."`. When nil, the table name
20
- # derives from {Inflector.tableize}.
15
+ # `table_name_override` is non-nil when the source contained `self.table_name = "..."`. When nil,
16
+ # the table name derives from {Inflector.tableize}.
21
17
  #
22
- # Single-table-inheritance subclasses (`class Admin < User`)
23
- # carry an `sti_parent:` pointer; their {Entry} resolves its
24
- # table from the root model and inherits the chain's
25
- # declared associations / enums / aliases / scopes /
26
- # validations / callbacks.
18
+ # Single-table-inheritance subclasses (`class Admin < User`) carry an `sti_parent:` pointer; their
19
+ # {Entry} resolves its table from the root model and inherits the chain's declared associations /
20
+ # enums / aliases / scopes / validations / callbacks.
27
21
  class ModelIndex
28
- # `associations` is a frozen `Array<Hash>` where each
29
- # row carries `{ name:, kind:, target:, nullable: }`:
22
+ # `associations` is a frozen `Array<Hash>` where each row carries `{ name:, kind:, target:,
23
+ # nullable: }`:
30
24
  #
31
- # - `name` — String, the association method name as
32
- # the user invokes it (`"posts"`).
33
- # - `kind` `:singular` (`belongs_to` / `has_one`)
34
- # or `:collection` (`has_many`).
35
- # - `target` — String, the target class name resolved
36
- # either from an explicit `class_name:`
25
+ # - `name` — String, the association method name as the user invokes it (`"posts"`).
26
+ # - `kind` — `:singular` (`belongs_to` / `has_one`) or `:collection` (`has_many`).
27
+ # - `target` String, the target class name resolved either from an explicit `class_name:`
37
28
  # option or via {Inflector.classify}.
38
- # - `nullable` — Boolean; whether a `:singular` accessor
39
- # can return `nil`. `has_one` `true`;
40
- # `belongs_to` → `false` (required by default
41
- # since Rails 5) unless `optional: true` /
42
- # `required: false`. Meaningless for
43
- # `:collection` rows.
29
+ # - `nullable` — Boolean; whether a `:singular` accessor can return `nil`. `has_one` → `true`;
30
+ # `belongs_to` `false` (required by default since Rails 5) unless `optional: true`
31
+ # / `required: false`. Meaningless for `:collection` rows.
44
32
  Entry = Struct.new(:class_name, :table_name, :columns, :associations,
45
33
  :enums, :scopes, :validations, :callbacks, :aliases,
46
34
  keyword_init: true) do
@@ -71,8 +59,7 @@ module Rigor
71
59
  # `scopes` is `Array<scope_name>`.
72
60
  def scope?(name) = scopes.include?(name.to_s)
73
61
 
74
- # `validations` is `Array<attribute_name>` covering
75
- # both `validates :name, ...` and the
62
+ # `validations` is `Array<attribute_name>` covering both `validates :name, ...` and the
76
63
  # `validates_*_of :name, ...` shorthand families.
77
64
  def validation?(name) = validations.include?(name.to_s)
78
65
  def validated_attributes = validations
@@ -80,8 +67,8 @@ module Rigor
80
67
  # `callbacks` is `Array<{ name:, callback: }>`.
81
68
  def callback_targets = callbacks.map { |c| c[:name] }
82
69
 
83
- # `aliases` is `Hash<alias_name => target_attribute>`
84
- # populated from `alias_attribute` declarations.
70
+ # `aliases` is `Hash<alias_name => target_attribute>` populated from `alias_attribute`
71
+ # declarations.
85
72
  def alias?(name) = aliases.key?(name.to_s)
86
73
  def resolve_alias(name) = aliases[name.to_s]
87
74
  end
@@ -101,22 +88,20 @@ module Rigor
101
88
  def class_names = entries.keys
102
89
  def empty? = entries.empty?
103
90
 
104
- def self.build(model_rows:, schema_table:)
91
+ def self.build(model_rows:, schema_table:, type_override_columns: nil)
105
92
  rows_by_name = model_rows.to_h { |row| [row.fetch(:class_name), row] }
93
+ overrides = type_override_columns || []
106
94
 
107
95
  entries = model_rows.each_with_object({}) do |row, acc|
108
96
  class_name = row.fetch(:class_name)
109
- # The STI ancestry chain, root → self. For a plain
110
- # (non-STI) model this is just `[row]`.
97
+ # The STI ancestry chain, root → self. For a plain (non-STI) model this is just `[row]`.
111
98
  chain = sti_chain(row, rows_by_name)
112
99
  table_name = sti_table_name(chain)
113
- columns = schema_table.columns_for(table_name) || []
100
+ columns = apply_type_overrides(schema_table.columns_for(table_name) || [], overrides)
114
101
 
115
- # STI children inherit their ancestors' declared
116
- # associations / enums / aliases / scopes /
117
- # validations / callbacks. Without the merge a
118
- # `where(<parent-association>: ...)` on the child
119
- # would surface as a false `unknown-column`.
102
+ # STI children inherit their ancestors' declared associations / enums / aliases / scopes /
103
+ # validations / callbacks. Without the merge a `where(<parent-association>: ...)` on the
104
+ # child would surface as a false `unknown-column`.
120
105
  acc[class_name] = Entry.new(
121
106
  class_name: class_name,
122
107
  table_name: table_name,
@@ -132,8 +117,25 @@ module Rigor
132
117
  new(entries.freeze)
133
118
  end
134
119
 
135
- # The STI ancestry chain for a row, ordered root self.
136
- # Walks `sti_parent` pointers, guarding against a cycle.
120
+ # Remaps every type-overridden column's `ruby_type` to `"Object"` so instance-side column narrowing
121
+ # declines to narrow it (the `"Object"` case in `ruby_type_to_type`). A `serialize` / `mount_uploader`
122
+ # / custom-`attribute` column reads as a rich object at runtime, not its SQL scalar, so narrowing it
123
+ # to e.g. `String` false-positives on the object's methods (`note.position.diff_refs`). The column
124
+ # stays in the set, so `where(col: ...)` existence validation is unaffected — only its value type.
125
+ def self.apply_type_overrides(columns, overrides)
126
+ return columns if overrides.empty?
127
+
128
+ columns.map do |column|
129
+ next column unless overrides.include?(column.name)
130
+
131
+ SchemaTable::Column.new(
132
+ name: column.name, type: column.type, ruby_type: "Object", array: column.array
133
+ )
134
+ end
135
+ end
136
+
137
+ # The STI ancestry chain for a row, ordered root → self. Walks `sti_parent` pointers, guarding
138
+ # against a cycle.
137
139
  def self.sti_chain(row, rows_by_name, seen = [])
138
140
  class_name = row.fetch(:class_name)
139
141
  parent_name = row[:sti_parent]
@@ -145,9 +147,8 @@ module Rigor
145
147
  sti_chain(parent, rows_by_name, seen + [class_name]) + [row]
146
148
  end
147
149
 
148
- # The effective table name for an STI chain: the nearest
149
- # explicit `self.table_name =` override walking leaf
150
- # root, else the name inflected from the root class.
150
+ # The effective table name for an STI chain: the nearest explicit `self.table_name =` override
151
+ # walking leaf root, else the name inflected from the root class.
151
152
  def self.sti_table_name(chain)
152
153
  chain.reverse_each do |row|
153
154
  override = row[:table_name_override]
@@ -156,18 +157,16 @@ module Rigor
156
157
  Rigor::Plugin::Inflector.tableize(strip_leading_namespace(chain.first.fetch(:class_name)))
157
158
  end
158
159
 
159
- # Dedups association-style rows by `:name`, keeping the
160
- # LAST occurrence so a child redeclaration overrides the
161
- # inherited ancestor row.
160
+ # Dedups association-style rows by `:name`, keeping the LAST occurrence so a child redeclaration
161
+ # overrides the inherited ancestor row.
162
162
  def self.merge_named_rows(rows)
163
163
  seen = {}
164
164
  rows.each { |row| seen[row[:name]] = row }
165
165
  seen.values.map(&:freeze).freeze
166
166
  end
167
167
 
168
- # `Hash<column => Array<value>>` merged across the STI
169
- # chain; a child redeclaration of the same enum column
170
- # overrides the ancestor's value list.
168
+ # `Hash<column => Array<value>>` merged across the STI chain; a child redeclaration of the same
169
+ # enum column overrides the ancestor's value list.
171
170
  def self.merge_enums(chain)
172
171
  chain.each_with_object({}) do |row, acc|
173
172
  (row[:enums] || {}).each { |col, values| acc[col] = Array(values).map(&:freeze).freeze }
@@ -181,10 +180,8 @@ module Rigor
181
180
  end.freeze
182
181
  end
183
182
 
184
- # `::User` → `User`. The discoverer might prefix with
185
- # `::` for top-level constants depending on how it
186
- # resolved the path; the table-name derivation uses the
187
- # short form regardless.
183
+ # `::User` → `User`. The discoverer might prefix with `::` for top-level constants depending on
184
+ # how it resolved the path; the table-name derivation uses the short form regardless.
188
185
  def self.strip_leading_namespace(name)
189
186
  name.start_with?("::") ? name[2..] : name
190
187
  end
@@ -21,30 +21,23 @@ module Rigor
21
21
  # end
22
22
  # end
23
23
  #
24
- # `t.references "x"` becomes an `x_id` integer column
25
- # (foreign-key indices and constraints are ignored only the
26
- # column shape matters for type inference); add a `polymorphic:
27
- # true` option and an `x_type` string column is emitted too.
28
- # `t.timestamps` adds `created_at` and `updated_at` datetime
29
- # columns. `t.column "x", :type` is the generic column form.
30
- # Any other `t.<method> "name"` call is treated as a column
31
- # whose type symbol is the method name — unknown types degrade
32
- # to `Object` per `SchemaTable.ruby_type_for` rather than being
33
- # dropped. Only the structural calls in {NON_COLUMN_METHODS}
34
- # (indexes, constraints, foreign keys) are skipped.
24
+ # `t.references "x"` becomes an `x_id` integer column (foreign-key indices and constraints are
25
+ # ignored only the column shape matters for type inference); add a `polymorphic: true` option and
26
+ # an `x_type` string column is emitted too. `t.timestamps` adds `created_at` and `updated_at`
27
+ # datetime columns. `t.column "x", :type` is the generic column form. Any other `t.<method> "name"`
28
+ # call is treated as a column whose type symbol is the method name — unknown types degrade to
29
+ # `Object` per `SchemaTable.ruby_type_for` rather than being dropped. Only the structural calls in
30
+ # {NON_COLUMN_METHODS} (indexes, constraints, foreign keys) are skipped.
35
31
  #
36
- # Designed for the Prism interpretation pattern from
37
- # rigor-lisp-eval — recursive descent on the AST, no eval.
32
+ # Designed for the Prism interpretation pattern from rigor-lisp-eval — recursive descent on the AST,
33
+ # no eval.
38
34
  class SchemaParser
39
35
  TIMESTAMPS_COLUMNS = %w[created_at updated_at].freeze
40
36
 
41
- # Structural `t.<method>` calls inside a `create_table`
42
- # block that declare indexes / constraints rather than
43
- # columns. Everything NOT in this set that carries a
44
- # literal column name is treated as a column declaration
45
- # so a real column is never silently dropped — a dropped
46
- # column turns every query against it into a false
47
- # `unknown-column` diagnostic.
37
+ # Structural `t.<method>` calls inside a `create_table` block that declare indexes / constraints
38
+ # rather than columns. Everything NOT in this set that carries a literal column name is treated as
39
+ # a column declaration so a real column is never silently dropped — a dropped column turns every
40
+ # query against it into a false `unknown-column` diagnostic.
48
41
  NON_COLUMN_METHODS = %i[
49
42
  index check_constraint exclusion_constraint
50
43
  unique_constraint foreign_key primary_keys
@@ -114,10 +107,9 @@ module Rigor
114
107
  false
115
108
  end
116
109
 
117
- # Walks the block body collecting `t.<method>(...)` calls.
118
- # Skips nested blocks (e.g. inside `if`-conditioned columns)
119
- # only at the top level — for richer schema constructs the
120
- # parser falls back silently.
110
+ # Walks the block body collecting `t.<method>(...)` calls. Skips nested blocks (e.g. inside
111
+ # `if`-conditioned columns) only at the top level — for richer schema constructs the parser falls
112
+ # back silently.
121
113
  def collect_column_calls(node, &)
122
114
  return if node.nil?
123
115
 
@@ -139,13 +131,11 @@ module Rigor
139
131
  when :column
140
132
  parse_generic_column(call_node)
141
133
  else
142
- # Structural DSL call (index / constraint / FK) —
143
- # not a column.
134
+ # Structural DSL call (index / constraint / FK) — not a column.
144
135
  return nil if NON_COLUMN_METHODS.include?(method)
145
136
 
146
- # Any other `t.<method> "name"` is a column. The
147
- # method name is the type symbol; unknown types
148
- # degrade to `Object` rather than dropping the column.
137
+ # Any other `t.<method> "name"` is a column. The method name is the type symbol; unknown
138
+ # types degrade to `Object` rather than dropping the column.
149
139
  parse_typed_column(method, call_node)
150
140
  end
151
141
  end
@@ -162,11 +152,9 @@ module Rigor
162
152
  )
163
153
  end
164
154
 
165
- # `t.references "x"` adds an `x_id` integer column.
166
- # `t.references "x", polymorphic: true` additionally adds
167
- # an `x_type` string column without it every
168
- # `where(x_type: ...)` on the polymorphic owner surfaces
169
- # as a false `unknown-column`.
155
+ # `t.references "x"` adds an `x_id` integer column. `t.references "x", polymorphic: true`
156
+ # additionally adds an `x_type` string column — without it every `where(x_type: ...)` on the
157
+ # polymorphic owner surfaces as a false `unknown-column`.
170
158
  def parse_references_column(call_node)
171
159
  name = string_argument(call_node, 0)
172
160
  return nil if name.nil?
@@ -184,10 +172,9 @@ module Rigor
184
172
  keyword_true?(call_node, :polymorphic)
185
173
  end
186
174
 
187
- # Returns true iff `call_node` has a `name: true` keyword
188
- # argument. Used to detect schema modifiers like
189
- # `t.bigint "status_ids", array: true` (Postgres array
190
- # column) and `t.references "x", polymorphic: true`.
175
+ # Returns true iff `call_node` has a `name: true` keyword argument. Used to detect schema
176
+ # modifiers like `t.bigint "status_ids", array: true` (Postgres array column) and `t.references
177
+ # "x", polymorphic: true`.
191
178
  def keyword_true?(call_node, name)
192
179
  return false if call_node.arguments.nil?
193
180
 
@@ -204,9 +191,8 @@ module Rigor
204
191
  false
205
192
  end
206
193
 
207
- # `t.column "name", "type"` / `t.column "name", :type` —
208
- # the explicit generic column form. The type lives in the
209
- # second argument; an absent type degrades to `:string`.
194
+ # `t.column "name", "type"` / `t.column "name", :type` — the explicit generic column form. The
195
+ # type lives in the second argument; an absent type degrades to `:string`.
210
196
  def parse_generic_column(call_node)
211
197
  name = string_argument(call_node, 0)
212
198
  return nil if name.nil?
@@ -3,18 +3,15 @@
3
3
  module Rigor
4
4
  module Plugin
5
5
  class Activerecord < Rigor::Plugin::Base
6
- # Parsed `db/schema.rb`. Maps each table name to its column
7
- # set; each column carries its declared type. Marshal-clean
8
- # by construction so the cache producer can round-trip it
9
- # without a custom serialize / deserialize pair.
6
+ # Parsed `db/schema.rb`. Maps each table name to its column set; each column carries its declared
7
+ # type. Marshal-clean by construction so the cache producer can round-trip it without a custom
8
+ # serialize / deserialize pair.
10
9
  #
11
- # The mapping from Rails column types to Ruby class names is
12
- # deliberately conservative — `:string`/`:text` → `String`,
13
- # `:integer`/`:bigint` → `Integer`, `:boolean` → `bool`,
14
- # `:datetime`/`:timestamp` `Time`, `:date` `Date`,
15
- # `:decimal`/`:float` → `Float`. Exotic types (json, jsonb,
16
- # ltree, hstore, custom) fall back to `Object` so the
17
- # plugin stays silent rather than guessing.
10
+ # The mapping from Rails column types to Ruby class names is deliberately conservative —
11
+ # `:string`/`:text` → `String`, `:integer`/`:bigint` → `Integer`, `:boolean` → `bool`,
12
+ # `:datetime`/`:timestamp` → `Time`, `:date` → `Date`, `:decimal`/`:float` → `Float`. Exotic types
13
+ # (json, jsonb, ltree, hstore, custom) fall back to `Object` so the plugin stays silent rather than
14
+ # guessing.
18
15
  class SchemaTable
19
16
  Column = Struct.new(:name, :type, :ruby_type, :array, keyword_init: true) do
20
17
  def to_h = { name: name, type: type, ruby_type: ruby_type, array: array }
@@ -40,21 +37,17 @@ module Rigor
40
37
  binary: "String",
41
38
  json: "Object",
42
39
  jsonb: "Object",
43
- # PostgreSQL-flavoured string-ish column types Rails
44
- # schemas commonly carry. `uuid` / `citext` / `inet`
45
- # all behave as `String` for query purposes; mapping
46
- # them here keeps the column out of the "unknown type
47
- # → dropped column → false unknown-column" path.
40
+ # PostgreSQL-flavoured string-ish column types Rails schemas commonly carry. `uuid` / `citext` /
41
+ # `inet` all behave as `String` for query purposes; mapping them here keeps the column out of
42
+ # the "unknown type dropped column false unknown-column" path.
48
43
  uuid: "String",
49
44
  citext: "String",
50
45
  inet: "String"
51
46
  }.freeze
52
47
 
53
- # Implicit columns that every Rails table has unless the
54
- # schema explicitly opts out. The plugin assumes these
55
- # exist; users who run `create_table id: false` get no
56
- # implicit `id` column from the parser, but most apps
57
- # never disable it.
48
+ # Implicit columns that every Rails table has unless the schema explicitly opts out. The plugin
49
+ # assumes these exist; users who run `create_table id: false` get no implicit `id` column from
50
+ # the parser, but most apps never disable it.
58
51
  IMPLICIT_COLUMNS = [
59
52
  Column.new(name: "id", type: :integer, ruby_type: "Integer").freeze
60
53
  ].freeze
@@ -86,9 +79,8 @@ module Rigor
86
79
 
87
80
  def table_names = tables.keys
88
81
 
89
- # Maps a Rails column type symbol to its Ruby class name.
90
- # Returns "Object" for unknown types the analyzer treats
91
- # that as "do not narrow" (silent on unknowns).
82
+ # Maps a Rails column type symbol to its Ruby class name. Returns "Object" for unknown types —
83
+ # the analyzer treats that as "do not narrow" (silent on unknowns).
92
84
  def self.ruby_type_for(column_type)
93
85
  RUBY_TYPE_MAPPING.fetch(column_type.to_sym, "Object")
94
86
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema_table"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class Activerecord < Rigor::Plugin::Base
8
+ # Parses a PostgreSQL `db/structure.sql` (the `schema_format = :sql` schema dump) into the same
9
+ # {SchemaTable} the Ruby-DSL {SchemaParser} produces, so a project that commits `structure.sql`
10
+ # instead of `schema.rb` is no longer inert (GitLab-class apps use `structure.sql`; without this the
11
+ # plugin skips every AR call check and — worse — degrades ordinary relation chains to `Array`,
12
+ # cascading false diagnostics).
13
+ #
14
+ # The dump is regular enough to read line-by-line without a SQL parser. `pg_dump` emits, per table:
15
+ #
16
+ # CREATE TABLE namespaces (
17
+ # id bigint NOT NULL,
18
+ # name character varying NOT NULL,
19
+ # visibility_level integer DEFAULT 20 NOT NULL,
20
+ # tag_ids bigint[],
21
+ # created_at timestamp without time zone
22
+ # );
23
+ #
24
+ # Load-bearing regularities this relies on: column names / types are lowercase, column modifiers
25
+ # (`NOT NULL`, `DEFAULT …`, `GENERATED …`, `COLLATE …`) are UPPERCASE, one column per line, the body
26
+ # closes on a line starting with `)`. So the type is the run of tokens before the first
27
+ # uppercase-initial token, and a table-level constraint line (`CONSTRAINT …`, `PRIMARY KEY …`) is
28
+ # recognised by its uppercase leading keyword and skipped. Anything it cannot map (custom enum,
29
+ # `tsvector`, `ltree`) degrades to `Object` via {SchemaTable.ruby_type_for} — never a dropped column,
30
+ # which would turn every query on it into a false `unknown-column`.
31
+ class StructureSqlParser
32
+ # Normalised PostgreSQL type (lowercased, length/precision + `[]` stripped) → the Rails column-type
33
+ # symbol {SchemaTable.ruby_type_for} already understands. Unmapped types fall through to their own
34
+ # symbol, which `ruby_type_for` maps to `Object` (the safe "do not narrow" default).
35
+ PG_TYPE_TO_RAILS = {
36
+ "bigint" => :bigint, "int8" => :bigint, "bigserial" => :bigint,
37
+ "integer" => :integer, "int" => :integer, "int4" => :integer, "serial" => :integer,
38
+ "smallint" => :integer, "int2" => :integer, "smallserial" => :integer,
39
+ "boolean" => :boolean, "bool" => :boolean,
40
+ "text" => :text,
41
+ "character varying" => :string, "varchar" => :string,
42
+ "character" => :string, "char" => :string, "name" => :string, "citext" => :citext,
43
+ "timestamp without time zone" => :datetime, "timestamp with time zone" => :datetime,
44
+ "timestamp" => :datetime, "timestamptz" => :datetime,
45
+ "date" => :date,
46
+ "time without time zone" => :time, "time with time zone" => :time, "time" => :time,
47
+ "numeric" => :decimal, "decimal" => :decimal, "money" => :decimal,
48
+ "double precision" => :float, "real" => :float, "float8" => :float, "float4" => :float,
49
+ "bytea" => :binary,
50
+ "json" => :json, "jsonb" => :jsonb,
51
+ "uuid" => :uuid, "inet" => :inet, "cidr" => :string, "macaddr" => :string
52
+ }.freeze
53
+
54
+ # Uppercase leading keywords that mark a table-level constraint line rather than a column.
55
+ CONSTRAINT_KEYWORDS = %w[CONSTRAINT PRIMARY UNIQUE CHECK FOREIGN EXCLUDE LIKE PARTITION].freeze
56
+
57
+ # @param source [String] contents of `db/structure.sql`
58
+ # @return [SchemaTable]
59
+ def self.parse(source)
60
+ new.parse(source)
61
+ end
62
+
63
+ def parse(source)
64
+ tables = {}
65
+ each_create_table(source.to_s) do |table_name, body_lines|
66
+ columns = parse_columns(body_lines)
67
+ tables[table_name] = columns unless columns.empty?
68
+ end
69
+ SchemaTable.new(tables.freeze)
70
+ end
71
+
72
+ private
73
+
74
+ # Yields `[table_name, body_lines]` for every `CREATE TABLE … (` … `)` block. Body lines are the raw
75
+ # lines between the opening `(` line and the closing `)` line.
76
+ def each_create_table(source)
77
+ lines = source.lines
78
+ index = 0
79
+ while index < lines.length
80
+ match = lines[index].match(/\ACREATE (?:UNLOGGED |TEMPORARY )?TABLE (?:IF NOT EXISTS )?(\S+)\s*\(/)
81
+ unless match
82
+ index += 1
83
+ next
84
+ end
85
+
86
+ table_name = normalize_table_name(match[1])
87
+ index += 1
88
+ body = []
89
+ while index < lines.length && lines[index] !~ /\A\s*\)/
90
+ body << lines[index]
91
+ index += 1
92
+ end
93
+ yield(table_name, body) if table_name
94
+ index += 1 # step past the closing `)` line
95
+ end
96
+ end
97
+
98
+ # Strips quotes and a leading schema qualifier. Only the default `public` schema (or an unqualified
99
+ # name) is accepted; other schemas (`gitlab_partitions_dynamic.*`, etc.) are partitions of a base
100
+ # table already emitted unqualified, so returning nil skips them without losing a real table.
101
+ def normalize_table_name(raw)
102
+ name = raw.delete('"')
103
+ if name.include?(".")
104
+ schema, table = name.split(".", 2)
105
+ return nil unless schema == "public"
106
+
107
+ name = table
108
+ end
109
+ name.empty? ? nil : name
110
+ end
111
+
112
+ def parse_columns(body_lines)
113
+ columns = {}
114
+ # A column's `DEFAULT` may carry a multi-line single-quoted string (a serialized YAML default,
115
+ # `DEFAULT '--- []\n'::character varying`). The column name + type sit before the `DEFAULT`, so
116
+ # the first physical line still parses correctly; the continuation lines must be skipped or they
117
+ # read as bogus columns (`'::character varying`). A line with an ODD count of single quotes opens
118
+ # (or closes) such a string — PG doubles a literal quote (`''`), so escapes stay even and only a
119
+ # genuinely unterminated string toggles the state.
120
+ in_string = false
121
+ body_lines.each do |raw|
122
+ if in_string
123
+ in_string = false if raw.count("'").odd?
124
+ next
125
+ end
126
+ in_string = true if raw.count("'").odd?
127
+
128
+ line = raw.strip.chomp(",")
129
+ next if line.empty?
130
+
131
+ name, rest = line.split(/\s+/, 2)
132
+ next if name.nil? || rest.nil?
133
+ next if CONSTRAINT_KEYWORDS.include?(name) # a table-level constraint, not a column
134
+
135
+ column = build_column(name.delete('"'), rest)
136
+ columns[column.name] = column if column
137
+ end
138
+ columns.freeze
139
+ end
140
+
141
+ def build_column(name, rest)
142
+ type_str, array = extract_type(rest)
143
+ return nil if type_str.empty?
144
+
145
+ rails_type = PG_TYPE_TO_RAILS.fetch(type_str, type_str.to_sym)
146
+ SchemaTable::Column.new(
147
+ name: name,
148
+ type: rails_type,
149
+ ruby_type: SchemaTable.ruby_type_for(rails_type),
150
+ array: array
151
+ )
152
+ end
153
+
154
+ # The type is every token before the first uppercase-initial token (a column modifier such as
155
+ # `NOT`, `NULL`, `DEFAULT`, `GENERATED`, `COLLATE`). A trailing `[]` marks a Postgres array column;
156
+ # a `(length[,scale])` specifier is dropped. Returns `[normalized_type, array?]`.
157
+ def extract_type(rest)
158
+ type_tokens = []
159
+ rest.split(/\s+/).each do |token|
160
+ break if token.match?(/\A[A-Z]/)
161
+
162
+ type_tokens << token
163
+ end
164
+ type_str = type_tokens.join(" ")
165
+ array = type_str.include?("[]")
166
+ type_str = type_str.gsub("[]", "").gsub(/\([0-9,\s]*\)/, "").strip.downcase
167
+ [type_str, array]
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end