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
@@ -8,27 +8,21 @@ require_relative "helper_table"
8
8
  module Rigor
9
9
  module Plugin
10
10
  class RailsRoutes < Rigor::Plugin::Base
11
- # Statically interprets `config/routes.rb`'s DSL via
12
- # Prism never executes the file. The interpreter is
13
- # deliberately narrow; it covers the subset documented
14
- # in the plugin's README and degrades silently on
15
- # constructs it doesn't recognise.
11
+ # Statically interprets `config/routes.rb`'s DSL via Prism — never executes the file. The interpreter
12
+ # is deliberately narrow; it covers the subset documented in the plugin's README and degrades
13
+ # silently on constructs it doesn't recognise.
16
14
  #
17
- # Recognised DSL surface (per the Rails-plugins
18
- # roadmap):
15
+ # Recognised DSL surface (per the Rails-plugins roadmap):
19
16
  #
20
- # - `Rails.application.routes.draw do ... end` (entry
21
- # block; the body is interpreted)
17
+ # - `Rails.application.routes.draw do ... end` (entry block; the body is interpreted)
22
18
  # - `resources :name [, only: [...] | except: [...]]`
23
19
  # - `resource :name`
24
20
  # - `get/post/patch/put/delete "path", to:, as:`
25
21
  # - `root to: "..."` / `root "..."`
26
22
  # - `scope "path", as: :name do ... end` (with `as:` key)
27
23
  # - One level of `namespace :foo do ... end`
28
- # - One level of nested `resources` (`resources :users
29
- # do; resources :posts; end`)
30
- # - `member do ... end` / `collection do ... end`
31
- # inside `resources`
24
+ # - One level of nested `resources` (`resources :users do; resources :posts; end`)
25
+ # - `member do ... end` / `collection do ... end` inside `resources`
32
26
  #
33
27
  # Out of scope for v0.1.0 (silent skips):
34
28
  #
@@ -40,12 +34,10 @@ module Rigor
40
34
  module RoutesParser
41
35
  # Standard resource actions Rails generates by default.
42
36
  DEFAULT_RESOURCE_ACTIONS = %i[index show new create edit update destroy].freeze
43
- # Default actions for `resource` (singular) — no index,
44
- # no `:id` segment.
37
+ # Default actions for `resource` (singular) — no index, no `:id` segment.
45
38
  DEFAULT_SINGULAR_ACTIONS = %i[show new create edit update destroy].freeze
46
39
 
47
- # Helper-name conventions per action. `:show` and
48
- # `:update` / `:destroy` share the singular-form
40
+ # Helper-name conventions per action. `:show` and `:update` / `:destroy` share the singular-form
49
41
  # helper (Rails dedupes).
