rigortype 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -10,34 +10,26 @@ require_relative "activerecord/analyzer"
10
10
 
11
11
  module Rigor
12
12
  module Plugin
13
- # rigor-activerecord — types ActiveRecord finder + relation
14
- # calls against the project's `db/schema.rb` and discovered
15
- # AR model classes.
13
+ # rigor-activerecord — types ActiveRecord finder + relation calls against the project's `db/schema.rb`
14
+ # and discovered AR model classes.
16
15
  #
17
16
  # ## Architecture
18
17
  #
19
18
  # Two cached producers per plugin run:
20
19
  #
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}.
20
+ # 1. `:schema_table` reads `db/schema.rb` via the `IoBoundary` and parses it through {SchemaParser} into
21
+ # a {SchemaTable} mapping `table_name { column_name → Column }`.
22
+ # 2. `:model_index` walks every `.rb` file under the configured `model_search_paths`, finds class
23
+ # declarations whose direct superclass is in `model_base_classes`, and composes them with the schema
24
+ # table into a {ModelIndex}.
29
25
  #
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.
26
+ # Both producers ride `Plugin::Base#cache_for` (ADR-60 WD3 record-and-validate): each producer's
27
+ # in-block boundary reads are captured into its dependency descriptor after the block runs, and
28
+ # `model_index`'s `watch:` covers model-file additions, so editing `db/schema.rb`, editing any model, or
29
+ # adding a new model file invalidates exactly the right cache entry.
36
30
  #
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.
31
+ # The per-file `#diagnostics_for_file` hook delegates to {Analyzer}, which walks Prism and emits
32
+ # diagnostics for `Model.find` / `Model.find_by` / `Model.where` calls against the index.
41
33
  #
42
34
  # ## Configuration
43
35
  #
@@ -48,25 +40,19 @@ module Rigor
48
40
  # model_search_paths: ["app/models"]
49
41
  # model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"]
50
42
  #
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.
43
+ # All three keys default to the values shown above. The class name `Rigor::Plugin::Activerecord` (single
44
+ # capital R) is intentional — keeps the constant lookup distinct from `::ActiveRecord` even though the
45
+ # gem name is hyphenated.
55
46
  #
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.
47
+ # Note: this plugin is the seventh worked example. It does NOT require `active_record` at runtime — it
48
+ # only reads project source, the same way the other examples do. Rigor stays decoupled from Rails.
60
49
  class Activerecord < Rigor::Plugin::Base
61
50
  manifest(
62
51
  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.
52
+ # Bumped 2026-05-28 — implicit-self class-side AR call resolution: `select(:uri).group(:uri)` inside
53
+ # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
54
+ # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
55
+ # `:select` added to the relation-entry-point list.
70
56
  version: "0.5.0",
71
57
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
72
58
  config_schema: {
@@ -75,37 +61,30 @@ module Rigor
75
61
  "model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
76
62
  },
77
63
  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.
64
+ # ADR-25 — the bundled `ActiveRecord::Relation` RBS that relation-typed call sites (`has_many`
65
+ # accessors, `Model.where`, scopes) dispatch against.
81
66
  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.
67
+ # ADR-26 — `ActiveRecord::Relation` is an "open" receiver: it delegates an unbounded set of
68
+ # user-defined scopes / class methods to its model, so `call.undefined-method` must not fire for
69
+ # it. `CheckRules` reads this manifest field and skips the rule for the class.
87
70
  open_receivers: ["ActiveRecord::Relation"]
88
71
  )
89
72
 
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).
73
+ # The class the bundled `sig/active_record/relation.rbs` describes; `dynamic_return` contributes
74
+ # `ActiveRecord::Relation[Model]` for relation-returning call sites (`has_many` accessors,
75
+ # `Model.where`, scopes).
94
76
  RELATION_CLASS_NAME = "ActiveRecord::Relation"
95
77
 
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}.
78
+ # Cached: parsed schema table. The producer reads `@schema_file` via `io_boundary.read_file` so the
79
+ # descriptor picks up the digest, then parses through {SchemaParser}.
99
80
  producer :schema_table do |_params|
100
81
  contents = io_boundary.read_file(@schema_file)
101
82
  SchemaParser.parse(contents)
102
83
  end
103
84
 
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.
85
+ # Cached: model index. Walks every model file, then composes the rows with the cached schema table.
86
+ # `watch:` (ADR-60 WD3) covers model-file additions; the discoverer's in-block reads are captured
87
+ # into the record-and-validate dependency descriptor after the block runs.
109
88
  producer :model_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
