rigortype 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (433) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/08-understanding-errors.md +3 -3
  8. data/docs/manual/01-installation.md +1 -0
  9. data/docs/manual/02-cli-reference.md +16 -7
  10. data/docs/manual/04-diagnostics.md +7 -4
  11. data/docs/manual/06-baseline.md +5 -2
  12. data/docs/manual/07-plugins.md +1 -1
  13. data/docs/manual/14-rails-quickstart.md +4 -2
  14. data/docs/manual/15-type-protection-coverage.md +27 -4
  15. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  16. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  17. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  18. data/lib/rigor/analysis/baseline.rb +56 -96
  19. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  20. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  21. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  23. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  24. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  25. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  26. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  27. data/lib/rigor/analysis/check_rules.rb +174 -274
  28. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  29. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  30. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  31. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  32. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  33. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  34. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  35. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  36. data/lib/rigor/analysis/diagnostic.rb +48 -75
  37. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  38. data/lib/rigor/analysis/fact_store.rb +12 -22
  39. data/lib/rigor/analysis/incremental.rb +47 -63
  40. data/lib/rigor/analysis/incremental_session.rb +74 -100
  41. data/lib/rigor/analysis/project_scan.rb +12 -23
  42. data/lib/rigor/analysis/result.rb +3 -5
  43. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  44. data/lib/rigor/analysis/run_stats.rb +38 -61
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  48. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  49. data/lib/rigor/analysis/runner.rb +220 -333
  50. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  51. data/lib/rigor/analysis/worker_session.rb +64 -102
  52. data/lib/rigor/ast/type_node.rb +6 -8
  53. data/lib/rigor/ast.rb +6 -10
  54. data/lib/rigor/bleeding_edge.rb +27 -35
  55. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  56. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  57. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  58. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  59. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  60. data/lib/rigor/cache/descriptor.rb +58 -94
  61. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  62. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  63. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  64. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  65. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  66. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  67. data/lib/rigor/cache/rbs_environment.rb +11 -19
  68. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  69. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  70. data/lib/rigor/cache/store.rb +248 -186
  71. data/lib/rigor/cli/annotate_command.rb +51 -88
  72. data/lib/rigor/cli/baseline_command.rb +10 -17
  73. data/lib/rigor/cli/check_command.rb +121 -199
  74. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  75. data/lib/rigor/cli/ci_detector.rb +9 -14
  76. data/lib/rigor/cli/command.rb +8 -14
  77. data/lib/rigor/cli/coverage_command.rb +62 -48
  78. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  79. data/lib/rigor/cli/coverage_report.rb +0 -2
  80. data/lib/rigor/cli/coverage_scan.rb +7 -11
  81. data/lib/rigor/cli/diagnostic_formats.rb +46 -62
  82. data/lib/rigor/cli/diff_command.rb +10 -20
  83. data/lib/rigor/cli/docs_command.rb +26 -40
  84. data/lib/rigor/cli/doctor_command.rb +8 -9
  85. data/lib/rigor/cli/explain_command.rb +4 -7
  86. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  87. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  88. data/lib/rigor/cli/lsp_command.rb +11 -19
  89. data/lib/rigor/cli/mcp_command.rb +4 -6
  90. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  91. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  92. data/lib/rigor/cli/options.rb +11 -19
  93. data/lib/rigor/cli/plugin_command.rb +18 -30
  94. data/lib/rigor/cli/plugins_command.rb +29 -51
  95. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  96. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  97. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  98. data/lib/rigor/cli/protection_renderer.rb +5 -7
  99. data/lib/rigor/cli/protection_report.rb +34 -18
  100. data/lib/rigor/cli/renderable.rb +4 -6
  101. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  102. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  103. data/lib/rigor/cli/skill_command.rb +30 -47
  104. data/lib/rigor/cli/skill_describe.rb +40 -64
  105. data/lib/rigor/cli/trace_command.rb +9 -16
  106. data/lib/rigor/cli/trace_renderer.rb +35 -51
  107. data/lib/rigor/cli/triage_command.rb +6 -10
  108. data/lib/rigor/cli/triage_renderer.rb +1 -2
  109. data/lib/rigor/cli/type_of_command.rb +14 -23
  110. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  111. data/lib/rigor/cli/type_scan_command.rb +6 -10
  112. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  113. data/lib/rigor/cli/type_scan_report.rb +2 -3
  114. data/lib/rigor/cli/upgrade_command.rb +2 -3
  115. data/lib/rigor/cli.rb +16 -28
  116. data/lib/rigor/config_audit.rb +35 -44
  117. data/lib/rigor/configuration/dependencies.rb +41 -70
  118. data/lib/rigor/configuration/severity_profile.rb +28 -42
  119. data/lib/rigor/configuration.rb +150 -241
  120. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  121. data/lib/rigor/environment/class_registry.rb +19 -26
  122. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  123. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  124. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  125. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  126. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  127. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  128. data/lib/rigor/environment/rbs_loader.rb +420 -518
  129. data/lib/rigor/environment/reflection.rb +28 -53
  130. data/lib/rigor/environment/reporters.rb +13 -24
  131. data/lib/rigor/environment.rb +225 -304
  132. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  133. data/lib/rigor/flow_contribution/element.rb +7 -12
  134. data/lib/rigor/flow_contribution/fact.rb +33 -57
  135. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  136. data/lib/rigor/flow_contribution/merger.rb +32 -47
  137. data/lib/rigor/flow_contribution.rb +37 -55
  138. data/lib/rigor/inference/acceptance.rb +133 -219
  139. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  140. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  141. data/lib/rigor/inference/budget_trace.rb +43 -63
  142. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  143. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  144. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  145. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  146. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  147. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  148. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  149. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  150. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  151. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  152. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  153. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  154. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  155. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  156. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  157. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  158. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  159. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  160. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  161. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  162. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  163. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  164. data/lib/rigor/inference/def_return_typer.rb +8 -14
  165. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  166. data/lib/rigor/inference/expression_typer.rb +794 -1154
  167. data/lib/rigor/inference/fallback.rb +8 -12
  168. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  169. data/lib/rigor/inference/flow_tracer.rb +25 -36
  170. data/lib/rigor/inference/fork_map.rb +87 -0
  171. data/lib/rigor/inference/hkt_body.rb +45 -68
  172. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  173. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  174. data/lib/rigor/inference/hkt_registry.rb +50 -76
  175. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  176. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  177. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  178. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  179. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  180. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  181. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  182. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  183. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  184. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  185. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  186. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  187. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  188. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  189. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  190. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  191. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  192. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  193. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  194. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  195. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  196. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  197. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  198. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  199. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  200. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  201. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  202. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  203. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  204. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  205. data/lib/rigor/inference/mutation_widening.rb +115 -158
  206. data/lib/rigor/inference/narrowing.rb +627 -727
  207. data/lib/rigor/inference/origin_lookup.rb +38 -0
  208. data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
  209. data/lib/rigor/inference/precision_scanner.rb +26 -35
  210. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  211. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  212. data/lib/rigor/inference/protection_scanner.rb +16 -18
  213. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  214. data/lib/rigor/inference/scope_indexer.rb +473 -821
  215. data/lib/rigor/inference/statement_evaluator.rb +754 -1114
  216. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  217. data/lib/rigor/inference/synthetic_method.rb +9 -16
  218. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  219. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  220. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  221. data/lib/rigor/language_server/buffer_table.rb +11 -18
  222. data/lib/rigor/language_server/completion_provider.rb +69 -116
  223. data/lib/rigor/language_server/debouncer.rb +16 -25
  224. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  225. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  226. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  227. data/lib/rigor/language_server/hover_provider.rb +8 -11
  228. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  229. data/lib/rigor/language_server/loop.rb +14 -19
  230. data/lib/rigor/language_server/project_context.rb +46 -74
  231. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  232. data/lib/rigor/language_server/server.rb +55 -83
  233. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  234. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  235. data/lib/rigor/language_server/uri.rb +8 -13
  236. data/lib/rigor/language_server.rb +4 -6
  237. data/lib/rigor/mcp/loop.rb +2 -3
  238. data/lib/rigor/mcp/server.rb +4 -7
  239. data/lib/rigor/mcp.rb +3 -6
  240. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  241. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  242. data/lib/rigor/plugin/base.rb +335 -518
  243. data/lib/rigor/plugin/blueprint.rb +14 -23
  244. data/lib/rigor/plugin/box.rb +18 -29
  245. data/lib/rigor/plugin/fact_store.rb +16 -26
  246. data/lib/rigor/plugin/inflector.rb +37 -53
  247. data/lib/rigor/plugin/io_boundary.rb +33 -56
  248. data/lib/rigor/plugin/isolation.rb +42 -55
  249. data/lib/rigor/plugin/load_error.rb +10 -15
  250. data/lib/rigor/plugin/loader.rb +30 -49
  251. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  252. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  253. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  254. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  255. data/lib/rigor/plugin/macro.rb +10 -15
  256. data/lib/rigor/plugin/manifest.rb +85 -144
  257. data/lib/rigor/plugin/node_context.rb +14 -22
  258. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  259. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  260. data/lib/rigor/plugin/registry.rb +132 -205
  261. data/lib/rigor/plugin/services.rb +21 -33
  262. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  263. data/lib/rigor/plugin/trust_policy.rb +24 -38
  264. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  265. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  266. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  267. data/lib/rigor/protection/mutator.rb +50 -70
  268. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  269. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  270. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  271. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  272. data/lib/rigor/rbs_extended.rb +107 -197
  273. data/lib/rigor/reflection.rb +68 -86
  274. data/lib/rigor/scope/discovery_index.rb +14 -19
  275. data/lib/rigor/scope.rb +255 -310
  276. data/lib/rigor/sig_gen/classification.rb +6 -10
  277. data/lib/rigor/sig_gen/generator.rb +197 -323
  278. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  279. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  280. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  281. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  282. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  283. data/lib/rigor/sig_gen/renderer.rb +7 -13
  284. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  285. data/lib/rigor/sig_gen/write_result.rb +8 -16
  286. data/lib/rigor/sig_gen/writer.rb +95 -174
  287. data/lib/rigor/sig_gen.rb +3 -6
  288. data/lib/rigor/signature_path_audit.rb +24 -30
  289. data/lib/rigor/source/constant_path.rb +10 -14
  290. data/lib/rigor/source/literals.rb +31 -45
  291. data/lib/rigor/source/node_locator.rb +9 -11
  292. data/lib/rigor/source/node_walker.rb +9 -13
  293. data/lib/rigor/source.rb +3 -4
  294. data/lib/rigor/testing.rb +16 -20
  295. data/lib/rigor/triage/catalogue.rb +38 -62
  296. data/lib/rigor/triage.rb +31 -52
  297. data/lib/rigor/trinary.rb +9 -13
  298. data/lib/rigor/type/acceptance_router.rb +4 -6
  299. data/lib/rigor/type/accepts_result.rb +10 -14
  300. data/lib/rigor/type/app.rb +19 -27
  301. data/lib/rigor/type/bot.rb +4 -6
  302. data/lib/rigor/type/bound_method.rb +10 -15
  303. data/lib/rigor/type/combinator.rb +165 -257
  304. data/lib/rigor/type/constant.rb +23 -34
  305. data/lib/rigor/type/data_class.rb +10 -15
  306. data/lib/rigor/type/data_instance.rb +14 -20
  307. data/lib/rigor/type/difference.rb +21 -32
  308. data/lib/rigor/type/dynamic.rb +3 -5
  309. data/lib/rigor/type/hash_shape.rb +32 -18
  310. data/lib/rigor/type/integer_range.rb +11 -16
  311. data/lib/rigor/type/intersection.rb +27 -42
  312. data/lib/rigor/type/nominal.rb +10 -15
  313. data/lib/rigor/type/plain_lattice.rb +9 -13
  314. data/lib/rigor/type/refined.rb +67 -114
  315. data/lib/rigor/type/singleton.rb +4 -6
  316. data/lib/rigor/type/struct_class.rb +11 -16
  317. data/lib/rigor/type/struct_instance.rb +15 -21
  318. data/lib/rigor/type/tuple.rb +14 -19
  319. data/lib/rigor/type/union.rb +30 -42
  320. data/lib/rigor/type_node/generic.rb +14 -26
  321. data/lib/rigor/type_node/identifier.rb +12 -19
  322. data/lib/rigor/type_node.rb +3 -12
  323. data/lib/rigor/value_semantics.rb +16 -21
  324. data/lib/rigor/version.rb +1 -1
  325. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
  337. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  340. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
  348. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  350. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  351. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  353. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  354. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  355. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  356. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  357. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  358. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  359. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  361. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  363. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  365. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  366. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  367. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  368. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  369. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  372. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  373. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  376. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  377. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  378. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  379. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  380. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  381. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  385. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  386. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  389. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  390. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
  398. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
  399. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  400. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  406. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  408. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  409. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  411. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  416. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  417. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  426. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  427. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  428. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  429. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  430. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  431. data/sig/rigor/environment.rbs +1 -0
  432. data/sig/rigor/scope.rbs +6 -0
  433. metadata +9 -1