50
42
  ACTION_HTTP_METHODS = {
51
43
  index: :get,
@@ -71,10 +63,8 @@ module Rigor
71
63
  context = Context.new(file_reader: file_reader)
72
64
  interpret(parse_result.value, context)
73
65
 
74
- # Apply name-transform alias rules discovered during
75
- # the walk (the `direct(name.sub(X, Y)) do |...| send(
76
- # "#{name}_url", ...) end` GitLab pattern — see
77
- # `collect_alias_rules` below).
66
+ # Apply name-transform alias rules discovered during the walk (the `direct(name.sub(X, Y)) do
67
+ # |...| send("#{name}_url", ...) end` GitLab pattern — see `collect_alias_rules` below).
78
68
  apply_alias_rules(context)
79
69
 
80
70
  # Each helper has both `_path` and `_url` forms.
@@ -93,21 +83,15 @@ module Rigor
93
83
  HelperTable.new(paired, custom_helpers: custom_helpers, devise_resources: context.devise_resources)
94
84
  end
95
85
 
96
- # For every registered alias rule `(from_str, to_str,
97
- # arity_delta)`, find every existing entry whose name
98
- # contains `from_str` and register an aliased entry with
99
- # `from_str to_str` substitution. The pattern matches
100
- # GitLab's shorthand-helper idiom: a loop over registered
101
- # route names that invokes `direct(new_name) do |project,
102
- # *args| send("#{name}_url", project&.namespace,
103
- # project, *args) end` where `new_name = name.sub(FROM,
104
- # TO)`. The direct block extracts the namespace from
105
- # `project` and forwards the rest, so the alias's arity
106
- # is one LESS than the original helper's
107
- # (`namespace_project_blob_path(namespace_id, project_id,
108
- # blob_id)` → `project_blob_path(project, blob_id)`).
109
- # We register each alias under the original entry's
110
- # arity AND `original - 1` to span both interpretations.
86
+ # For every registered alias rule `(from_str, to_str, arity_delta)`, find every existing entry whose
87
+ # name contains `from_str` and register an aliased entry with `from_str → to_str` substitution. The
88
+ # pattern matches GitLab's shorthand-helper idiom: a loop over registered route names that invokes
89
+ # `direct(new_name) do |project, *args| send("#{name}_url", project&.namespace, project, *args) end`
90
+ # where `new_name = name.sub(FROM, TO)`. The direct block extracts the namespace from `project` and
91
+ # forwards the rest, so the alias's arity is one LESS than the original helper's
92
+ # (`namespace_project_blob_path(namespace_id, project_id, blob_id)` →
93
+ # `project_blob_path(project, blob_id)`). We register each alias under the original entry's arity
94
+ # AND `original - 1` to span both interpretations.
111
95
  def apply_alias_rules(context)
112
96
  return if context.alias_rules.empty?
113
97
 
@@ -119,10 +103,8 @@ module Rigor
119
103
  new_name = entry.name.sub(from, to)
120
104
  next if new_name == entry.name
121
105
 
122
- # Register at original arity AND at arity-1 so the
123
- # common GitLab `|project, *args|` pattern (which
124
- # collapses namespace_id + project_id into project)
125
- # passes the arity check.
106
+ # Register at original arity AND at arity-1 so the common GitLab `|project, *args|` pattern
107
+ # (which collapses namespace_id + project_id into project) passes the arity check.
126
108
  [entry.arity, [entry.arity - 1, 0].max].uniq.each do |arity|
127
109
  aliases << HelperTable::Entry.new(
128
110
  name: new_name, arity: arity,
@@ -135,9 +117,8 @@ module Rigor
135
117
  context.entries.concat(aliases)
136
118
  end
137
119
 
138
- # Per-parse mutable accumulator. Tracks the current
139
- # nesting prefix (namespaces + parent resource) and the
140
- # entries collected so far.
120
+ # Per-parse mutable accumulator. Tracks the current nesting prefix (namespaces + parent resource)
121
+ # and the entries collected so far.
141
122
  class Context
142
123
  attr_reader :entries, :file_reader, :devise_resources, :alias_rules
143
124
 
@@ -149,22 +130,15 @@ module Rigor
149
130
  # - `{ kind: :scope, parent: "user", arity_segments: [":user_id"] }`
150
131
  # - `{ kind: :as_scope, name: "event", path: "/:event_slug", arity: 1 }`
151
132
  @stack = []
152
- # Devise resource segments (singularised) declared
153
- # via `devise_for :resource`. Drives the
133
+ # Devise resource segments (singularised) declared via `devise_for :resource`. Drives the
154
134
  # OmniAuth-helper recognition in `HelperTable#omniauth_match?`.
155
135
  @devise_resources = []
156
- # Registered `concern :name do ... end` blocks.
157
- # Keyed by Symbol name; value is the block's body
158
- # node. `resources :foo, concerns: :name do ... end`
159
- # replays the body at the resource's site.
136
+ # Registered `concern :name do ... end` blocks. Keyed by Symbol name; value is the block's body
137
+ # node. `resources :foo, concerns: :name do ... end` replays the body at the resource's site.
160
138
  @concerns = {}
161
- # `(from_str, to_str)` pairs collected from
162
- # iterative `direct(name.sub(FROM, TO)) do ... end`
163
- # patterns. Applied after parsing via
164
- # `apply_alias_rules` to generate substituted-name
165
- # aliases for every matching entry — closes
166
- # GitLab's `namespace_project_*` → `project_*`
167
- # shorthand idiom.
139
+ # `(from_str, to_str)` pairs collected from iterative `direct(name.sub(FROM, TO)) do ... end`
140
+ # patterns. Applied after parsing via `apply_alias_rules` to generate substituted-name aliases
141
+ # for every matching entry — closes GitLab's `namespace_project_*` → `project_*` shorthand idiom.
168
142
  @alias_rules = []
169
143
  end
170
144
 
@@ -200,17 +174,12 @@ module Rigor
200
174
  @stack.pop
201
175
  end
202
176
 
203
- # `resource :foo do ... end` (SINGULAR resource) —
204
- # adds the resource name to the helper prefix and
205
- # path for nested declarations, but DOESN'T
206
- # contribute a dynamic `:id` segment (singular
207
- # resources have no `:id`). So `resource :instance
208
- # do; resources :domain_blocks; end` generates
209
- # `instance_domain_blocks_path` (arity 0), not
210
- # `instance_domain_blocks_path(:id)`.
177
+ # `resource :foo do ... end` (SINGULAR resource) — adds the resource name to the helper prefix
178
+ # and path for nested declarations, but DOESN'T contribute a dynamic `:id` segment (singular
179
+ # resources have no `:id`). So `resource :instance do; resources :domain_blocks; end` generates
180
+ # `instance_domain_blocks_path` (arity 0), not `instance_domain_blocks_path(:id)`.
211
181
  #
212
- # Helper-prefix segment uses the AS-GIVEN name (no
213
- # singularising — `resource :foo` keeps `foo`).
182
+ # Helper-prefix segment uses the AS-GIVEN name (no singularising — `resource :foo` keeps `foo`).
214
183
  def push_singular_resource(parent_name)
215
184
  name = parent_name.to_s
216
185
  @stack.push(kind: :singular_scope, parent: name, path_segment: "/#{name}")
@@ -219,14 +188,10 @@ module Rigor
219
188
  @stack.pop
220
189
  end
221
190
 
222
- # `member do ... end` / `collection do ... end` —
223
- # records the mode so subsequent shorthand HTTP-verb
224
- # calls (`post :memorialize`) inside the block can
225
- # derive their helper name from the enclosing
226
- # resource. The frame also carries the immediate
227
- # parent's singular / plural names so member /
228
- # collection actions can pick the correct form
229
- # (`memorialize_account_path(id)` vs
191
+ # `member do ... end` / `collection do ... end` — records the mode so subsequent shorthand
192
+ # HTTP-verb calls (`post :memorialize`) inside the block can derive their helper name from the
193
+ # enclosing resource. The frame also carries the immediate parent's singular / plural names so
194
+ # member / collection actions can pick the correct form (`memorialize_account_path(id)` vs
230
195
  # `memorialize_accounts_path`).
231
196
  def push_action_block(mode, parent_singular, parent_plural)
232
197
  @stack.push(kind: :"#{mode}_block", parent_singular: parent_singular, parent_plural: parent_plural)
@@ -235,11 +200,9 @@ module Rigor
235
200
  @stack.pop
236
201
  end
237
202
 
238
- # `with_options only: [:index], concerns: :batch do
239
- # ... end` every call inside the block inherits
240
- # these default options (each call's own options
241
- # override the defaults). `effective_options_for`
242
- # below merges them in caller-precedence order.
203
+ # `with_options only: [:index], concerns: :batch do ... end` — every call inside the block
204
+ # inherits these default options (each call's own options override the defaults).
205
+ # `effective_options_for` below merges them in caller-precedence order.
243
206
  def push_with_options(defaults)
244
207
  @stack.push(kind: :with_options, defaults: defaults)
245
208
  yield
@@ -247,15 +210,10 @@ module Rigor
247
210
  @stack.pop
248
211
  end
249
212
 
250
- # The merged default-options chain from every
251
- # enclosing `with_options` frame on the stack
252
- # (outer-most first; inner frames take precedence).
253
- # `handle_resources` / `handle_resource` merge this
254
- # with the node's own option hash so a bare
255
- # `resources :links` inside `with_options only:
256
- # [:index], concerns: :batch do ... end` is
257
- # treated as if it had those options written
258
- # inline.
213
+ # The merged default-options chain from every enclosing `with_options` frame on the stack
214
+ # (outer-most first; inner frames take precedence). `handle_resources` / `handle_resource` merge
215
+ # this with the node's own option hash so a bare `resources :links` inside `with_options only:
216
+ # [:index], concerns: :batch do ... end` is treated as if it had those options written inline.
259
217
  def with_options_defaults
260
218
  defaults = {}
261
219
  @stack.each do |frame|
@@ -266,10 +224,8 @@ module Rigor
266
224
  defaults
267
225
  end
268
226
 
269
- # Returns the top-most `:scope` frame's singular /
270
- # plural names, or `nil` when not inside a resources
271
- # / resource block. Used by `handle_member_or_collection`
272
- # to push the action-block frame.
227
+ # Returns the top-most `:scope` frame's singular / plural names, or `nil` when not inside a
228
+ # resources / resource block. Used by `handle_member_or_collection` to push the action-block frame.
273
229
  def innermost_resource
274
230
  scope_frame = @stack.rfind { |f| f[:kind] == :scope }
275
231
  return nil if scope_frame.nil?
@@ -277,17 +233,14 @@ module Rigor
277
233
  { singular: scope_frame[:parent], plural: scope_frame[:parent_plural] || scope_frame[:parent] }
278
234
  end
279
235
 
280
- # Top-most `:member_block` / `:collection_block`
281
- # frame, or nil when not in a shorthand action
282
- # context. `handle_explicit_route` uses this to
283
- # detect a `post :memorialize` symbol-only call shape.
236
+ # Top-most `:member_block` / `:collection_block` frame, or nil when not in a shorthand action
237
+ # context. `handle_explicit_route` uses this to detect a `post :memorialize` symbol-only call shape.
284
238
  def innermost_action_block
285
239
  @stack.rfind { |f| %i[member_block collection_block].include?(f[:kind]) }
286
240
  end
287
241
 
288
- # `scope "/:slug", as: "foo" do ... end` — adds a
289
- # helper-name prefix without the "parent resource" arity
290
- # arithmetic that push_resource uses.
242
+ # `scope "/:slug", as: "foo" do ... end` — adds a helper-name prefix without the "parent
243
+ # resource" arity arithmetic that push_resource uses.
291
244
  def push_as_scope(name, path, arity_count)
292
245
  @stack.push(kind: :as_scope, name: name.to_s, path: path, arity: arity_count)
293
246
  yield
@@ -295,20 +248,16 @@ module Rigor
295
248
  @stack.pop
296
249
  end
297
250
 
298
- # Helper-name prefix from namespaces (`admin_`,
299
- # `admin_users_`, …).
251
+ # Helper-name prefix from namespaces (`admin_`, `admin_users_`, …).
300
252
  def helper_prefix
301
253
  segments = @stack.filter_map { |frame| frame_helper_segment(frame) }
302
254
  segments.map { |segment| "#{segment}_" }.join
303
255
  end
304
256
 
305
- # The chain of resource-singular segments above the
306
- # current scope, in order used to form member-route
307
- # helper names of the form `<as>_<singular_chain>_path`.
308
- # Includes nested resources (`resources :projects do;
309
- # resources :issues do; get 'foo', as: 'bar'; end; end`
310
- # → `bar_project_issue_path`). Returns `""` outside any
311
- # `:scope` / `:singular_scope` frame.
257
+ # The chain of resource-singular segments above the current scope, in order — used to form
258
+ # member-route helper names of the form `<as>_<singular_chain>_path`. Includes nested resources
259
+ # (`resources :projects do; resources :issues do; get 'foo', as: 'bar'; end; end` →
260
+ # `bar_project_issue_path`). Returns `""` outside any `:scope` / `:singular_scope` frame.
312
261
  def resource_singular_chain
313
262
  segments = @stack.filter_map do |frame|
314
263
  case frame[:kind]
@@ -319,12 +268,10 @@ module Rigor
319
268
  segments.empty? ? "" : "#{segments.join('_')}_"
320
269
  end
321
270
 
322
- # Namespace-only prefix (drops the resource-singular
323
- # segments that `helper_prefix` includes). Used so that
324
- # an `:as => 'foo'` inside `namespace :admin do
325
- # resources :projects do; ...; end; end` generates
326
- # `admin_foo_project_path` — namespace first, then
327
- # `:as` action prefix, then singular chain.
271
+ # Namespace-only prefix (drops the resource-singular segments that `helper_prefix` includes).
272
+ # Used so that an `:as => 'foo'` inside `namespace :admin do resources :projects do; ...; end;
273
+ # end` generates `admin_foo_project_path` namespace first, then `:as` action prefix, then
274
+ # singular chain.
328
275
  def namespace_only_prefix
329
276
  segments = @stack.filter_map do |frame|
330
277
  case frame[:kind]
@@ -335,34 +282,28 @@ module Rigor
335
282
  segments.empty? ? "" : "#{segments.join('_')}_"
336
283
  end
337
284
 
338
- # True when we're inside a `resources` or `resource`
339
- # scope (the AST has pushed a `:scope` or
340
- # `:singular_scope` frame). Drives member-route
341
- # helper-name generation in `handle_explicit_route`.
285
+ # True when we're inside a `resources` or `resource` scope (the AST has pushed a `:scope` or
286
+ # `:singular_scope` frame). Drives member-route helper-name generation in `handle_explicit_route`.
342
287
  def inside_resource_scope?
343
288
  @stack.any? { |frame| %i[scope singular_scope].include?(frame[:kind]) }
344
289
  end
345
290
 
346
- # The innermost `:scope` frame (plural resource),
347
- # carrying `parent` (singular) and `parent_plural`.
348
- # Used by the inline `:on => :collection` / `:on =>
349
- # :member` form to derive the helper name.
291
+ # The innermost `:scope` frame (plural resource), carrying `parent` (singular) and
292
+ # `parent_plural`. Used by the inline `:on => :collection` / `:on => :member` form to derive the
293
+ # helper name.
350
294
  def innermost_scope_frame
351
295
  @stack.rfind { |f| f[:kind] == :scope }
352
296
  end
353
297
 
354
- # Path prefix — including the parent's `:user_id`
355
- # segments for nested resources and the namespace
356
- # path prefix.
298
+ # Path prefix — including the parent's `:user_id` segments for nested resources and the
299
+ # namespace path prefix.
357
300
  def path_prefix
358
301
  parts = @stack.flat_map { |frame| frame_path_segments(frame) }
359
302
  parts.join
360
303
  end
361
304
 
362
- # Number of dynamic segments (`:user_id`-style)
363
- # captured by the parent scope chain. Used to
364
- # compute helper arity for nested resources.
365
- # Each nested-resource `:scope` frame contributes 1;
305
+ # Number of dynamic segments (`:user_id`-style) captured by the parent scope chain. Used to
306
+ # compute helper arity for nested resources. Each nested-resource `:scope` frame contributes 1;
366
307
  # each `:as_scope` frame contributes its own arity.
367
308
  def parent_segment_count
368
309
  @stack.sum do |frame|
@@ -381,10 +322,8 @@ module Rigor
381
322
  when :namespace then frame[:name]
382
323
  when :scope then frame[:parent]
383
324
  when :as_scope
384
- # An `:as_scope` frame with an empty `:name` is a
385
- # path-only frame pushed by `scope(path: 'X')`
386
- # without `:as` — contributes path/arity but no
387
- # helper-prefix segment.
325
+ # An `:as_scope` frame with an empty `:name` is a path-only frame pushed by `scope(path: 'X')`
326
+ # without `:as` contributes path/arity but no helper-prefix segment.
388
327
  frame[:name].to_s.empty? ? nil : frame[:name]
389
328
  when :singular_scope then frame[:parent]
390
329
  end
@@ -394,12 +333,9 @@ module Rigor
394
333
  case frame[:kind]
395
334
  when :namespace then ["/#{frame[:name]}"]
396
335
  when :scope
397
- # Use the as-given plural when available
398
- # (`parent_plural` was captured at
399
- # push_resource time) the singularize /
400
- # pluralize round-trip is lossy for irregular
401
- # forms (`media → medium → medias`) but the
402
- # original name is always correct.
336
+ # Use the as-given plural when available (`parent_plural` was captured at push_resource
337
+ # time) — the singularize / pluralize round-trip is lossy for irregular forms (`media
338
+ # medium → medias`) but the original name is always correct.
403
339
  plural = frame[:parent_plural] || pluralize(frame[:parent])
404
340
  ["/#{plural}/:#{frame[:parent]}_id"]
405
341
  when :as_scope then frame[:path] ? [frame[:path]] : []
@@ -408,18 +344,14 @@ module Rigor
408
344
  end
409
345
  end
410
346
 
411
- # ADR-39: inflection delegates to the shared
412
- # `Rigor::Plugin::Inflector`, which calls the real
413
- # `ActiveSupport::Inflector` (the authority Rails itself uses to
414
- # generate helper names) when available, falling back to a
415
- # built-in approximation otherwise. This replaces the hand-tuned
416
- # AS-replica these methods used to carry — every special case
417
- # they accreted (`news` uncountable, `media`→`medium`,
418
- # `databases`→`database`, `custom_css` ss-preservation,
419
- # `async_refresh`→`async_refreshes`) was reverse-engineered
420
- # `ActiveSupport::Inflector` behaviour, so the real inflector
421
- # produces the same result for those and the correct one for the
422
- # words the replica never covered.
347
+ # ADR-39: inflection delegates to the shared `Rigor::Plugin::Inflector`, which calls the real
348
+ # `ActiveSupport::Inflector` (the authority Rails itself uses to generate helper names) when
349
+ # available, falling back to a built-in approximation otherwise. This replaces the hand-tuned
350
+ # AS-replica these methods used to carry every special case they accreted (`news` uncountable,
351
+ # `media`→`medium`, `databases`→`database`, `custom_css` ss-preservation,
352
+ # `async_refresh`→`async_refreshes`) was reverse-engineered `ActiveSupport::Inflector` behaviour,
353
+ # so the real inflector produces the same result for those and the correct one for the words the
354
+ # replica never covered.
423
355
  def singularize(word)
424
356
  Rigor::Plugin::Inflector.singularize(word)
425
357
  end
@@ -447,21 +379,15 @@ module Rigor
447
379
  # `Rails.application.routes.draw do ... end`
448
380
  interpret_block_body(node, context)
449
381
  else
450
- # `draw(:admin)` — routing partial at
451
- # config/routes/{name}.rb.
382
+ # `draw(:admin)` — routing partial at config/routes/{name}.rb.
452
383
  load_drawn_routes(node, context)
453
384
  end
454
385
  when :draw_all
455
- # `draw_all :user` — from the
456
- # `action_dispatch-draw_all` gem (used by GitLab
457
- # FOSS). Same single-file load semantics as `draw`
458
- # the gem just allows multiple route-dir search
459
- # paths (we look in the one we know about).
460
- # GitLab's `draw_all :user` loads
461
- # `config/routes/user.rb` containing `devise_for
462
- # :users, controllers: ...` plus the broader user
463
- # route catalogue; without this every Devise
464
- # session helper reads as `unknown-helper`.
386
+ # `draw_all :user` — from the `action_dispatch-draw_all` gem (used by GitLab FOSS). Same
387
+ # single-file load semantics as `draw` — the gem just allows multiple route-dir search paths
388
+ # (we look in the one we know about). GitLab's `draw_all :user` loads
389
+ # `config/routes/user.rb` containing `devise_for :users, controllers: ...` plus the broader
390
+ # user route catalogue; without this every Devise session helper reads as `unknown-helper`.
465
391
  load_drawn_routes(node, context)
466
392
  when :namespace
467
393
  handle_namespace(node, context)
@@ -474,21 +400,15 @@ module Rigor
474
400
  when :scope
475
401
  handle_scope(node, context)
476
402
  when :get, :post, :patch, :put, :delete, :match
477
- # `match 'login', :to => 'a#b', :as => :signin, :via => [:get, :post]`
478
- # generates the same helper-name shape as `get` /
479
- # `post` (first arg = path string, `:as` overrides the
480
- # derived helper name). The only difference is the
481
- # HTTP-method set (driven by `:via`); the helper-name
482
- # generation logic is identical, so reuse the same
483
- # handler. Redmine relies heavily on `match` for
484
- # multi-method endpoints (`account/lost_password`,
485
- # `news/preview`, etc.).
403
+ # `match 'login', :to => 'a#b', :as => :signin, :via => [:get, :post]` generates the same
404
+ # helper-name shape as `get` / `post` (first arg = path string, `:as` overrides the derived
405
+ # helper name). The only difference is the HTTP-method set (driven by `:via`); the helper-name
406
+ # generation logic is identical, so reuse the same handler. Redmine relies heavily on `match`
407
+ # for multi-method endpoints (`account/lost_password`, `news/preview`, etc.).
486
408
  handle_explicit_route(node, context)
487
409
  when :member, :collection
488
- # Inside a `resources` block, `member do ... end`
489
- # / `collection do ... end` introduces extra
490
- # routes. Interpreted only when we have a parent
491
- # scope (otherwise the call is meaningless).
410
+ # Inside a `resources` block, `member do ... end` / `collection do ... end` introduces extra
411
+ # routes. Interpreted only when we have a parent scope (otherwise the call is meaningless).
492
412
  handle_member_or_collection(node, context)
493
413
  when :devise_for
494
414
  handle_devise_for(node, context)
@@ -501,19 +421,14 @@ module Rigor
501
421
  when :concern
502
422
  handle_concern_definition(node, context)
503
423
  when :direct
504
- # `direct(:name) do |args...| ... end` — Rails DSL
505
- # for a custom URL helper. We register `name_path`
506
- # and (auto-paired) `name_url`. The arity comes
507
- # from the block's required-parameter count. Only
508
- # literal Symbol/String names are handled here;
509
- # non-literal forms are caught by the
424
+ # `direct(:name) do |args...| ... end` — Rails DSL for a custom URL helper. We register
425
+ # `name_path` and (auto-paired) `name_url`. The arity comes from the block's required-parameter
426
+ # count. Only literal Symbol/String names are handled here; non-literal forms are caught by the
510
427
  # `detect_alias_rule_in_each` walker upstream.
511
428
  handle_direct(node, context)
512
429
  when :each
513
- # `.each do |name| ... end` — scan the block for
514
- # the GitLab `direct(name.sub(X, Y)) do ... end`
515
- # alias-generation idiom. If found, record the
516
- # (X, Y) pair so `apply_alias_rules` can expand
430
+ # `.each do |name| ... end` — scan the block for the GitLab `direct(name.sub(X, Y)) do ... end`
431
+ # alias-generation idiom. If found, record the (X, Y) pair so `apply_alias_rules` can expand
517
432
  # every matching registered entry.
518
433
  detect_alias_rule_in_each(node, context)
519
434
  interpret_block_body(node, context)
@@ -522,11 +437,9 @@ module Rigor
522
437
  end
523
438
  end
524
439
 
525
- # `direct(:name) do |arg1, arg2, ...| ... end` — Rails
526
- # adds a custom URL helper. We register `name_path`
527
- # with the block's required-arg count as the arity (the
528
- # auto-pairing in `parse` adds `name_url`). Only handled
529
- # for literal Symbol or String first-arg.
440
+ # `direct(:name) do |arg1, arg2, ...| ... end` — Rails adds a custom URL helper. We register
441
+ # `name_path` with the block's required-arg count as the arity (the auto-pairing in `parse` adds
442
+ # `name_url`). Only handled for literal Symbol or String first-arg.
530
443
  def handle_direct(node, context)
531
444
  name = Rigor::Source::Literals.symbol_or_string_name(node.arguments&.arguments&.first)
532
445
  return if name.nil? || name.empty?
@@ -556,13 +469,10 @@ module Rigor
556
469
  # Walks a `.each do |loop_var| ... end` body looking for:
557
470
  # - `<new_name_var> = <loop_var>.sub(FROM_STR, TO_STR)`
558
471
  # - `direct(<new_name_var>) do |...| ... end`
559
- # When both shapes appear with the same `<new_name_var>`,
560
- # registers `(FROM_STR, TO_STR)` as an alias rule. The
561
- # iterated collection (`Rails.application.routes.set`)
562
- # is assumed to yield the names of already-registered
563
- # helpers — GitLab's idiom. Other iterations that happen
564
- # to match the shape are rare; the FP risk is bounded
565
- # because alias generation only adds entries.
472
+ # When both shapes appear with the same `<new_name_var>`, registers `(FROM_STR, TO_STR)` as an
473
+ # alias rule. The iterated collection (`Rails.application.routes.set`) is assumed to yield the
474
+ # names of already-registered helpers — GitLab's idiom. Other iterations that happen to match the
475
+ # shape are rare; the FP risk is bounded because alias generation only adds entries.
566
476
  def detect_alias_rule_in_each(each_node, context)
567
477
  block = each_node.block
568
478
  return unless block.is_a?(Prism::BlockNode)
@@ -573,8 +483,7 @@ module Rigor
573
483
  body = block.body
574
484
  return if body.nil?
575
485
 
576
- # First pass: gather every `<var> = <loop_var>.sub(FROM, TO)`
577
- # assignment.
486
+ # First pass: gather every `<var> = <loop_var>.sub(FROM, TO)` assignment.
578
487
  sub_assignments = {}
579
488
  walk_for_alias_pattern(body) do |node|
580
489
  next unless node.is_a?(Prism::LocalVariableWriteNode)
@@ -596,9 +505,8 @@ module Rigor
596
505
 
597
506
  return if sub_assignments.empty?
598
507
 
599
- # Second pass: find `direct(<var>) do ... end` calls
600
- # whose first arg is one of the captured assignment
601
- # variables.
508
+ # Second pass: find `direct(<var>) do ... end` calls whose first arg is one of the captured
509
+ # assignment variables.
602
510
  walk_for_alias_pattern(body) do |node|
603
511
  next unless node.is_a?(Prism::CallNode) && node.name == :direct
604
512
  next if node.arguments.nil?
@@ -644,14 +552,11 @@ module Rigor
644
552
  context.push_namespace(name) { interpret_block_body(node, context) }
645
553
  end
646
554
 
647
- # `devise_for :users [, skip: [...], path: "..."]` —
648
- # generates the standard Devise route-helper catalogue
649
- # for the named resource. Symbol-literal first arg
650
- # only; non-literal forms (e.g. dynamic resource names
651
- # built from constants) are silently skipped because the
652
- # helper SET depends on the literal name and we cannot
653
- # statically resolve a variable here. `skip:` is read
654
- # so the project's omitted controllers do not register.
555
+ # `devise_for :users [, skip: [...], path: "..."]` — generates the standard Devise route-helper
556
+ # catalogue for the named resource. Symbol-literal first arg only; non-literal forms (e.g. dynamic
557
+ # resource names built from constants) are silently skipped because the helper SET depends on the
558
+ # literal name and we cannot statically resolve a variable here. `skip:` is read so the project's
559
+ # omitted controllers do not register.
655
560
  def handle_devise_for(node, context)
656
561
  resource = symbol_argument(node, 0)
657
562
  return if resource.nil?
@@ -664,18 +569,13 @@ module Rigor
664
569
  end
665
570
  end
666
571
 
667
- # `with_options X do ... end` — applies the `X`
668
- # options hash as defaults for every call inside the
669
- # block. Mastodon's `with_options only: [:index],
670
- # concerns: :batch do resources :links; resources
671
- # :tags; end` lets us register the inner resources
672
- # with the implicit defaults — closing `batch_*_path`
673
- # cluster generated via the `:batch` concern.
572
+ # `with_options X do ... end` — applies the `X` options hash as defaults for every call inside the
573
+ # block. Mastodon's `with_options only: [:index], concerns: :batch do resources :links; resources
574
+ # :tags; end` lets us register the inner resources with the implicit defaults — closing
575
+ # `batch_*_path` cluster generated via the `:batch` concern.
674
576
  #
675
- # We push a `:with_options` frame carrying the
676
- # defaults; `effective_options_for(node)` (in
677
- # `handle_resources` / `handle_resource`) merges them
678
- # in before reading specific option keys.
577
+ # We push a `:with_options` frame carrying the defaults; `effective_options_for(node)` (in
578
+ # `handle_resources` / `handle_resource`) merges them in before reading specific option keys.
679
579
  def handle_with_options(node, context)
680
580
  defaults = options_hash(node)
681
581
  context.push_with_options(defaults) do
@@ -683,18 +583,13 @@ module Rigor
683
583
  end
684
584
  end
685
585
 
686
- # `mount Foo::Engine, at: '/path', as: :name` —
687
- # mounted Rails engines. The mount adds a single
688
- # helper for the mount point itself: `<as>_path` /
689
- # `<as>_url` (arity 0). The engine's own internal
690
- # helpers are out of scope (they'd need the engine's
691
- # routes.rb available; almost no static parser
692
- # follows that). Mastodon uses `mount Sidekiq::Web,
693
- # at: 'sidekiq', as: :sidekiq` and similar.
586
+ # `mount Foo::Engine, at: '/path', as: :name` — mounted Rails engines. The mount adds a single
587
+ # helper for the mount point itself: `<as>_path` / `<as>_url` (arity 0). The engine's own internal
588
+ # helpers are out of scope (they'd need the engine's routes.rb available; almost no static parser
589
+ # follows that). Mastodon uses `mount Sidekiq::Web, at: 'sidekiq', as: :sidekiq` and similar.
694
590
  #
695
- # When `as:` is omitted Rails derives a helper name
696
- # from the engine class that's harder to compute
697
- # statically, so we silently skip.
591
+ # When `as:` is omitted Rails derives a helper name from the engine class — that's harder to
592
+ # compute statically, so we silently skip.
698
593
  def handle_mount(node, context)
699
594
  options = options_hash(node)
700
595
  as_name = options[:as]
@@ -710,15 +605,11 @@ module Rigor
710
605
  )
711
606
  end
712
607
 
713
- # `use_doorkeeper do ... end` — Doorkeeper gem's
714
- # standard OAuth route helpers (`oauth_token_path`,
715
- # `oauth_authorization_path`, `oauth_application_path`,
716
- # etc.). We generate the full catalogue plus walk the
717
- # block body for `skip_controllers <names>` calls so
718
- # the project's omitted controllers don't register.
719
- # `controllers <hash>` mappings inside the block change
720
- # the serving controller class but not the helper
721
- # names — they can stay unmodelled.
608
+ # `use_doorkeeper do ... end` — Doorkeeper gem's standard OAuth route helpers (`oauth_token_path`,
609
+ # `oauth_authorization_path`, `oauth_application_path`, etc.). We generate the full catalogue plus
610
+ # walk the block body for `skip_controllers <names>` calls so the project's omitted controllers
611
+ # don't register. `controllers <hash>` mappings inside the block change the serving controller
612
+ # class but not the helper names they can stay unmodelled.
722
613
  def handle_use_doorkeeper(node, context)
723
614
  skip = collect_doorkeeper_skips(node)
724
615
  DoorkeeperRoutes.generate(skip: skip).each do |entry|
@@ -750,37 +641,27 @@ module Rigor
750
641
 
751
642
  # `scope "/:slug", as: "event" do ... end`
752
643
  #
753
- # When `as:` is present the block body is interpreted under a
754
- # new `:as_scope` stack frame that adds the given prefix to
755
- # every helper registered inside. Dynamic path segments
756
- # (`:slug`) are counted so nested-resource arities stay
757
- # correct.
644
+ # When `as:` is present the block body is interpreted under a new `:as_scope` stack frame that
645
+ # adds the given prefix to every helper registered inside. Dynamic path segments (`:slug`) are
646
+ # counted so nested-resource arities stay correct.
758
647
  #
759
- # When `as:` is absent the block is interpreted without any
760
- # prefix change helper names are unaffected by the scope's
761
- # path, which matches Rails' behaviour for path-only scopes.
648
+ # When `as:` is absent the block is interpreted without any prefix change — helper names are
649
+ # unaffected by the scope's path, which matches Rails' behaviour for path-only scopes.
762
650
  def handle_scope(node, context)
763
651
  as_name = keyword_symbol(node, :as)
764
- # `scope :path_arg, as: :name` (path as a positional
765
- # arg) vs `scope(path: ':project_id', as: :project)`
766
- # (path as a `:path` keyword). GitLab's project routes
767
- # rely on the latter for the `*namespace_id` /
768
- # `:project_id` outer scopes. The leading `/` is added
769
- # if missing so the path-prefix join produces clean
770
- # segments.
652
+ # `scope :path_arg, as: :name` (path as a positional arg) vs `scope(path: ':project_id', as:
653
+ # :project)` (path as a `:path` keyword). GitLab's project routes rely on the latter for the
654
+ # `*namespace_id` / `:project_id` outer scopes. The leading `/` is added if missing so the
655
+ # path-prefix join produces clean segments.
771
656
  path = string_argument(node, 0) || keyword_value_string(node, :path)
772
657
  path = "/#{path}" if path && !path.start_with?("/")
773
658
  arity = path ? count_path_placeholders(path) : 0
774
659
 
775
660
  if as_name.nil?
776
- # Even without `:as`, a `scope(path: 'groups/*id')`
777
- # still contributes its path / arity segments to
778
- # helpers declared inside (GitLab's
779
- # `scope(path: 'groups/*id', controller: :groups)
780
- # do; get :edit, as: :edit_group end` → arity 1).
781
- # Skip the frame when there's also no path
782
- # contribution (a pure `scope(module: :foo)` with
783
- # no helper-name / path effect).
661
+ # Even without `:as`, a `scope(path: 'groups/*id')` still contributes its path / arity segments
662
+ # to helpers declared inside (GitLab's `scope(path: 'groups/*id', controller: :groups) do; get
663
+ # :edit, as: :edit_group end` → arity 1). Skip the frame when there's also no path contribution
664
+ # (a pure `scope(module: :foo)` with no helper-name / path effect).
784
665
  return interpret_block_body(node, context) if path.nil? || arity.zero?
785
666
 
786
667
  context.push_as_scope("", path, arity) do
@@ -801,13 +682,10 @@ module Rigor
801
682
  options = effective_options_for(node, context)
802
683
  actions = restrict_actions_from(options, DEFAULT_RESOURCE_ACTIONS)
803
684
  base_arity = context.parent_segment_count
804
- # `resources :collections, as: :actor_collections`
805
- # remaps the helper name family
806
- # `actor_collections_path` for index,
807
- # `actor_collection_path(id)` for show. Path / arity
808
- # are unaffected. Mastodon uses this inside a
809
- # concern (`resources :collections, only: [:show],
810
- # as: :actor_collections`).
685
+ # `resources :collections, as: :actor_collections` remaps the helper name family —
686
+ # `actor_collections_path` for index, `actor_collection_path(id)` for show. Path / arity are
687
+ # unaffected. Mastodon uses this inside a concern (`resources :collections, only: [:show], as:
688
+ # :actor_collections`).
811
689
  helper_name = options[:as] || name
812
690
 
813
691
  register_resourceful_helpers(helper_name, actions, base_arity, context, plural: true)
@@ -827,30 +705,23 @@ module Rigor
827
705
  base_arity = context.parent_segment_count
828
706
  helper_name = options[:as] || name
829
707
 
830
- # Singular resource — no `:id` segment, no `:index`
831
- # / pluralised helper. The "show" helper is
708
+ # Singular resource — no `:id` segment, no `:index` / pluralised helper. The "show" helper is
832
709
  # `<name>_path` (singular).
833
710
  register_resourceful_helpers(helper_name, actions, base_arity, context, plural: false)
834
711
 
835
- # Push a `:singular_scope` frame so nested
836
- # declarations pick up the singular resource's
837
- # name in their helper prefix (Mastodon's
838
- # `resource :instance do; scope module: :instances
839
- # do; resources :domain_blocks; end; end` →
840
- # `instance_domain_blocks_path`). The singular
841
- # frame adds NO `:id` segment to arity — singular
842
- # resources don't carry one.
712
+ # Push a `:singular_scope` frame so nested declarations pick up the singular resource's name in
713
+ # their helper prefix (Mastodon's `resource :instance do; scope module: :instances do; resources
714
+ # :domain_blocks; end; end` `instance_domain_blocks_path`). The singular frame adds NO `:id`
715
+ # segment to arity singular resources don't carry one.
843
716
  context.push_singular_resource(name) do
844
717
  replay_concerns(node, context)
845
718
  interpret_block_body(node, context)
846
719
  end
847
720
  end
848
721
 
849
- # `concern :account_resources do ... end` registers the
850
- # body for later replay; we DO NOT interpret it at the
851
- # definition site (the body has no parent-resource
852
- # context yet). Concerns at the top level land in the
853
- # Context's `concerns` map by Symbol name.
722
+ # `concern :account_resources do ... end` registers the body for later replay; we DO NOT interpret
723
+ # it at the definition site (the body has no parent-resource context yet). Concerns at the top
724
+ # level land in the Context's `concerns` map by Symbol name.
854
725
  def handle_concern_definition(node, context)
855
726
  name = symbol_argument(node, 0)
856
727
  body = node.block&.body
@@ -859,12 +730,9 @@ module Rigor
859
730
  context.register_concern(name, body)
860
731
  end
861
732
 
862
- # `resources :accounts, concerns: :account_resources do ... end`
863
- # replays the registered concern body inside the
864
- # current Context (which already has the accounts
865
- # resource frame pushed). Supports both single-symbol
866
- # (`concerns: :name`) and array-of-symbols
867
- # (`concerns: [:a, :b]`) forms.
733
+ # `resources :accounts, concerns: :account_resources do ... end` — replays the registered concern
734
+ # body inside the current Context (which already has the accounts resource frame pushed). Supports
735
+ # both single-symbol (`concerns: :name`) and array-of-symbols (`concerns: [:a, :b]`) forms.
868
736
  def replay_concerns(resource_node, context)
869
737
  replay_concerns_from_options(effective_options_for(resource_node, context), context)
870
738
  end
@@ -881,33 +749,25 @@ module Rigor
881
749
  end
882
750
  end
883
751
 
884
- # Returns the merged option hash for a node — the
885
- # caller's own options override `with_options`
886
- # defaults from the surrounding Context stack. Used by
887
- # `handle_resources` / `handle_resource` so a bare
888
- # `resources :foo` inside `with_options only: [:index],
889
- # concerns: :batch do ... end` is treated as if it
890
- # had those options written inline.
752
+ # Returns the merged option hash for a node — the caller's own options override `with_options`
753
+ # defaults from the surrounding Context stack. Used by `handle_resources` / `handle_resource` so a
754
+ # bare `resources :foo` inside `with_options only: [:index], concerns: :batch do ... end` is
755
+ # treated as if it had those options written inline.
891
756
  def effective_options_for(node, context)
892
757
  context.with_options_defaults.merge(options_hash(node))
893
758
  end
894
759
 
895
- # Reads the value of an options-hash key. Distinct from
896
- # `keyword_symbol` / `keyword_array` because `concerns:`
897
- # accepts EITHER a single Symbol or an Array of Symbols
898
- # — same Rails idiom Rails accepts.
760
+ # Reads the value of an options-hash key. Distinct from `keyword_symbol` / `keyword_array` because
761
+ # `concerns:` accepts EITHER a single Symbol or an Array of Symbols — same Rails idiom Rails accepts.
899
762
  def keyword_value(node, key)
900
763
  options_hash(node)[key]
901
764
  end
902
765
 
903
766
  def handle_root(node, context)
904
- # `root to: "..."` / `root "..."` — single helper
905
- # `root_path`, arity 0, GET. Real-world Rails apps also
906
- # use `root :to => 'welcome#index', :as => 'home'` (the
907
- # canonical Redmine idiom across 230+ call sites), which
908
- # registers an additional `home_path` / `home_url` alias
909
- # for the same path. Mastodon and Solidus also use the
910
- # `as:` form occasionally for analytics-friendly URL
767
+ # `root to: "..."` / `root "..."` — single helper `root_path`, arity 0, GET. Real-world Rails
768
+ # apps also use `root :to => 'welcome#index', :as => 'home'` (the canonical Redmine idiom across
769
+ # 230+ call sites), which registers an additional `home_path` / `home_url` alias for the same
770
+ # path. Mastodon and Solidus also use the `as:` form occasionally for analytics-friendly URL
911
771
  # naming.
912
772
  path = context.path_prefix.empty? ? "/" : context.path_prefix
913
773
  context.entries << HelperTable::Entry.new(
@@ -925,23 +785,17 @@ module Rigor
925
785
  end
926
786
 
927
787
  def handle_explicit_route(node, context)
928
- # Member / collection block shorthand: `post :memorialize`
929
- # inside `member do ... end` (no path arg, just a
930
- # SymbolNode). Rails generates a helper based on the
931
- # action name + the enclosing resource: a member
932
- # action becomes `<action>_<singular_chain>_path(id)`,
933
- # a collection action becomes
934
- # `<action>_<plural_chain>_path`.
788
+ # Member / collection block shorthand: `post :memorialize` inside `member do ... end` (no path
789
+ # arg, just a SymbolNode). Rails generates a helper based on the action name + the enclosing
790
+ # resource: a member action becomes `<action>_<singular_chain>_path(id)`, a collection action
791
+ # becomes `<action>_<plural_chain>_path`.
935
792
  return register_member_collection_action(node, context) if member_collection_shorthand?(node, context)
936
793
 
937
- # `:on => :collection` / `:on => :member` inline form
938
- # Rails accepts this as a shorthand for `collection
939
- # do ... end` / `member do ... end` wrappers around a
940
- # single action. `get 'report', :on => :collection`
941
- # inside `resources :time_entries` generates
942
- # `report_time_entries_path` (collection-style: action
943
- # prefix on the plural resource name). Treat the action
944
- # name as the path's basename string (Redmine's idiom).
794
+ # `:on => :collection` / `:on => :member` inline form — Rails accepts this as a shorthand for
795
+ # `collection do ... end` / `member do ... end` wrappers around a single action. `get 'report',
796
+ # :on => :collection` inside `resources :time_entries` generates `report_time_entries_path`
797
+ # (collection-style: action prefix on the plural resource name). Treat the action name as the
798
+ # path's basename string (Redmine's idiom).
945
799
  on_target = keyword_symbol(node, :on)
946
800
  if on_target && context.inside_resource_scope?
947
801
  path_arg = string_argument(node, 0) || symbol_argument(node, 0)&.to_s
@@ -949,36 +803,27 @@ module Rigor
949
803
  return register_on_target_action(node, context, as_name, on_target) if as_name
950
804
  end
951
805
 
952
- # `get "/about", to: "static#about", as: :about` —
953
- # path as the first positional arg. Also handle the
954
- # hashrocket-key form `get 'help/*path' => 'help#show',
955
- # as: :help_page` where the path is the String key in
956
- # the trailing keyword/options hash (its value is the
957
- # `controller#action`). Without this, the arity check
958
- # underestimates because the placeholder count comes
959
- # from a nil path.
806
+ # `get "/about", to: "static#about", as: :about` — path as the first positional arg. Also handle
807
+ # the hashrocket-key form `get 'help/*path' => 'help#show', as: :help_page` where the path is the
808
+ # String key in the trailing keyword/options hash (its value is the `controller#action`). Without
809
+ # this, the arity check underestimates because the placeholder count comes from a nil path.
960
810
  path = string_argument(node, 0) || hashrocket_path_key(node)
961
811
  as_name = keyword_symbol(node, :as)
962
812
  return if as_name.nil? && path.nil?
963
813
 
964
- # When `as:` is omitted, Rails derives a helper name
965
- # from the path for static paths (no :segment). We
966
- # do the same when the path has no placeholders. The
967
- # special case `get '/'` inside a `:as_scope` (e.g.
968
- # `scope path: '/blob/*id', as: :blob do; get '/' end`)
969
- # has empty path content after stripping the leading
970
- # slash — Rails reuses the enclosing as_scope's name.
971
- # We surface that by registering the helper with an
972
- # empty `as_name`, which `explicit_route_helper_name`
973
- # then composes from `helper_prefix.chomp("_")`.
814
+ # When `as:` is omitted, Rails derives a helper name from the path for static paths (no
815
+ # :segment). We do the same when the path has no placeholders. The special case `get '/'` inside
816
+ # a `:as_scope` (e.g. `scope path: '/blob/*id', as: :blob do; get '/' end`) has empty path
817
+ # content after stripping the leading slash — Rails reuses the enclosing as_scope's name. We
818
+ # surface that by registering the helper with an empty `as_name`, which
819
+ # `explicit_route_helper_name` then composes from `helper_prefix.chomp("_")`.
974
820
  if as_name.nil?
975
821
  return if path.nil? || path.include?(":")
976
822
 
977
823
  as_name = path.delete_prefix("/").tr("/", "_")
978
824
  if as_name.empty? && context.helper_prefix.empty?
979
- # `get '/'` style — only meaningful when the
980
- # helper_prefix is non-empty (otherwise Rails would
981
- # generate a root helper, handled elsewhere).
825
+ # `get '/'` style — only meaningful when the helper_prefix is non-empty (otherwise Rails
826
+ # would generate a root helper, handled elsewhere).
982
827
  return
983
828
  end
984
829
  end
@@ -987,11 +832,9 @@ module Rigor
987
832
  total_placeholders = count_path_placeholders(path)
988
833
  optional = count_optional_path_placeholders(path)
989
834
  base_arity = context.parent_segment_count + total_placeholders
990
- # Register the maximum-arity entry; if the path
991
- # carries `(...)` optional segments (e.g.
992
- # `'settings(/:tab)'`), register additional entries
993
- # for each smaller arity so the call-site arity check
994
- # accepts `settings_project_path(:id)` (no `:tab`).
835
+ # Register the maximum-arity entry; if the path carries `(...)` optional segments (e.g.
836
+ # `'settings(/:tab)'`), register additional entries for each smaller arity so the call-site
837
+ # arity check accepts `settings_project_path(:id)` (no `:tab`).
995
838
  (0..optional).each do |drop|
996
839
  context.entries << HelperTable::Entry.new(
997
840
  name: name, arity: base_arity - drop,
@@ -1001,28 +844,21 @@ module Rigor
1001
844
  end
1002
845
  end
1003
846
 
1004
- # Builds the helper name for an explicit `get` / `post`
1005
- # / `match` route. Rails uses two distinct shapes:
847
+ # Builds the helper name for an explicit `get` / `post` / `match` route. Rails uses two distinct
848
+ # shapes:
1006
849
  #
1007
- # - **Outside a `resources` scope** (top-level or inside
1008
- # `namespace` / `scope`): `<namespace_prefix><as>_path`
1009
- # — e.g. `match 'login', as: :signin` →
1010
- # `signin_path`; inside `namespace :admin`
1011
- # `admin_signin_path`.
1012
- # - **Inside a `resources` scope**: the `:as` acts as
1013
- # the ACTION prefix on the resource's singular chain.
1014
- # `resources :projects do; get 'settings', as:
1015
- # :settings; end` → `settings_project_path(:id)` (NOT
1016
- # `project_settings_path`). The namespace prefix still
1017
- # leads: `namespace :admin do; resources :projects do;
1018
- # get 'foo', as: :bar; end; end` →
850
+ # - **Outside a `resources` scope** (top-level or inside `namespace` / `scope`):
851
+ # `<namespace_prefix><as>_path` — e.g. `match 'login', as: :signin``signin_path`; inside
852
+ # `namespace :admin` → `admin_signin_path`.
853
+ # - **Inside a `resources` scope**: the `:as` acts as the ACTION prefix on the resource's singular
854
+ # chain. `resources :projects do; get 'settings', as: :settings; end` →
855
+ # `settings_project_path(:id)` (NOT `project_settings_path`). The namespace prefix still leads:
856
+ # `namespace :admin do; resources :projects do; get 'foo', as: :bar; end; end` →
1019
857
  # `admin_bar_project_path(:id)`.
1020
858
  def explicit_route_helper_name(context, as_name)
1021
- # Empty `as_name` is the `get '/'`-inside-an-as_scope
1022
- # case (e.g. `scope path: '/blob/*id', as: :blob do;
1023
- # get '/' end` `blob_path(:id)`). Use the
1024
- # helper_prefix as-is (drop the trailing underscore)
1025
- # — Rails reuses the enclosing scope's name verbatim.
859
+ # Empty `as_name` is the `get '/'`-inside-an-as_scope case (e.g. `scope path: '/blob/*id', as:
860
+ # :blob do; get '/' end` → `blob_path(:id)`). Use the helper_prefix as-is (drop the trailing
861
+ # underscore) Rails reuses the enclosing scope's name verbatim.
1026
862
  return "#{context.helper_prefix.chomp('_')}_path" if as_name.to_s.empty?
1027
863
 
1028
864
  if context.inside_resource_scope?
@@ -1032,25 +868,20 @@ module Rigor
1032
868
  end
1033
869
  end
1034
870
 
1035
- # True when the call is `<verb> :symbol [, options...]`
1036
- # or `<verb> 'string'` inside one of:
1037
- # - a `member do ... end` / `collection do ... end`
1038
- # block (canonical shorthand context), or
1039
- # - a `resources :name do ... end` / `resource :name
1040
- # do ... end` block at the *direct* nesting level.
1041
- # Rails defaults a bare symbol- or string-named verb
1042
- # inside resources to a member action (GitLab's
1043
- # `resource :application_settings do; match :general,
1044
- # via: [...] end` → `general_application_settings_path`).
871
+ # True when the call is `<verb> :symbol [, options...]` or `<verb> 'string'` inside one of:
872
+ # - a `member do ... end` / `collection do ... end` block (canonical shorthand context), or
873
+ # - a `resources :name do ... end` / `resource :name do ... end` block at the *direct* nesting
874
+ # level. Rails defaults a bare symbol- or string-named verb inside resources to a member action
875
+ # (GitLab's `resource :application_settings do; match :general, via: [...] end`
876
+ # `general_application_settings_path`).
1045
877
  def member_collection_shorthand?(node, context)
1046
878
  return false unless context.innermost_action_block || context.inside_resource_scope?
1047
879
 
1048
880
  first_arg = node.arguments&.arguments&.first
1049
881
  return true if first_arg.is_a?(Prism::SymbolNode)
1050
882
 
1051
- # A plain action-name string with no `/` or `:` —
1052
- # treat it as if it were a symbol (`get 'report'` ==
1053
- # `get :report` inside member/collection block).
883
+ # A plain action-name string with no `/` or `:` — treat it as if it were a symbol (`get 'report'`
884
+ # == `get :report` inside member/collection block).
1054
885
  first_arg.is_a?(Prism::StringNode) &&
1055
886
  !first_arg.unescaped.include?("/") &&
1056
887
  !first_arg.unescaped.include?(":")
@@ -1058,20 +889,16 @@ module Rigor
1058
889
 
1059
890
  # Generates the member / collection action helper.
1060
891
  # Member: `<action>_<helper_prefix>path(id)`, arity =
1061
- # parent_segment_count (which already includes the
1062
- # enclosing resource's `:id`).
892
+ # parent_segment_count (which already includes the enclosing resource's `:id`).
1063
893
  # Collection: `<action>_<plural_helper_prefix>path`,
1064
- # arity = parent_segment_count - 1 (no `:id` segment;
1065
- # the collection URL is /<resource>/<action>).
894
+ # arity = parent_segment_count - 1 (no `:id` segment; the collection URL is /<resource>/<action>).
1066
895
  def register_member_collection_action(node, context)
1067
896
  action_name = symbol_argument(node, 0)&.to_s ||
1068
897
  string_argument(node, 0).to_s
1069
898
  frame = context.innermost_action_block
1070
- # No `member do` / `collection do` wrapper but we're
1071
- # inside a resources block Rails defaults a bare
1072
- # `<verb> :symbol` inside resources to a MEMBER
1073
- # action (e.g. `get :preview` → `preview_<singular>_path
1074
- # (:id)`).
899
+ # No `member do` / `collection do` wrapper but we're inside a resources block — Rails defaults a
900
+ # bare `<verb> :symbol` inside resources to a MEMBER action (e.g. `get :preview`
901
+ # `preview_<singular>_path(:id)`).
1075
902
  if frame.nil?
1076
903
  register_member_action(node, context, action_name)
1077
904
  elsif frame[:kind] == :member_block
@@ -1081,12 +908,9 @@ module Rigor
1081
908
  end
1082
909
  end
1083
910
 
1084
- # `get 'report', :on => :collection` (or `:member`)
1085
- # inline form inside `resources`. The helper name shape
1086
- # matches member_action / collection_action: the action
1087
- # name prefixes the resource's singular (member) or
1088
- # plural (collection). Closes Redmine's
1089
- # `report_time_entries_path` cluster.
911
+ # `get 'report', :on => :collection` (or `:member`) inline form inside `resources`. The helper name
912
+ # shape matches member_action / collection_action: the action name prefixes the resource's
913
+ # singular (member) or plural (collection). Closes Redmine's `report_time_entries_path` cluster.
1090
914
  def register_on_target_action(node, context, action_name, on_target)
1091
915
  scope_frame = context.innermost_scope_frame
1092
916
  return if scope_frame.nil?
@@ -1114,12 +938,9 @@ module Rigor
1114
938
  end
1115
939
 
1116
940
  def register_member_action(node, context, action_name)
1117
- # `helper_prefix` already ends with "_" (each segment
1118
- # appends one); the formula below yields e.g.
1119
- # `memorialize_admin_account_path`. Arity equals the
1120
- # parent segment count — Rails member URLs carry the
1121
- # enclosing resource's `:id`, which the :scope frame
1122
- # already counts.
941
+ # `helper_prefix` already ends with "_" (each segment appends one); the formula below yields e.g.
942
+ # `memorialize_admin_account_path`. Arity equals the parent segment count — Rails member URLs
943
+ # carry the enclosing resource's `:id`, which the :scope frame already counts.
1123
944
  name = "#{action_name}_#{context.helper_prefix}path"
1124
945
  context.entries << HelperTable::Entry.new(
1125
946
  name: name, arity: context.parent_segment_count,
@@ -1129,10 +950,8 @@ module Rigor
1129
950
  end
1130
951
 
1131
952
  def register_collection_action(node, context, action_name, frame)
1132
- # Collection URL drops the immediate parent's `:id`
1133
- # segment. The plural helper prefix swaps the
1134
- # singular form (in `helper_prefix`) for the plural
1135
- # — the immediate-resource frame stored both.
953
+ # Collection URL drops the immediate parent's `:id` segment. The plural helper prefix swaps the
954
+ # singular form (in `helper_prefix`) for the plural the immediate-resource frame stored both.
1136
955
  plural_prefix = context.helper_prefix.sub(/#{frame[:parent_singular]}_\z/, "#{frame[:parent_plural]}_")
1137
956
  name = "#{action_name}_#{plural_prefix}path"
1138
957
  arity = [context.parent_segment_count - 1, 0].max
@@ -1159,8 +978,7 @@ module Rigor
1159
978
  end
1160
979
 
1161
980
  def handle_member_or_collection(node, context)
1162
- # Only meaningful when we're inside a `resources` /
1163
- # `resource` block. The Context's stack tells us.
981
+ # Only meaningful when we're inside a `resources` / `resource` block. The Context's stack tells us.
1164
982
  resource = context.innermost_resource
1165
983
  return interpret_block_body(node, context) if resource.nil?
1166
984
 
@@ -1171,25 +989,20 @@ module Rigor
1171
989
  end
1172
990
 
1173
991
  def in_singular_resource?(*)
1174
- # Stub: always returns true so member / collection
1175
- # blocks descend. The singular-resource frame
1176
- # (`push_singular_resource`) is modelled in Context;
1177
- # a future caller could use it to tighten this check.
992
+ # Stub: always returns true so member / collection blocks descend. The singular-resource frame
993
+ # (`push_singular_resource`) is modelled in Context; a future caller could use it to tighten this
994
+ # check.
1178
995
  true
1179
996
  end
1180
997
 
1181
- # Generate the standard helpers for a resource(s).
1182
- # `plural: true` for `resources :users`, `false` for
1183
- # `resource :profile`.
998
+ # Generate the standard helpers for a resource(s). `plural: true` for `resources :users`, `false`
999
+ # for `resource :profile`.
1184
1000
  def register_resourceful_helpers(name, actions, base_arity, context, plural:)
1185
- # Plural resources (`resources :users`) singularise
1186
- # for show / new / edit helpers `user_path(id)`.
1187
- # Singular resources (`resource :foo`) use the name
1188
- # AS-IS singularising would mangle deliberately-
1189
- # plural names like Mastodon's `resource
1190
- # :relationships` → `relationships_path`, not
1191
- # `relationship_path`. The URL path uses `name`
1192
- # in both shapes (Rails never singularises the URL).
1001
+ # Plural resources (`resources :users`) singularise for show / new / edit helpers →
1002
+ # `user_path(id)`. Singular resources (`resource :foo`) use the name AS-IS — singularising would
1003
+ # mangle deliberately-plural names like Mastodon's `resource :relationships`
1004
+ # `relationships_path`, not `relationship_path`. The URL path uses `name` in both shapes (Rails
1005
+ # never singularises the URL).
1193
1006
  singular = plural ? singularize_word(name.to_s) : name.to_s
1194
1007
  path_base = "#{context.path_prefix}/#{name}"
1195
1008
 
@@ -1203,20 +1016,13 @@ module Rigor
1203
1016
  end
1204
1017
  end
1205
1018
 
1206
- # Maps an action keyword to the route-helper entry it
1207
- # produces. The five "named-helper" actions
1208
- # (`:index` / `:show` / `:new` / `:edit` plus
1209
- # singular-resource `:show`) generate a distinct
1210
- # helper; the three "verb-only" actions (`:create` /
1211
- # `:update` / `:destroy`) Rails serves under the same
1212
- # path-helper Rails reuses for show / index forms so
1213
- # we emit them too, otherwise an `only: [:create]`
1214
- # resource (e.g. Mastodon's `resource :inbox, only:
1215
- # [:create]`) registers NO helpers and downstream
1216
- # callers see a false `unknown-helper inbox_path`.
1217
- # The HelperTable already dedupes by name, so a
1218
- # resource that lists both `:show` and `:update` does
1219
- # not double-register.
1019
+ # Maps an action keyword to the route-helper entry it produces. The five "named-helper" actions
1020
+ # (`:index` / `:show` / `:new` / `:edit` plus singular-resource `:show`) generate a distinct
1021
+ # helper; the three "verb-only" actions (`:create` / `:update` / `:destroy`) Rails serves under the
1022
+ # same path-helper Rails reuses for show / index forms — so we emit them too, otherwise an `only:
1023
+ # [:create]` resource (e.g. Mastodon's `resource :inbox, only: [:create]`) registers NO helpers and
1024
+ # downstream callers see a false `unknown-helper inbox_path`. The HelperTable already dedupes by
1025
+ # name, so a resource that lists both `:show` and `:update` does not double-register.
1220
1026
  def entry_for_action(action, name:, singular:, base_arity:, path_base:, helper_prefix:, plural:)
1221
1027
  case action
1222
1028
  when :index then index_entry(plural, helper_prefix, name, base_arity, path_base, singular)
@@ -1229,18 +1035,14 @@ module Rigor
1229
1035
  )
1230
1036
  when :edit then edit_entry(plural, helper_prefix, singular, base_arity, path_base)
1231
1037
  when :create
1232
- # Plural `resources` collection POST shares the
1233
- # index helper (`<name>_path` → collection URL).
1234
- # Singular `resource` POST shares the show helper
1235
- # (`<name>_path` → resource URL). Both shapes
1236
- # produce a `<prefix><name>_path` entry; only the
1237
- # arity / path differ.
1038
+ # Plural `resources` collection POST shares the index helper (`<name>_path` → collection URL).
1039
+ # Singular `resource` POST shares the show helper (`<name>_path` → resource URL). Both shapes
1040
+ # produce a `<prefix><name>_path` entry; only the arity / path differ.
1238
1041
  create_entry(plural, helper_prefix, name, singular, base_arity, path_base)
1239
1042
  when :update, :destroy
1240
- # Member PATCH / PUT / DELETE on plural resources
1241
- # share the show helper (`<prefix><singular>_path(id)`).
1242
- # Singular-resource PATCH / DELETE shares
1243
- # `<prefix><name>_path` (no `:id`).
1043
+ # Member PATCH / PUT / DELETE on plural resources share the show helper
1044
+ # (`<prefix><singular>_path(id)`). Singular-resource PATCH / DELETE shares `<prefix><name>_path`
1045
+ # (no `:id`).
1244
1046
  show_entry(plural, helper_prefix, singular, base_arity, path_base)
1245
1047
  end
1246
1048
  end
@@ -1264,27 +1066,18 @@ module Rigor
1264
1066
  def index_entry(plural, helper_prefix, name, base_arity, path_base, singular)
1265
1067
  return nil unless plural
1266
1068
 
1267
- # Rails appends `_index_path` to the index helper
1268
- # name when the singular form of the resource matches
1269
- # the plural form AND the noun isn't in the canonical
1270
- # UNCOUNTABLE list. The collision would otherwise put
1271
- # both the index (`:id`-less) and show (`:id`-bearing)
1272
- # helpers under the same name, and Rails disambiguates
1273
- # by suffixing the index form. Mastodon's
1274
- # `resources :reblogged_by, controller:
1275
- # :reblogged_by_accounts, only: :index` and similar
1276
- # rely on this — calls like
1277
- # `api_v1_status_reblogged_by_index_url(status.id)`
1278
- # would otherwise read as `unknown-helper`.
1069
+ # Rails appends `_index_path` to the index helper name when the singular form of the resource
1070
+ # matches the plural form AND the noun isn't in the canonical UNCOUNTABLE list. The collision
1071
+ # would otherwise put both the index (`:id`-less) and show (`:id`-bearing) helpers under the same
1072
+ # name, and Rails disambiguates by suffixing the index form. Mastodon's `resources
1073
+ # :reblogged_by, controller: :reblogged_by_accounts, only: :index` and similar rely on this —
1074
+ # calls like `api_v1_status_reblogged_by_index_url(status.id)` would otherwise read as
1075
+ # `unknown-helper`.
1279
1076
  #
1280
- # Rails adds `_index_` whenever `singular == plural`
1281
- # including for UNCOUNTABLE nouns (Redmine's
1282
- # `resources :news` registers `news_index_path` for
1283
- # the index). The earlier "skip UNCOUNTABLE" carve-out
1284
- # was empirically wrong; only the
1285
- # IRREGULAR-singular path (e.g. `media → medium`)
1286
- # actually has `singular != plural` and skips
1287
- # `_index_`.
1077
+ # Rails adds `_index_` whenever `singular == plural` — including for UNCOUNTABLE nouns (Redmine's
1078
+ # `resources :news` registers `news_index_path` for the index). The earlier "skip UNCOUNTABLE"
1079
+ # carve-out was empirically wrong; only the IRREGULAR-singular path (e.g. `media medium`)
1080
+ # actually has `singular != plural` and skips `_index_`.
1288
1081
  index_name = if name.to_s == singular
1289
1082
  "#{helper_prefix}#{name}_index_path"
1290
1083
  else
@@ -1322,9 +1115,8 @@ module Rigor
1322
1115
  end
1323
1116
 
1324
1117
  def restrict_actions_from(options, default)
1325
- # `resources :foo, only: :show` is the same as
1326
- # `only: [:show]` in Rails; `options_hash` preserves the
1327
- # Symbol shape from the source, so coerce here.
1118
+ # `resources :foo, only: :show` is the same as `only: [:show]` in Rails; `options_hash` preserves
1119
+ # the Symbol shape from the source, so coerce here.
1328
1120
  if (only = options[:only])
1329
1121
  Array(only) & default
1330
1122
  elsif (except = options[:except])
@@ -1362,21 +1154,17 @@ module Rigor
1362
1154
  options_hash(node)[key]
1363
1155
  end
1364
1156
 
1365
- # Reads a keyword option whose value is a literal String
1366
- # (returned as-is). Returns nil when the key is missing
1367
- # or the value is non-literal. Used for `scope(path:
1368
- # ':project_id', ...)` shape parsing where `:path` is
1369
- # passed as a keyword rather than a positional arg.
1157
+ # Reads a keyword option whose value is a literal String (returned as-is). Returns nil when the key
1158
+ # is missing or the value is non-literal. Used for `scope(path: ':project_id', ...)` shape parsing
1159
+ # where `:path` is passed as a keyword rather than a positional arg.
1370
1160
  def keyword_value_string(node, key)
1371
1161
  value = options_hash(node)[key]
1372
1162
  value.is_a?(String) ? value : nil
1373
1163
  end
1374
1164
 
1375
- # `get 'help/*path' => 'help#show', as: :help_page` —
1376
- # the path lives in a String-keyed AssocNode of the
1377
- # trailing keyword/options hash (its value is the
1378
- # `controller#action` String). Returns the path String,
1379
- # or nil when no such hashrocket-key entry exists.
1165
+ # `get 'help/*path' => 'help#show', as: :help_page` — the path lives in a String-keyed AssocNode of
1166
+ # the trailing keyword/options hash (its value is the `controller#action` String). Returns the
1167
+ # path String, or nil when no such hashrocket-key entry exists.
1380
1168
  def hashrocket_path_key(node)
1381
1169
  args = node.arguments&.arguments || []
1382
1170
  last = args.last
@@ -1409,29 +1197,24 @@ module Rigor
1409
1197
  def count_path_placeholders(path)
1410
1198
  return 0 if path.nil?
1411
1199
 
1412
- # Both `:name` (regular segment) and `*name` (wildcard
1413
- # / "globbing" segment) are Rails path parameters and
1414
- # contribute to helper arity. GitLab's
1415
- # `path: '*namespace_id'` outer scope adds 1 to the
1416
- # arity of every helper underneath.
1200
+ # Both `:name` (regular segment) and `*name` (wildcard / "globbing" segment) are Rails path
1201
+ # parameters and contribute to helper arity. GitLab's `path: '*namespace_id'` outer scope adds 1
1202
+ # to the arity of every helper underneath.
1417
1203
  path.scan(/[:*][a-z_][a-z0-9_]*/).size
1418
1204
  end
1419
1205
 
1420
- # Number of placeholders inside `(...)` groups — Rails'
1421
- # optional-segment syntax (`'settings(/:tab)'`). Required
1422
- # arity is `count_path_placeholders - count_optional`;
1423
- # callers accepting a `(required..required+optional)`
1424
- # arity range register an entry per achievable arity.
1206
+ # Number of placeholders inside `(...)` groups — Rails' optional-segment syntax
1207
+ # (`'settings(/:tab)'`). Required arity is `count_path_placeholders - count_optional`; callers
1208
+ # accepting a `(required..required+optional)` arity range register an entry per achievable arity.
1425
1209
  def count_optional_path_placeholders(path)
1426
1210
  return 0 if path.nil?
1427
1211
 
1428
1212
  path.scan(/\([^)]*\)/).sum { |group| group.scan(/[:*][a-z_][a-z0-9_]*/).size }
1429
1213
  end
1430
1214
 
1431
- # ADR-39 — delegates to the shared inflector (real
1432
- # `ActiveSupport::Inflector` when available). Previously a
1433
- # hand-tuned AS-replica kept in sync with `Context#singularize`;
1434
- # both now share the one authoritative implementation.
1215
+ # ADR-39 — delegates to the shared inflector (real `ActiveSupport::Inflector` when available).
1216
+ # Previously a hand-tuned AS-replica kept in sync with `Context#singularize`; both now share the
1217
+ # one authoritative implementation.
1435
1218
  def singularize_word(word)
1436
1219
  Rigor::Plugin::Inflector.singularize(word)
1437
1220
  end