110
89
  rows = ModelDiscoverer.new(
111
90
  io_boundary: io_boundary,
@@ -125,15 +104,11 @@ module Rigor
125
104
  @load_errors = []
126
105
  end
127
106
 
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.
107
+ # ADR-9 cross-plugin publication. Builds the model index eagerly during the per-run
108
+ # `prepare(services)` pass and publishes a flat Hash form to the shared fact store so downstream
109
+ # Tier-2 consumers (rigor-actionpack Phase 1 strong-parameter validation, rigor-factorybot Phase 1
110
+ # (c) attribute column cross-check, future plugins that need to know "what columns does class
111
+ # `User` expose?") can read it without coupling to this plugin's carrier classes.
137
112
  #
138
113
  # The published shape:
139
114
  #
@@ -143,11 +118,9 @@ module Rigor
143
118
  # ...
144
119
  # }
145
120
  #
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).
121
+ # Consumers do `services.fact_store.read(plugin_id: "activerecord", name: :model_index)` and look up
122
+ # by class name. Discovery failures (missing schema, unparseable models) leave the fact unpublished —
123
+ # the consumer's own degrade path runs (typically a no-op).
151
124
  def prepare(services)
152
125
  index = model_index
153
126
  return if index.nil? || index.empty?
@@ -162,12 +135,10 @@ module Rigor
162
135
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
163
136
  index = model_index
164
137
  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.
138
+ # Project-global error (missing `db/schema.rb`, parse failure, etc.) — emit once per run rather
139
+ # than once per analyzed file. On a Redmine-shape project that uses migrations only (no
140
+ # `schema.rb`), the old path produced 346 identical load-errors; on a Solidus monorepo (no
141
+ # top-level `schema.rb`), 999.
171
142
  return [] if @load_errors_emitted
172
143
 
173
144
  @load_errors_emitted = true
@@ -179,15 +150,11 @@ module Rigor
179
150
  Analyzer.new(path: path, model_index: index).analyze(root).diagnostics
180
151
  end
181
152
 
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.
153
+ # Rails migration files (`db/migrate/<timestamp>_*.rb`) and post-migration files
154
+ # (`db/post_migrate/`) reference the EVOLVING schema at the time the migration was written —
155
+ # `User.where(admin: ...)` is valid when the migration ran on a schema that still had the `admin`
156
+ # column, even though the current `db/schema.rb` no longer carries it. Validating these files
157
+ # against the CURRENT schema is a category error; the column diagnostics MUST stay silent.
191
158
  MIGRATION_PATH_PATTERNS = [
192
159
  %r{(\A|/)db/migrate/},
193
160
  %r{(\A|/)db/post_migrate/}
@@ -201,48 +168,36 @@ module Rigor
201
168
  MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
202
169
  end
203
170
 
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).
171
+ # The class-side finder / relation entry-point names `finder_return_type` recognises. Static half of
172
+ # the `dynamic_return` name gate; the run-time half comes from the model index (scopes,
173
+ # associations, columns).
208
174
  FINDER_METHOD_NAMES = %i[find find_by! find_by where all order limit none select].freeze
209
175
  private_constant :FINDER_METHOD_NAMES
210
176
 
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.
177
+ # Return-type contribution via the run-time `methods:` name gate (ADR-52 slice 5b). `Model.find(id)`
178
+ # narrows the call site's return type to `Nominal[Model]`, so chained calls (`User.find(1).name`)
179
+ # resolve through the analyzer's normal dispatch instead of the RBS-level untyped fall-back; scopes,
180
+ # association accessors, and column readers narrow per the paths below.
218
181
  #
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.
182
+ # WHY a method-name gate and not `receivers:` — the ADR-52 "rigor-activerecord blocker": a project
183
+ # model not in RBS types its constant as `Dynamic[top]`, so a receiver-type gate declines exactly the
184
+ # calls this plugin exists for. A *name* gate never reads the receiver type; the block keeps the
185
+ # plugin's own AST-constant / `self_type` / `type_of` resolution, so the Dynamic-constant case still
186
+ # reaches it (the same shape as rigor-sorbet's catalog path). The set — the static finder names ∪
187
+ # every scope, association, and column name (plus `column?` predicate forms) the model index
188
+ # discovered is exactly the union of names the four resolution paths below can return a type for,
189
+ # so gating on it is byte-identical to the old ungated hook. It is broad (`name`, `id`, …), but
190
+ # membership is one Set probe and the expensive block runs only on candidate hits.
234
191
  dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