@@ -3,23 +3,17 @@
3
3
  module Rigor
4
4
  module Plugin
5
5
  class RailsI18n < Rigor::Plugin::Base
6
- # Frozen catalogue of every dotted key discovered across
7
- # all loaded locale files. Each entry tracks the locales
8
- # the key appears in and, for each locale, the set of
9
- # `%{var}` interpolation placeholders observed in the
10
- # leaf string.
6
+ # Frozen catalogue of every dotted key discovered across all loaded locale files. Each entry tracks
7
+ # the locales the key appears in and, for each locale, the set of `%{var}` interpolation placeholders
8
+ # observed in the leaf string.
11
9
  #
12
- # The catalogue is intentionally lossy: it only records
13
- # the *presence* of each key per locale and the
14
- # placeholder names. The actual translated values are
15
- # not retained — the analyzer doesn't need them and
16
- # keeping them would bloat the cache slice.
10
+ # The catalogue is intentionally lossy: it only records the *presence* of each key per locale and the
11
+ # placeholder names. The actual translated values are not retained — the analyzer doesn't need them
12
+ # and keeping them would bloat the cache slice.
17
13
  class LocaleIndex
18
- # `placeholders` is a Hash: `locale_name => Set<String>`.
19
- # `array_value` is true when at least one locale's leaf
20
- # is an Array (used by `l(time, format:)` and similar).
21
- # `value_kinds` is a Hash: `locale_name => Symbol`
22
- # (`:string` / `:array` / `:hash`).
14
+ # `placeholders` is a Hash: `locale_name => Set<String>`. `array_value` is true when at least one
15
+ # locale's leaf is an Array (used by `l(time, format:)` and similar). `value_kinds` is a Hash:
16
+ # `locale_name => Symbol` (`:string` / `:array` / `:hash`).
23
17
  Entry = Data.define(:dotted_key, :placeholders, :value_kinds) do
