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