235
192
  contribution_return_type(call_node, scope)
236
193
  end
237
194
 
238
195
  private
239
196
 
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.
197
+ # The run-time name gate: finders ∪ scopes ∪ associations ∪ column readers (+ `?` predicates).
198
+ # Resolved lazily on first dispatch (after `#prepare` built the index), memoised by the engine.
199
+ # Returns [] when discovery found nothing — the gate then declines every call, matching the old
200
+ # hook's `index.nil? || index.empty?` early return.
246
201
  def recognised_method_names
247
202
  index = model_index
248
203
  return [] if index.nil? || index.empty?
@@ -259,8 +214,8 @@ module Rigor
259
214
  names
260
215
  end
261
216
 
262
- # Resolution body for `dynamic_return` — same four-path
263
- # order, returning the bare type the contract expects.
217
+ # Resolution body for `dynamic_return` — same four-path order, returning the bare type the contract
218
+ # expects.
264
219
  def contribution_return_type(call_node, scope)
265
220
  return nil unless call_node.is_a?(Prism::CallNode)
266
221
 
@@ -287,24 +242,19 @@ module Rigor
287
242
  class_scope_return_type(call_node, entry)
288
243
  end
289
244
 
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.
245
+ # Implicit-self class-side call: `select(:uri)` / `where(active: true)` inside a `def
246
+ # self.<method>` body, a class body, or a scope lambda body (`scope :x, -> { ... }`). The
247
+ # surrounding `self_type` is `Singleton[Model]` in all three cases, so the same finder / scope /
248
+ # relation entry-point resolution that handles `Model.where(...)` applies.
296
249
  #
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
250
+ # Without this, `select(:uri)` inside a class body falls through to RBS dispatch on
251
+ # `Singleton[Account]`, which finds `Kernel#select` (the IO multiplexer) at `core/kernel.rbs` — its
252
+ # `Array[String]` return masks the AR class-side `select`'s relation return type, so the canonical
253
+ # scope-body idiom
303
254
  #
304
255
  # scope :duplicate_uris, -> { select(:uri).group(:uri) }
305
256
  #
306
- # types `select(:uri)` as `Array[String]` and the chained
307
- # `.group` as `undefined-method`.
257
+ # types `select(:uri)` as `Array[String]` and the chained `.group` as `undefined-method`.
308
258
  def implicit_self_class_call_return_type(call_node, scope, index)
309
259
  return nil if scope.nil?
310
260
 
@@ -318,12 +268,10 @@ module Rigor
318
268
  class_scope_return_type(call_node, entry)
319
269
  end
320
270
 
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.
271
+ # Class-side finders + the class-side relation entry points. `find` / `find_by!` return the model;
272
+ # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
273
+ # relation then carries its element type through any further chained query method via the bundled
274
+ # `ActiveRecord::Relation` RBS.
327
275
  def finder_return_type(call_node, entry)
328
276
  case call_node.name
329
277
  when :find
@@ -331,8 +279,8 @@ module Rigor
331
279
 
332
280
  Rigor::Type::Combinator.nominal_of(entry.class_name)
333
281
  when :find_by!
334
- # The bang variant raises `RecordNotFound` instead of
335
- # returning `nil`, so the result is non-nullable.
282
+ # The bang variant raises `RecordNotFound` instead of returning `nil`, so the result is
283
+ # non-nullable.
336
284
  Rigor::Type::Combinator.nominal_of(entry.class_name)
337
285
  when :find_by
338
286
  Rigor::Type::Combinator.union(
@@ -341,31 +289,26 @@ module Rigor
341
289
  )
342
290
  when :where, :all, :order, :limit, :none, :select
343
291
  # `: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.
292
+ # `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }` shape: the implicit-self
293
+ # `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select` (IO
294
+ # multiplexer, return `Array[String]`), masking the AR class-side relation entry point. The rest
295
+ # of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a relation is
296
+ # open.
351
297
  relation_of(entry.class_name)
352
298
  end
353
299
  end
354
300
 
355
- # `Post.published` / `Post.recent(5)` — a user-declared
356
- # `scope` returns a relation of the model regardless of the
357
- # arguments it takes.
301
+ # `Post.published` / `Post.recent(5)` — a user-declared `scope` returns a relation of the model
302
+ # regardless of the arguments it takes.
358
303
  def class_scope_return_type(call_node, entry)