24
18
  def locales
25
19
  placeholders.keys
@@ -33,8 +27,7 @@ module Rigor
33
27
  placeholders.fetch(locale.to_s) { Set.new }
34
28
  end
35
29
 
36
- # Union of placeholder names across all known
37
- # locales — used by the analyzer when no specific
30
+ # Union of placeholder names across all known locales — used by the analyzer when no specific
38
31
  # locale is in scope.
39
32
  def all_placeholders
40
33
  placeholders.values.reduce(Set.new) { |acc, set| acc | set }
@@ -44,8 +37,7 @@ module Rigor
44
37
  attr_reader :entries, :locales
45
38
 
46
39
  # @param entries [Array<Entry>]
47
- # @param locales [Array<String>] all locale names
48
- # that contributed at least one key.
40
+ # @param locales [Array<String>] all locale names that contributed at least one key.
49
41
  def initialize(entries, locales:)
50
42
  @entries = entries.freeze
51
43
  @locales = locales.dup.freeze
@@ -62,19 +54,15 @@ module Rigor
62
54
  @by_key.key?(dotted_key.to_s)
63
55
  end
64
56
 
65
- # CLDR plural form keys recognised by Ruby I18n. When a
66
- # locale defines `accounts.posts.one`, `accounts.posts.other`,
67
- # the call `t('accounts.posts', count: n)` resolves into
68
- # the matching plural sub-key the parent `accounts.posts`
69
- # is a **pluralization namespace**, not a missing key.
70
- # Mastodon hits this on `accounts.posts` /
71
- # `accounts.following` / `accounts.followers` for the post,
72
- # follow, and follower counts shown on the profile page.
57
+ # CLDR plural form keys recognised by Ruby I18n. When a locale defines `accounts.posts.one`,
58
+ # `accounts.posts.other`, the call `t('accounts.posts', count: n)` resolves into the matching
59
+ # plural sub-key — the parent `accounts.posts` is a **pluralization namespace**, not a missing key.
60
+ # Mastodon hits this on `accounts.posts` / `accounts.following` / `accounts.followers` for the
61
+ # post, follow, and follower counts shown on the profile page.
73
62
  PLURAL_SUBKEYS = %w[zero one two few many other].freeze
74
63
 
75
- # Returns true when the dotted key itself isn't a leaf in
76
- # any locale, but at least one of its CLDR plural form
77
- # children exists.
64
+ # Returns true when the dotted key itself isn't a leaf in any locale, but at least one of its CLDR
65
+ # plural form children exists.
78
66
  def pluralization_namespace?(dotted_key)
79
67
  base = dotted_key.to_s
80
68
  PLURAL_SUBKEYS.any? { |sub| @by_key.key?("#{base}.#{sub}") }
@@ -88,14 +76,12 @@ module Rigor
88
76
  @entries.size
89
77
  end
90
78
 
91
- # All known dotted keys, sorted for stable did-you-mean
92
- # output.
79
+ # All known dotted keys, sorted for stable did-you-mean output.
93
80
  def keys
94
81
  @by_key.keys.sort
95
82
  end
96
83
 