359
304
  return nil unless entry.scope?(call_node.name)
360
305
 
361
306
  relation_of(entry.class_name)
362
307
  end
363
308
 
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).
309
+ # `ActiveRecord::Relation[Model]` — the type the bundled `sig/active_record/relation.rbs` describes.
310
+ # The class is declared `open_receivers` in the manifest, so a chained scope call the bundled RBS
311
+ # cannot enumerate does not surface as `call.undefined-method` (ADR-26).
369
312
  def relation_of(model_class_name)
370
313
  Rigor::Type::Combinator.nominal_of(
371
314
  RELATION_CLASS_NAME,
@@ -373,24 +316,17 @@ module Rigor
373
316
  )
374
317
  end
375
318
 
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.
319
+ # A scope invoked on an already-typed relation (`User.where(active: true).published`) keeps the
320
+ # relation type through the chain. The bundled `ActiveRecord::Relation` RBS cannot enumerate
321
+ # user-defined scopes, so without this the chain would lose its element type after the first scope
322
+ # call. Non-scope methods decline — the RBS tier resolves `where` / `order` / `each` / `first`
323
+ # precisely. Scopes may take arguments (`relation.recent(5)`), so unlike `instance_call_return_type`
324
+ # argument calls are not skipped.
386
325
  #
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.
326
+ # The cheap `scope_name?` pre-check is load-bearing: it gates the `scope.type_of(receiver)` call so
327
+ # the receiver type is computed ONLY when the method name could be a scope. `type_of` on a call
328
+ # receiver re-enters dispatch, and calling it for every call node in a long method chain is
329
+ # pathologically expensive the pre-check keeps the cost off the hot path.
394
330
  def relation_call_return_type(call_node, scope, index)
395
331
  return nil if call_node.receiver.nil?
396
332
  return nil unless scope_name?(call_node.name, index)
@@ -405,16 +341,15 @@ module Rigor
405
341
  relation_of(model_name)
406
342
  end
407
343
 
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).
344
+ # Whether `name` is a declared `scope` on ANY model in the index. A run-lifetime memoised Set so the
345
+ # per-call check in `relation_call_return_type` stays O(1).
411
346
  def scope_name?(name, index)
412
347
  @all_scope_names ||= index.entries.each_value.flat_map(&:scopes).to_set
413
348
  @all_scope_names.include?(name.to_s)
414
349
  end
415
350
 
416
- # When `type` is `ActiveRecord::Relation[Nominal[Model]]`,
417
- # returns the model class name; nil for any other type.
351
+ # When `type` is `ActiveRecord::Relation[Nominal[Model]]`, returns the model class name; nil for
352
+ # any other type.
418
353
  def relation_element_class_name(type)
419
354
  return nil unless type.is_a?(Rigor::Type::Nominal)
420
355
  return nil unless type.class_name == RELATION_CLASS_NAME
@@ -423,13 +358,10 @@ module Rigor
423
358
  element.class_name if element.is_a?(Rigor::Type::Nominal)
424
359
  end
425
360
 
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.
361
+ # Instance-side navigation: when the call's receiver resolves to `Nominal[Model]` and the method name
362
+ # matches a discovered association OR a table column, the call site gets a precise return type.
363
+ # Calls with arguments are skipped — accessor / association calls take no args, and argument forms
364
+ # (`user.posts(limit: 10)`, `user.name = x`) route through Rails APIs this slice does not model.
433
365
  def instance_call_return_type(call_node, scope, index)
434
366
  return nil unless call_node.arguments.nil?
435
367
 
@@ -444,16 +376,12 @@ module Rigor
444
376
  column_return_type(entry, call_node.name)
445
377
  end
446
378
 
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.
379
+ # The return type for an association accessor. A `belongs_to` / `has_one` singular association
380
+ # narrows to the target model — `belongs_to` is required (non-`nil`) by default since Rails 5 so it
381
+ # is `Nominal[Target]`, while `has_one` (and an `optional: true` / `required: false` `belongs_to`)
382
+ # adds the `nil` arm. A `has_many` / `has_and_belongs_to_many` collection narrows to
383
+ # `ActiveRecord::Relation[Target]` so chained query / iteration calls resolve. A polymorphic
384
+ # association has no single static target and declines rather than inventing a wrong type.
457
385
  def association_return_type(entry, method_name)
458
386
  association = entry.association(method_name)
459
387
  return nil if association.nil?