97
- # Returns the locales (set of strings) in which a key
98
- # is *missing*, given the configured locale list.
84
+ # Returns the locales (set of strings) in which a key is *missing*, given the configured locale list.
99
85
  def missing_locales_for(dotted_key, configured_locales:)
100
86
  entry = find(dotted_key)
101
87
  return configured_locales.to_set if entry.nil?
@@ -7,27 +7,21 @@ require_relative "locale_index"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class RailsI18n < Rigor::Plugin::Base
10
- # Walks `locale_search_paths` for `.yml` / `.yaml`
11
- # files, reads each through the trusted IoBoundary,
12
- # parses with `YAML.safe_load`, and folds the resulting
13
- # nested hash into a flat `dotted_key => Entry` table.
10
+ # Walks `locale_search_paths` for `.yml` / `.yaml` files, reads each through the trusted IoBoundary,
11
+ # parses with `YAML.safe_load`, and folds the resulting nested hash into a flat `dotted_key => Entry`
12
+ # table.
14
13
  #
15
- # The top-level YAML key is the locale (`en:`, `ja:`,
16
- # …). Anything underneath is recursively flattened into
17
- # dotted keys (`users.welcome`, `errors.messages.blank`,
18
- # …). For each leaf string, `%{var}` placeholders are
19
- # extracted via a simple regex.
14
+ # The top-level YAML key is the locale (`en:`, `ja:`, …). Anything underneath is recursively
15
+ # flattened into dotted keys (`users.welcome`, `errors.messages.blank`, …). For each leaf string,
16
+ # `%{var}` placeholders are extracted via a simple regex.
20
17
  #
21
- # Files that fail to parse are skipped with a load-error
22
- # diagnostic surfaced through the plugin's error
23
- # channel. Non-Hash YAML roots (e.g. a top-level
24
- # sequence) are also skipped — the format is locale-keyed
25
- # by convention.
18
+ # Files that fail to parse are skipped with a load-error diagnostic surfaced through the plugin's
19
+ # error channel. Non-Hash YAML roots (e.g. a top-level sequence) are also skipped — the format is
20
+ # locale-keyed by convention.
26
21
  class LocaleLoader
27
22
  PLACEHOLDER_RE = /%\{(?<name>[^}]+)\}/
28
23
 
29
- # Errno classes that indicate "this file is not
30
- # readable as a YAML locale" — swallowed so a single
24
+ # Errno classes that indicate "this file is not readable as a YAML locale" — swallowed so a single
31
25
  # bad path doesn't take down the rest of the index.
32
26
  IO_ERRORS = [Errno::ENOENT, Errno::EACCES, Errno::EISDIR].freeze
33
27
 
@@ -100,19 +94,15 @@ module Rigor
100
94
  end.sort
101
95
  end
102
96
 
103
- # Recursively walks the per-locale subtree, yielding
104
- # `[dotted_key, leaf_value]` for each leaf. Hash leaves are
105
- # *not* recorded as entries themselves — only their
106
- # descendants — but every leaf scalar / array IS recorded.
97
+ # Recursively walks the per-locale subtree, yielding `[dotted_key, leaf_value]` for each leaf. Hash
98
+ # leaves are *not* recorded as entries themselves — only their descendants — but every leaf scalar
99
+ # / array IS recorded.
107
100
  #
108
- # `breadcrumbs` is a single mutable stack reused across the
109
- # whole walk (push before recursing, pop after): for the
110
- # 530-file / 14 MB Mastodon locale corpus the old
111
- # `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape
112
- # allocated a fresh breadcrumb Array at every node plus an
113
- # intermediate result Array at every level — millions of
114
- # short-lived objects and the run's top allocation site. The
115
- # dotted key is still materialised once per leaf (it has to
101
+ # `breadcrumbs` is a single mutable stack reused across the whole walk (push before recursing, pop
102
+ # after): for the 530-file / 14 MB Mastodon locale corpus the old
103
+ # `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape allocated a fresh breadcrumb Array at
104
+ # every node plus an intermediate result Array at every level — millions of short-lived objects and
105
+ # the run's top allocation site. The dotted key is still materialised once per leaf (it has to
116
106
  # be); everything else is now allocation-free traversal.
117
107
  def each_flattened(node, breadcrumbs, &)
118
108
  if node.is_a?(Hash)
@@ -129,9 +119,8 @@ module Rigor
129
119
  def extract_placeholders(value)
130
120
  case value
131
121
  when String
132
- # Most locale leaves carry no `%{var}`; skip the scan +
133
- # flatten + to_set allocation trio for them. A string with
134
- # no `%{` yields an empty placeholder set either way.
122
+ # Most locale leaves carry no `%{var}`; skip the scan + flatten + to_set allocation trio for
123
+ # them. A string with no `%{` yields an empty placeholder set either way.
135
124
  return Set.new unless value.include?("%{")
136
125
 
137
126
  value.scan(PLACEHOLDER_RE).flatten.to_set
@@ -8,16 +8,12 @@ require_relative "rails_i18n/analyzer"
8
8
 
9
9
  module Rigor
10
10
  module Plugin
11
- # rigor-rails-i18n — validates `t('key.path')` /
12
- # `I18n.t(...)` calls against `config/locales/*.yml`.
11
+ # rigor-rails-i18n — validates `t('key.path')` / `I18n.t(...)` calls against `config/locales/*.yml`.
13
12
  #
14
13
  # Tier 1B of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
15
- # Statically reads every YAML file under
16
- # `locale_search_paths` (default `config/locales/`),
17
- # builds a flat `dotted_key => Entry` index keyed by the
18
- # leaf key path, and validates every `t(literal_key, ...)`
19
- # call site against the catalogue. No Rails runtime
20
- # dependency.
14
+ # Statically reads every YAML file under `locale_search_paths` (default `config/locales/`), builds a
15
+ # flat `dotted_key => Entry` index keyed by the leaf key path, and validates every `t(literal_key, ...)`
16
+ # call site against the catalogue. No Rails runtime dependency.
21
17
  #
22
18
  # ## Configuration
23
19
  #
@@ -30,55 +26,37 @@ module Rigor
30
26
  #
31
27
  # ## What it checks
32
28
  #
33
- # 1. **Key existence** — `t('users.welcome')` is flagged
34
- # when `users.welcome` does not appear in any locale.
35
- # 2. **Per-locale coverage** — when the key resolves in
36
- # some locales but not all configured locales, the
37
- # plugin emits a `missing-locale` warning. Suppressed
38
- # when the call site passes `default:`.
39
- # 3. **Interpolation variables** — the leaf string's
40
- # `%{var}` placeholders must match the call's keyword
41
- # arguments. Missing placeholders are errors; extra
42
- # arguments are warnings.
43
- # 4. **View template lazy keys** — `t('.title')` inside
44
- # `app/views/setting/index.html.erb` expands to
45
- # `setting.index.title` and is validated against the
46
- # locale index. ERB, Haml, and Slim templates are
29
+ # 1. **Key existence** — `t('users.welcome')` is flagged when `users.welcome` does not appear in any
30
+ # locale.
31
+ # 2. **Per-locale coverage** — when the key resolves in some locales but not all configured locales,
32
+ # the plugin emits a `missing-locale` warning. Suppressed when the call site passes `default:`.
33
+ # 3. **Interpolation variables** — the leaf string's `%{var}` placeholders must match the call's
34
+ # keyword arguments. Missing placeholders are errors; extra arguments are warnings.
35
+ # 4. **View template lazy keys** — `t('.title')` inside `app/views/setting/index.html.erb` expands to
36
+ # `setting.index.title` and is validated against the locale index. ERB, Haml, and Slim templates are
47
37
  # scanned under `view_search_paths`.
48
38
  #
49
39
  # ## Limitations
50
40
  #
51
- # - Only literal-string keys are validated. `t(key)` with
52
- # a variable receiver is silently passed through.
53
- # - Lazy lookup (`t('.key')`) is supported for controller
54
- # files (`app/controllers/**/*_controller.rb`): the key
55
- # is expanded to `<controller_scope>.<action>.<key>`
56
- # using the file path and the innermost enclosing `def`.
57
- # Lazy keys in non-controller `.rb` files (models, helpers,
58
- # mailers, …) are silently skipped the controller/action
59
- # scope cannot be statically determined there.
60
- # - View template lazy keys (`t('.key')` inside ERB / Haml /
61
- # Slim) are validated for key existence and per-locale
62
- # coverage. Interpolation variable validation is skipped
63
- # for view templates (the hash may come from controller
64
- # instance variables not visible in the template).
65
- # The view scan is a project-wide pass surfaced through the
66
- # per-file diagnostic hook, so under `--workers` each
67
- # fork-pool worker re-emits the full set (the same
68
- # once-per-run limitation the `load-error` path carries);
41
+ # - Only literal-string keys are validated. `t(key)` with a variable receiver is silently passed through.
42
+ # - Lazy lookup (`t('.key')`) is supported for controller files (`app/controllers/**/*_controller.rb`):
43
+ # the key is expanded to `<controller_scope>.<action>.<key>` using the file path and the innermost
44
+ # enclosing `def`. Lazy keys in non-controller `.rb` files (models, helpers, mailers, …) are silently
45
+ # skipped the controller/action scope cannot be statically determined there.
46
+ # - View template lazy keys (`t('.key')` inside ERB / Haml / Slim) are validated for key existence and
47
+ # per-locale coverage. Interpolation variable validation is skipped for view templates (the hash may
48
+ # come from controller instance variables not visible in the template). The view scan is a
49
+ # project-wide pass surfaced through the per-file diagnostic hook, so under `--workers` each fork-pool
50
+ # worker re-emits the full set (the same once-per-run limitation the `load-error` path carries);
69
51
  # sequential `rigor check` is unaffected.
70
- # - Pluralization (`t('errors.messages.too_short',
71
- # count: n)`) is recognised at the call site but the
72
- # `count` key is not used to validate the locale's
73
- # pluralization branches.
74
- # - YAML aliases / merges are accepted (Psych's standard
75
- # `aliases: true`) but custom Ruby classes inside the
76
- # YAML are NOT permitted (`safe_load`).
52
+ # - Pluralization (`t('errors.messages.too_short', count: n)`) is recognised at the call site but the
53
+ # `count` key is not used to validate the locale's pluralization branches.
54
+ # - YAML aliases / merges are accepted (Psych's standard `aliases: true`) but custom Ruby classes
55
+ # inside the YAML are NOT permitted (`safe_load`).
77
56
  class RailsI18n < Rigor::Plugin::Base
78
57
  manifest(
79
58
  id: "rails-i18n",
80
- # Bumped 2026-06-23 — view template lazy-key scanning
81
- # (`t('.key')` inside ERB / Haml / Slim under
59
+ # Bumped 2026-06-23 — view template lazy-key scanning (`t('.key')` inside ERB / Haml / Slim under
82
60
  # `view_search_paths`).
83
61
  version: "0.3.0",
84
62
  description: "Validates I18n `t(key)` calls against `config/locales/*.yml`.",
@@ -89,12 +67,10 @@ module Rigor
89
67
  }
90
68
  )
91
69
 
92
- # `watch:` covers every `.yml` / `.yaml` file under the locale
93
- # search paths so the cache invalidates when locale files are
94
- # added, removed, or edited (ADR-60 WD3). `@load_errors` is a
95
- # producer-side capture: it is populated only when the block
96
- # runs (a cache miss / a watched file changed), which is exactly
97
- # when a malformed YAML must re-surface.
70
+ # `watch:` covers every `.yml` / `.yaml` file under the locale search paths so the cache invalidates
71
+ # when locale files are added, removed, or edited (ADR-60 WD3). `@load_errors` is a producer-side
72
+ # capture: it is populated only when the block runs (a cache miss / a watched file changed), which is
73
+ # exactly when a malformed YAML must re-surface.
98
74
  producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