@@ -470,19 +398,14 @@ module Rigor
470
398
  end
471
399
  end
472
400
 
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`.
401
+ # Instance-side column access. `user.name` on a `Nominal[User]` receiver narrows to the column's
402
+ # value type; `user.name?` (the ActiveRecord-generated predicate) narrows to `bool`.
477
403
  #
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.
404
+ # The contributed type is deliberately NON-nullable even though the DB column may permit `NULL`:
405
+ # Rails code calls column accessors directly (`user.email.downcase`) as a matter of course, and
406
+ # contributing `T | nil` would light up that idiom with `possible-nil-receiver` across an entire
407
+ # codebase. Under-reporting a nil column is a false negative; over-reporting it is a false positive
408
+ # and the project ranks the latter as the worse failure.
486
409
  def column_return_type(entry, method_name)
487
410
  name = method_name.to_s
488
411
  predicate = name.end_with?("?")
@@ -498,12 +421,10 @@ module Rigor
498
421
  column.array? ? Rigor::Type::Combinator.nominal_of("Array", type_args: [inner]) : inner
499
422
  end
500
423
 
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.
424
+ # Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb /
425
+ # unrecognised column types) declines — `Nominal[Object]` would be NARROWER than the RBS-erased
426
+ # envelope and could surface false `call.undefined-method` on a value whose real shape the plugin
427
+ # cannot model.
507
428
  def ruby_type_to_type(ruby_type)
508
429
  case ruby_type
509
430
  when "bool" then bool_type
@@ -546,12 +467,9 @@ module Rigor
546
467
  node.arguments.arguments.size
547
468
  end
548
469
 
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.
470
+ # Marshal-clean Hash form for the cross-plugin fact store. Consumers (rigor-actionpack Phase 1,
471
+ # rigor-factorybot Phase 1 (c), ...) get a flat `class_name → { table:, columns: }` map without
472
+ # depending on this plugin's `ModelIndex` / `SchemaTable::Column` carrier classes.
555
473
  def index_to_published_hash(index)
556
474
  index.entries.transform_values do |entry|
557
475
  {
@@ -573,11 +491,9 @@ module Rigor
573
491
  table = schema_table_or_nil
574
492
  return nil if table.nil?
575
493
 
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).
494
+ # ADR-60 WD3 record-and-validate: the producer's own in-block `ModelDiscoverer` reads are
495
+ # captured into the dependency descriptor after the block runs, and the producer's `watch:` covers
496
+ # model-file additions so no priming walk is needed (it used to run the discover twice).
581
497
  @model_index = cache_for(:model_index, params: {}).call
582
498
  rescue StandardError => e
583
499
  @load_errors << "model index build failed: #{e.class}: #{e.message}"
@@ -586,20 +502,17 @@ module Rigor
586
502
 
587
503
  def schema_table_or_nil
588
504
  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.
505
+ # Memoize the *failure*, not just the success: `model_index` (and thus this method) is invoked per
506
+ # AR call site, so a missing / unreadable schema file would otherwise re-attempt the read and
507
+ # append a fresh interpolated error string to `@load_errors` on every call. On a large Rails app
508
+ # that grew `@load_errors` to millions of retained strings (measured: 4.2 M strings / ~1.5 GB on
509
+ # Redmine). One attempt is enough.
596
510
  return nil if @schema_load_attempted
597
511
 
598
512
  @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.
513
+ # ADR-60 WD3 record-and-validate: the producer reads `@schema_file` in-block, and that read is
514
+ # captured into the dependency descriptor after the block runs so no priming read is needed
515
+ # here.
603
516
  @schema_table = cache_for(:schema_table, params: {}).call
604
517
  rescue Plugin::AccessDeniedError => e
605
518
  @load_errors << "rigor-activerecord: #{e.message}"
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Gem entry point. Required by Rigor's plugin loader when
4
- # `.rigor.yml` lists `rigor-activerecord` under `plugins:`. The
5
- # loader expects this `require` to side-effect a call to
6
- # `Rigor::Plugin.register`, which the body of
7
- # `lib/rigor/plugin/activerecord.rb` performs at load time.
3
+ # Gem entry point. Required by Rigor's plugin loader when `.rigor.yml` lists `rigor-activerecord` under
4
+ # `plugins:`. The loader expects this `require` to side-effect a call to `Rigor::Plugin.register`, which
5
+ # the body of `lib/rigor/plugin/activerecord.rb` performs at load time.
8
6
  require_relative "rigor/plugin/activerecord"