99
75
  loader = LocaleLoader.new(
100
76
  io_boundary: io_boundary,
@@ -105,14 +81,12 @@ module Rigor
105
81
  index
106
82
  end
107
83
 
108
- # Scans view templates under `view_search_paths` for lazy
109
- # `t('.key')` / `I18n.translate('.key')` calls and validates
110
- # each expanded key against the locale index. Interpolation
111
- # validation is skipped — the hash may come from controller
112
- # instance variables not visible in the template source.
84
+ # Scans view templates under `view_search_paths` for lazy `t('.key')` / `I18n.translate('.key')`
85
+ # calls and validates each expanded key against the locale index. Interpolation validation is
86
+ # skipped the hash may come from controller instance variables not visible in the template source.
113
87
  #
114
- # Watches `**/*.{erb,haml,slim}` under each search root so
115
- # the cache invalidates when templates are edited.
88
+ # Watches `**/*.{erb,haml,slim}` under each search root so the cache invalidates when templates are
89
+ # edited.
116
90
  producer :view_diagnostics, watch: -> { [[@view_search_paths, "**/*.erb", "**/*.haml", "**/*.slim"]] } do |_params|
117
91
  index = producer_value(:locale_index)
118
92
  next [] if index.nil? || index.empty?
@@ -129,11 +103,10 @@ module Rigor
129
103
  @view_diagnostics_emitted = false
130
104
  end
131
105
 
132
- # File-level only: the once-per-run YAML load errors + the
133
- # runtime (cache-load) error + the view-template scan. Per-call
134
- # `t('key')` validation runs over the engine-owned walk via the
135
- # node_rule below (ADR-37). The locale index and view diagnostics
136
- # are lazily loaded + memoised by `producer_value`.
106
+ # File-level only: the once-per-run YAML load errors + the runtime (cache-load) error + the
107
+ # view-template scan. Per-call `t('key')` validation runs over the engine-owned walk via the
108
+ # node_rule below (ADR-37). The locale index and view diagnostics are lazily loaded + memoised by
109
+ # `producer_value`.
137
110
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
138
111
  index = producer_value(:locale_index)
139
112
  diagnostics = []
@@ -151,9 +124,8 @@ module Rigor
151
124
  diagnostics
152
125
  end
153
126
 
154
- # The lazy-key (`t('.key')`) expansion needs the enclosing method
155
- # (the controller action), supplied by the node-rule NodeContext;
156
- # the controller scope comes from the file path.
127
+ # The lazy-key (`t('.key')`) expansion needs the enclosing method (the controller action), supplied
128
+ # by the node-rule NodeContext; the controller scope comes from the file path.
157
129
  node_rule Prism::CallNode do |node, _scope, path, _fc, context|
158
130
  index = producer_value(:locale_index)
159
131
  next [] if index.nil? || index.empty?
@@ -205,12 +177,10 @@ module Rigor
205
177
  end.sort
206
178
  end
207
179
 
208
- # Diagnostics anchor on a path relative to the working
209
- # directorythe same base `File.expand_path` globbed the
210
- # view files against so view diagnostics render and match
211
- # baselines like every other diagnostic (which carry
212
- # project-relative paths). Falls back to the absolute path
213
- # for a view outside the working tree.
180
+ # Diagnostics anchor on a path relative to the working directory — the same base `File.expand_path`
181
+ # globbed the view files against so view diagnostics render and match baselines like every other
182
+ # diagnostic (which carry project-relative paths). Falls back to the absolute path for a view outside
183
+ # the working tree.
214
184
  def relative_view_path(absolute_path)
215
185
  Pathname.new(absolute_path).relative_path_from(Pathname.pwd).to_s
216
186
  rescue ArgumentError
@@ -232,12 +202,9 @@ module Rigor
232
202
  )
233
203
  end
234
204
 
235
- # The runner only invokes `diagnostics_for_file` for
236
- # Ruby files (`paths:` is filtered to `.rb`). YAML
237
- # parse errors therefore can't be anchored on the
238
- # offending locale file directly; instead, we emit
239
- # them once per run on the first analyzed Ruby file,
240
- # naming the offending YAML path in the message.
205
+ # The runner only invokes `diagnostics_for_file` for Ruby files (`paths:` is filtered to `.rb`). YAML
206
+ # parse errors therefore can't be anchored on the offending locale file directly; instead, we emit
207
+ # them once per run on the first analyzed Ruby file, naming the offending YAML path in the message.
241
208
  def consume_load_error_diagnostics(path)
242
209
  return [] if @load_errors_emitted
243
210
 
@@ -6,18 +6,13 @@ require "rigor/source/literals"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class RailsRoutes < Rigor::Plugin::Base
9
- # Walks a parsed file's AST looking for `*_path` /
10
- # `*_url` calls and validates each against the
11
- # plugin's {HelperTable}. Emits info diagnostics for
12
- # recognised helpers and error diagnostics for typos /
13
- # arity mismatches.
9
+ # Walks a parsed file's AST looking for `*_path` / `*_url` calls and validates each against the
10
+ # plugin's {HelperTable}. Emits info diagnostics for recognised helpers and error diagnostics for
11
+ # typos / arity mismatches.
14
12
  module Analyzer
15
- # Built-in Rails helpers we don't want to flag as
16
- # unknown. The plugin's HelperTable describes
17
- # user-declared routes; Rails (and a small set of
18
- # widely-used asset gems) ship built-in helpers
19
- # (`url_for`, `polymorphic_path`, vite_ruby's
20
- # `vite_asset_path`, …) the plugin deliberately ignores.
13
+ # Built-in Rails helpers we don't want to flag as unknown. The plugin's HelperTable describes
14
+ # user-declared routes; Rails (and a small set of widely-used asset gems) ship built-in helpers
15
+ # (`url_for`, `polymorphic_path`, vite_ruby's `vite_asset_path`, …) the plugin deliberately ignores.
21
16
  BUILTIN_PASSTHROUGH = %w[
22
17
  url_for_path url_for_url
23
18
  polymorphic_path polymorphic_url
@@ -32,35 +27,27 @@ module Rigor
32
27
  audio_path audio_url
33
28
  ].freeze
34
29
 
35
- # One route-helper observation. Carries no path/location — the
36
- # caller (the `node_rule` block) positions it via
37
- # `Plugin::Base#diagnostic`.
30
+ # One route-helper observation. Carries no path/location — the caller (the `node_rule` block)
31
+ # positions it via `Plugin::Base#diagnostic`.
38
32
  Violation = Struct.new(:rule, :severity, :message, keyword_init: true)
39
33
 
40
34
  module_function
41
35
 
42
- # RSpec / Minitest DSL methods that DECLARE a memoized
43
- # local — `let(:foo) { ... }`, `subject(:foo) { ... }`,
44
- # plus the bang and let_it_be variants. The first
45
- # positional Symbol argument is the local name. A bare
46
- # `subject { ... }` (no arg) defines `:subject` itself.
36
+ # RSpec / Minitest DSL methods that DECLARE a memoized local — `let(:foo) { ... }`, `subject(:foo)
37
+ # { ... }`, plus the bang and let_it_be variants. The first positional Symbol argument is the local
38
+ # name. A bare `subject { ... }` (no arg) defines `:subject` itself.
47
39
  SHADOWING_DSL = %i[
48
40
  let let! let_it_be let_it_be!
49
41
  subject subject!
50
42
  ].freeze
51
43
 
52
- # Paths under which `unknown-helper` is suppressed. Route
53
- # helpers are not customarily resolved through these
54
- # directories, so a bare `shared_inbox_url` call inside
55
- # `app/models/account.rb` (the call-site is the
56
- # `accounts.shared_inbox_url` AR column accessor same
57
- # name as a hypothetical route helper) or a `default_url`
58
- # inside `lib/paperclip/url_generator_extensions.rb` (the
59
- # call-site is `Paperclip::UrlGenerator#default_url`, a
60
- # gem-side instance method) would be a false positive.
61
- # The core engine's `call.undefined-method` still catches
62
- # a genuinely unreachable receiver. Cheap path-prefix
63
- # check; no AR-column / gem-include analysis required.
44
+ # Paths under which `unknown-helper` is suppressed. Route helpers are not customarily resolved
45
+ # through these directories, so a bare `shared_inbox_url` call inside `app/models/account.rb` (the
46
+ # call-site is the `accounts.shared_inbox_url` AR column accessor — same name as a hypothetical
47
+ # route helper) or a `default_url` inside `lib/paperclip/url_generator_extensions.rb` (the
48
+ # call-site is `Paperclip::UrlGenerator#default_url`, a gem-side instance method) would be a false
49
+ # positive. The core engine's `call.undefined-method` still catches a genuinely unreachable
50
+ # receiver. Cheap path-prefix check; no AR-column / gem-include analysis required.
64
51
  SKIP_UNKNOWN_HELPER_PATHS = [
65
52
  %r{(?:\A|/)app/models/},
66
53
  %r{(?:\A|/)app/services/},
@@ -78,12 +65,10 @@ module Rigor
78
65
  # @param root [Prism::Node]
79
66
  # @param helper_table [HelperTable]
80
67
  # @return [Array<Diagnostic>]
81
- # The route-helper violations for a single call node (0..2),
82
- # or `[]` when the node is not an implicit `*_path` / `*_url`
83
- # helper call, is shadowed by a same-file binding, or is in a
84
- # suppressed directory. ADR-37: the engine owns the walk; the
85
- # same-file `shadowing` set is built once as the node-rule file
86
- # context.
68
+ # The route-helper violations for a single call node (0..2), or `[]` when the node is not an
69
+ # implicit `*_path` / `*_url` helper call, is shadowed by a same-file binding, or is in a
70
+ # suppressed directory. ADR-37: the engine owns the walk; the same-file `shadowing` set is built
71
+ # once as the node-rule file context.
87
72
  #
88
73
  # @param call_node [Prism::Node]
89
74
  # @param helper_table [HelperTable]
@@ -97,9 +82,8 @@ module Rigor
97
82
  return [] if BUILTIN_PASSTHROUGH.include?(name)
98
83
  return [] if shadowing.include?(name)
99
84
 
100
- # The SKIP set silences both `unknown-helper` and `wrong-arity`:
101
- # a `group_path` inside `app/services/...` is more likely the
102
- # file's own method (an `attr_accessor`) than a route helper.
85
+ # The SKIP set silences both `unknown-helper` and `wrong-arity`: a `group_path` inside
86
+ # `app/services/...` is more likely the file's own method (an `attr_accessor`) than a route helper.
103
87
  suppress_unknown = SKIP_UNKNOWN_HELPER_PATHS.any? { |re| path.match?(re) }
104
88
 
105
89
  entry = helper_table.find(name)
@@ -111,26 +95,21 @@ module Rigor
111
95
  violations << arity if arity
112
96
  violations
113
97
  elsif helper_table.recognised?(name) || suppress_unknown
114
- # Recognised custom / dynamic helper (no arity to check), or
115
- # a directory where helpers aren't customarily resolved.
98
+ # Recognised custom / dynamic helper (no arity to check), or a directory where helpers aren't
99
+ # customarily resolved.
116
100
  []
117
101
  else
118
102
  [unknown_helper_violation(name, helper_table)]
119
103
  end
120
104
  end
121
105
 
122
- # Walks the AST once and returns the Set of names that
123
- # shadow a route helper for this file. Includes:
106
+ # Walks the AST once and returns the Set of names that shadow a route helper for this file. Includes:
124
107
  #
125
- # - `def name` declarations (any level — the
126
- # per-method-scope visibility model Ruby uses means a
127
- # local `def` shadows the helper at every call site
128
- # reachable from where it is defined; we approximate
129
- # "reachable" with "anywhere in the same file").
130
- # - RSpec `let` / `let!` / `let_it_be` / `let_it_be!` /
131
- # `subject` / `subject!` declarations.
132
- # - Local assignments at any scope
133
- # (`foo_url = "..."`).
108
+ # - `def name` declarations (any level — the per-method-scope visibility model Ruby uses means a
109
+ # local `def` shadows the helper at every call site reachable from where it is defined; we
110
+ # approximate "reachable" with "anywhere in the same file").
111
+ # - RSpec `let` / `let!` / `let_it_be` / `let_it_be!` / `subject` / `subject!` declarations.
112
+ # - Local assignments at any scope (`foo_url = "..."`).
134
113
  def collect_shadowing_names(root)
135
114
  names = Set.new
136
115
  Source::NodeWalker.each(root) do |node|
@@ -160,22 +139,17 @@ module Rigor
160
139
  end
161
140
  end
162
141
 
163
- # `*_path` / `*_url` calls without an explicit
164
- # receiver. Calls like `obj.users_path` or
165
- # `Foo::users_path` are NOT route-helper invocations
166
- # in Rails — controllers / views call helpers
142
+ # `*_path` / `*_url` calls without an explicit receiver. Calls like `obj.users_path` or
143
+ # `Foo::users_path` are NOT route-helper invocations in Rails — controllers / views call helpers
167
144
  # implicitly.
168
145
  def implicit_helper_call?(node)
169
146
  node.receiver.nil? && (node.name.to_s.end_with?("_path") || node.name.to_s.end_with?("_url"))
170
147
  end
171
148
 
172
- # Paths where the implicit-params-fill pattern is
173
- # idiomatic controllers / mailers / views routinely
174
- # call `*_path` / `*_url` helpers with fewer args than
175
- # the route's static placeholder count because Rails
176
- # fills `:foo_id` segments from `request.params` at
177
- # runtime (controllers / views) or from the call's
178
- # polymorphic-friendly receiver (mailers).
149
+ # Paths where the implicit-params-fill pattern is idiomatic — controllers / mailers / views
150
+ # routinely call `*_path` / `*_url` helpers with fewer args than the route's static placeholder
151
+ # count because Rails fills `:foo_id` segments from `request.params` at runtime (controllers /
152
+ # views) or from the call's polymorphic-friendly receiver (mailers).
179
153
  IMPLICIT_FILL_PATHS = [
180
154
  %r{(?:\A|/)app/controllers/},
181
155
  %r{(?:\A|/)app/mailers/},
@@ -200,40 +174,29 @@ module Rigor
200
174
  def arity_violation(call_node, entry, helper_table, path)
201
175
  args = call_node.arguments&.arguments || []
202
176
  actual = args.size
203
- # Uncountable nouns (`news` / `series` / `media`) cause
204
- # Rails to register two entries under the same helper
205
- # name `news_path` accepts both arity 0 (index) and
206
- # arity 1 (show). The HelperTable multimap stores both;
207
- # accepts_arity? checks the full set.
177
+ # Uncountable nouns (`news` / `series` / `media`) cause Rails to register two entries under the
178
+ # same helper name `news_path` accepts both arity 0 (index) and arity 1 (show). The HelperTable
179
+ # multimap stores both; accepts_arity? checks the full set.
208
180
  return nil if helper_table.accepts_arity?(entry.name, actual)
209
181
 
210
- # Rails accepts a kwargs-only call shape that supplies
211
- # route segments by name:
182
+ # Rails accepts a kwargs-only call shape that supplies route segments by name:
212
183
  # short_account_status_url(account_username: u, id: i)
213
- # for the route `/@:account_username/:id` (arity 2).
214
- # Our positional-arg count is 1 (the KeywordHashNode),
215
- # so the strict check rejects but Rails would
216
- # resolve every segment from the hash. When the call
217
- # has a trailing KeywordHashNode AND the positional
218
- # count (excluding it) is `<= expected_arity`, accept
219
- # — the kwargs may carry the missing segments.
184
+ # for the route `/@:account_username/:id` (arity 2). Our positional-arg count is 1 (the
185
+ # KeywordHashNode), so the strict check rejects — but Rails would resolve every segment from the
186
+ # hash. When the call has a trailing KeywordHashNode AND the positional count (excluding it) is
187
+ # `<= expected_arity`, accept the kwargs may carry the missing segments.
220
188
  if args.last.is_a?(Prism::KeywordHashNode)
221
189
  positional = actual - 1
222
190
  return nil if helper_table.acceptable_arities(entry.name).any? { |exp| positional <= exp }
223
191
  end
224
192
 
225
- # Underflow tolerance for controllers / mailers /
226
- # views. A `redirect_to namespace_project_milestones_path`
227
- # inside `Projects::MilestonesController` legitimately
228
- # passes 0 args Rails fills the missing `:namespace_id`
229
- # / `:project_id` segments from `request.params` at
230
- # runtime. Mailers reach helpers polymorphically too
231
- # (`project_commit_url(commit)` resolves project from
232
- # commit.project). Silence when `actual < min_arity`
233
- # AND the call site is in a controller / mailer / view
234
- # directory (where the implicit-params-fill pattern is
235
- # idiomatic). Overflow (`actual > max_arity`) still
236
- # fires — that's almost always a typo.
193
+ # Underflow tolerance for controllers / mailers / views. A `redirect_to
194
+ # namespace_project_milestones_path` inside `Projects::MilestonesController` legitimately passes
195
+ # 0 args — Rails fills the missing `:namespace_id` / `:project_id` segments from `request.params`
196
+ # at runtime. Mailers reach helpers polymorphically too (`project_commit_url(commit)` resolves
197
+ # project from commit.project). Silence when `actual < min_arity` AND the call site is in a
198
+ # controller / mailer / view directory (where the implicit-params-fill pattern is idiomatic).
199
+ # Overflow (`actual > max_arity`) still fires — that's almost always a typo.
237
200
  arities_set = helper_table.acceptable_arities(entry.name)
238
201
  min_arity = arities_set.min
239
202
  return nil if actual < min_arity && implicit_fill_path?(path)
@@ -248,8 +211,8 @@ module Rigor
248
211
  end
249
212
 
250
213
  def unknown_helper_violation(name, helper_table)
251
- # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester
252
- # replaces the hand-rolled Levenshtein this module used to carry.
214
+ # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester replaces the hand-rolled
215
+ # Levenshtein this module used to carry.
253
216
  suggestion = Rigor::Plugin::Base.suggest(name, helper_table.names)
254
217
  message = "no route helper `#{name}`"
255
218
  message += " (did you mean `#{suggestion}`?)" if suggestion