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
@@ -5,17 +5,17 @@ require "rigor/plugin"
5
5
  require_relative "rails_routes/helper_table"
6
6
  require_relative "rails_routes/routes_parser"
7
7
  require_relative "rails_routes/helper_discoverer"
8
+ require_relative "rails_routes/grape_api_discoverer"
8
9
  require_relative "rails_routes/analyzer"
9
10
 
10
11
  module Rigor
11
12
  module Plugin
12
- # rigor-rails-routes — validates Rails route-helper calls
13
- # (`users_path`, `edit_user_path(@user)`, …) against the
14
- # project's `config/routes.rb`.
13
+ # rigor-rails-routes — validates Rails route-helper calls (`users_path`, `edit_user_path(@user)`, …)
14
+ # against the project's `config/routes.rb`.
15
15
  #
16
16
  # Tier 1A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
- # Statically interprets the routes DSL via Prism — no
18
- # `rails` runtime dependency. Recognised v0.1.0 surface:
17
+ # Statically interprets the routes DSL via Prism — no `rails` runtime dependency. Recognised v0.1.0
18
+ # surface:
19
19
  #
20
20
  # - `Rails.application.routes.draw do ... end`
21
21
  # - `resources :name [, only: [...] | except: [...]]`
@@ -25,10 +25,8 @@ module Rigor
25
25
  # - One level of `namespace :foo do ... end`
26
26
  # - One level of nested `resources`
27
27
  #
28
- # The plugin publishes its parsed `:helper_table` through
29
- # the ADR-9 cross-plugin fact store; `rigor-actionpack`
30
- # Phase 4 consumes it for route-helper validation in
31
- # controller code.
28
+ # The plugin publishes its parsed `:helper_table` through the ADR-9 cross-plugin fact store;
29
+ # `rigor-actionpack` Phase 4 consumes it for route-helper validation in controller code.
32
30
  #
33
31
  # ## Configuration
34
32
  #
@@ -37,69 +35,63 @@ module Rigor
37
35
  # config:
38
36
  # routes_file: config/routes.rb # default; optional
39
37
  #
38
+ # A project mounting a Grape API keeps its `grape-path-helpers` helpers (`api_v4_groups_badges_path`)
39
+ # out of `unknown-helper`: those names come from grape's runtime route table, so {GrapeApiDiscoverer}
40
+ # recognises the namespace their `prefix` / `version` declarations open rather than enumerating names
41
+ # it cannot know.
42
+ #
40
43
  # ## Limitations (v0.1.0)
41
44
  #
42
- # - `scope :path:` / `scope :module:` / `scope :as:` are
43
- # not interpreted — helpers nested inside these
45
+ # - `scope :path:` / `scope :module:` / `scope :as:` are not interpreted — helpers nested inside these
44
46
  # constructs are silently skipped.
45
- # - Constraints / format restrictions / mountable
46
- # engines are out of scope.
47
- # - The English inflector is intentionally tiny: it
48
- # handles `posts` `post`, `users` `user`,
49
- # `categories` ↔ `category`, `boxes` ↔ `box`. Custom
50
- # inflections (`fish` ↔ `fish`, `child` ↔ `children`)
51
- # are out of scope; users who need them ship a hand-
52
- # written RBS for the affected helper.
47
+ # - Constraints / format restrictions / mountable engines are out of scope.
48
+ # - The English inflector is intentionally tiny: it handles `posts` ↔ `post`, `users` ↔ `user`,
49
+ # `categories` `category`, `boxes` `box`. Custom inflections (`fish` ↔ `fish`, `child` ↔
50
+ # `children`) are out of scope; users who need them ship a hand-written RBS for the affected helper.
53
51
  class RailsRoutes < Rigor::Plugin::Base
54
52
  manifest(
55
53
  id: "rails-routes",
56
- # Bumped 2026-05-28 — GitLab FOSS sweep adds: (a)
57
- # `draw_all :name` support (action_dispatch-draw_all
58
- # gem; single-file load semantics matching `draw :name`);
59
- # (b) keyword-style `scope(path: ':project_id',
60
- # as: :project)` path read from the `:path` keyword,
61
- # not only from the positional first arg; (c) detection
62
- # of iterative `direct(name.sub(FROM, TO)) do ... end`
63
- # alias-generation idiom generates `<TO>_*` aliases
64
- # for every registered `<FROM>_*` helper. GitLab uses
65
- # this to shorten `namespace_project_*` → `project_*`.
66
- version: "0.27.0",
54
+ # Bumped 2026-05-28 — GitLab FOSS sweep adds: (a) `draw_all :name` support (action_dispatch-draw_all
55
+ # gem; single-file load semantics matching `draw :name`); (b) keyword-style `scope(path:
56
+ # ':project_id', as: :project)` path read from the `:path` keyword, not only from the positional
57
+ # first arg; (c) detection of iterative `direct(name.sub(FROM, TO)) do ... end` alias-generation
58
+ # idiomgenerates `<TO>_*` aliases for every registered `<FROM>_*` helper. GitLab uses this to
59
+ # shorten `namespace_project_*` `project_*`. Bumped 2026-07-10 recognises the open helper
60
+ # namespace `grape-path-helpers` generates (`api_v4_*_path`), grounded in the project's own Grape
61
+ # `prefix` / `version` declarations (see {GrapeApiDiscoverer}).
62
+ version: "0.28.0",
67
63
  description: "Validates Rails route-helper calls against `config/routes.rb`.",
68
64
  config_schema: {
69
65
  "routes_file" => { kind: :string, default: "config/routes.rb" },
70
- # `helper_paths` — the directories `HelperDiscoverer` walks
71
- # for project-defined `*_path` / `*_url` methods. Default to
72
- # the whole `app/` tree the suffix filter inside the
73
- # discoverer keeps the registered set tight, and real-world
74
- # Rails apps routinely keep URL builders under
75
- # `app/controllers` (private `def page_url`, `def callback_url`
76
- # shapes), `app/lib` (Mastodon's
77
- # `TranslationService::DeepL#base_url`), `app/services`
78
- # (`SoftwareUpdateCheckService#api_url`), `app/serializers`,
79
- # `app/presenters`, `app/decorators`, not only `app/helpers/`.
80
- # Walking the whole tree is the honest answer to "does this
81
- # `_path` / `_url` name exist anywhere in the project?"; the
82
- # cost is a one-time Prism parse per file at startup, which is
83
- # bounded.
84
- "helper_paths" => { kind: :array, default: ["app"] }
66
+ # `helper_paths` — the directories `HelperDiscoverer` walks for project-defined `*_path` / `*_url`
67
+ # methods. Default to the whole `app/` tree the suffix filter inside the discoverer keeps the
68
+ # registered set tight, and real-world Rails apps routinely keep URL builders under
69
+ # `app/controllers` (private `def page_url`, `def callback_url` shapes), `app/lib` (Mastodon's
70
+ # `TranslationService::DeepL#base_url`), `app/services` (`SoftwareUpdateCheckService#api_url`),
71
+ # `app/serializers`, `app/presenters`, `app/decorators`, not only `app/helpers/`. Walking the
72
+ # whole tree is the honest answer to "does this `_path` / `_url` name exist anywhere in the
73
+ # project?"; the cost is a one-time Prism parse per file at startup, which is bounded.
74
+ "helper_paths" => { kind: :array, default: ["app"] },
75
+ # `grape_api_paths` the directories scanned for Grape API classes, whose `prefix` / `version`
76
+ # declarations ground the open `api_v4_*_path` helper namespace `grape-path-helpers` generates
77
+ # (see {GrapeApiDiscoverer}). Defaults cover the two conventional homes; a project with no grape
78
+ # API declares no prefixes and nothing changes for it.
79
+ "grape_api_paths" => { kind: :array, default: ["lib/api", "app/api"] }
85
80
  },
86
81
  produces: [:helper_table]
87
82
  )
88
83
 
89
- # Cached producer — reads `config/routes.rb` through
90
- # the trusted `IoBoundary` and parses through
91
- # {RoutesParser}. The descriptor's auto-collected
92
- # `FileEntry` digest invalidates the cache on routes-
93
- # file edits.
84
+ # Cached producer — reads `config/routes.rb` through the trusted `IoBoundary` and parses through
85
+ # {RoutesParser}. The descriptor's auto-collected `FileEntry` digest invalidates the cache on
86
+ # routes-file edits.
94
87
  #
95
- # Passes a `file_reader` lambda so the parser can follow
96
- # `draw(:admin)` `config/routes/admin.rb` partials.
97
- # `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:`
98
- # so ADDING a helper file invalidates the table the producer's
99
- # own in-block reads (the routes file + the partials it follows
100
- # via `draw`) are captured post-compute, but a brand-new helper
101
- # file the prior run never read would otherwise read as fresh.
102
- producer :helper_table, watch: -> { @helper_paths.map { |dir| [dir, "**/*.rb"] } } do |_params|
88
+ # Passes a `file_reader` lambda so the parser can follow `draw(:admin)` → `config/routes/admin.rb`
89
+ # partials. `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:` and `grape_api_paths:` so
90
+ # ADDING a helper file or a Grape API whose `version` declaration opens a new namespace —
91
+ # invalidates the table. The producer's own in-block reads (the routes file + the partials it follows
92
+ # via `draw`) are captured post-compute, but a brand-new file the prior run never read would otherwise
93
+ # read as fresh.
94
+ producer :helper_table, watch: -> { (@helper_paths + @grape_api_paths).map { |dir| [dir, "**/*.rb"] } } do |_p|
103
95
  routes_dir = "#{File.dirname(@routes_file)}/routes"
104
96
  file_reader = lambda do |name|
105
97
  io_boundary.read_file("#{routes_dir}/#{name}")
@@ -107,26 +99,24 @@ module Rigor
107
99
  nil
108
100
  end
109
101
  contents = io_boundary.read_file(@routes_file)
110
- custom_helpers = discover_custom_helpers
111
- RoutesParser.parse(contents, file_reader: file_reader, custom_helpers: custom_helpers)
102
+ RoutesParser.parse(contents, file_reader: file_reader, custom_helpers: discover_custom_helpers,
103
+ grape_prefixes: discover_grape_prefixes)
112
104
  end
113
105
 
114
106
  def init(_services)
115
107
  @routes_file = config.fetch("routes_file")
116
108
  @helper_paths = Array(config.fetch("helper_paths")).map(&:to_s)
109
+ @grape_api_paths = Array(config.fetch("grape_api_paths")).map(&:to_s)
117
110
  @helper_table = nil
118
111
  @helper_table_built = false
119
112
  @load_error = nil
120
113
  end
121
114
 
122
- # Walks every configured `helper_paths:` directory
123
- # through the trusted `IoBoundary` and returns the set
124
- # of project-defined `*_path` / `*_url` method names
125
- # for {HelperDiscoverer}. Each file digest is captured
126
- # by the boundary so editing a helper file invalidates
127
- # the `:helper_table` cache automatically. Returns the
128
- # empty set when nothing under `helper_paths:` exists —
129
- # the routes table still works.
115
+ # Walks every configured `helper_paths:` directory through the trusted `IoBoundary` and returns the
116
+ # set of project-defined `*_path` / `*_url` method names for {HelperDiscoverer}. Each file digest is
117
+ # captured by the boundary so editing a helper file invalidates the `:helper_table` cache
118
+ # automatically. Returns the empty set when nothing under `helper_paths:` exists — the routes table
119
+ # still works.
130
120
  def discover_custom_helpers
131
121
  contents_per_path = {}
132
122
  each_helper_file do |path|
@@ -137,8 +127,23 @@ module Rigor
137
127
  HelperDiscoverer.discover(contents_per_path)
138
128
  end
139
129
 
140
- def each_helper_file(&)
141
- @helper_paths.each do |dir|
130
+ # Walks `grape_api_paths:` through the trusted `IoBoundary` and returns the helper-name prefixes the
131
+ # project's Grape API classes generate (`["api_v3", "api_v4"]`). Empty for a project with no grape API,
132
+ # which leaves `unknown-helper` behaviour exactly as it was.
133
+ def discover_grape_prefixes
134
+ contents_per_path = {}
135
+ each_ruby_file(@grape_api_paths) do |path|
136
+ contents_per_path[path] = io_boundary.read_file(path)
137
+ rescue Plugin::AccessDeniedError, Errno::ENOENT
138
+ next
139
+ end
140
+ GrapeApiDiscoverer.discover(contents_per_path)
141
+ end
142
+
143
+ def each_helper_file(&) = each_ruby_file(@helper_paths, &)
144
+
145
+ def each_ruby_file(dirs, &)
146
+ dirs.each do |dir|
142
147
  absolute = File.expand_path(dir)
143
148
  next unless File.directory?(absolute)
144
149
 
@@ -146,8 +151,7 @@ module Rigor
146
151
  end
147
152
  end
148
153
 
149
- # Publishes the parsed table to the cross-plugin fact
150
- # store; `rigor-actionpack` Phase 4 reads it via
154
+ # Publishes the parsed table to the cross-plugin fact store; `rigor-actionpack` Phase 4 reads it via
151
155
  # `services.fact_store.read`.
152
156
  def prepare(services)
153
157
  table = helper_table_or_nil
@@ -160,10 +164,9 @@ module Rigor
160
164
  )
161
165
  end
162
166
 
163
- # File-level only: the once-per-run load-error emission. Per-call
164
- # helper validation runs over the engine-owned walk via the
165
- # node_rule below (ADR-37), with the same-file `shadowing` set
166
- # built once as the node-rule file context.
167
+ # File-level only: the once-per-run load-error emission. Per-call helper validation runs over the
168
+ # engine-owned walk via the node_rule below (ADR-37), with the same-file `shadowing` set built once
169
+ # as the node-rule file context.
167
170
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
168
171
  table = helper_table_or_nil
169
172
  if table.nil? && @load_error
@@ -191,31 +194,23 @@ module Rigor
191
194
 
192
195
  private
193
196
 
194
- # The load-error path used to emit the same warning on
195
- # every analyzed file in the project. On large monorepos
196
- # (Mastodon: 1,302 files; Solidus: ~1,000 files) and on
197
- # legacy projects without a top-level `config/routes.rb`,
198
- # this multiplied a single root cause into 1,000+
199
- # identical diagnostics. The error is project-global —
200
- # report it once per run.
197
+ # The load-error path used to emit the same warning on every analyzed file in the project. On large
198
+ # monorepos (Mastodon: 1,302 files; Solidus: ~1,000 files) and on legacy projects without a top-level
199
+ # `config/routes.rb`, this multiplied a single root cause into 1,000+ identical diagnostics. The
200
+ # error is project-global report it once per run.
201
201
 
202
202
  def helper_table_or_nil
203
- # Memoise the build *attempt*, not just a truthy result: when the
204
- # routes file is missing or unparseable the table is nil, and the
205
- # old `return @helper_table if @helper_table` re-ran the whole
206
- # read-and-parse on every call. The engine consults this per
207
- # route-helper dispatch, so a nil table meant re-reading (and, on
208
- # a real project, re-parsing) `config/routes.rb` tens of thousands
209
- # of times per run. The result — nil included — is stable for the
210
- # run, so one attempt is correct.
203
+ # Memoise the build *attempt*, not just a truthy result: when the routes file is missing or
204
+ # unparseable the table is nil, and the old `return @helper_table if @helper_table` re-ran the
205
+ # whole read-and-parse on every call. The engine consults this per route-helper dispatch, so a nil
206
+ # table meant re-reading (and, on a real project, re-parsing) `config/routes.rb` tens of thousands
207
+ # of times per run. The result — nil included is stable for the run, so one attempt is correct.
211
208
  return @helper_table if @helper_table_built
212
209
 
213
210
  @helper_table_built = true
214
- # ADR-60 WD3 record-and-validate: the producer's in-block reads
215
- # (the routes file + the partials it follows) are captured into
216
- # the dependency descriptor AFTER the block runs, and the
217
- # producer's `watch:` covers helper-file additions — so no
218
- # priming read is needed here.
211
+ # ADR-60 WD3 record-and-validate: the producer's in-block reads (the routes file + the partials it
212
+ # follows) are captured into the dependency descriptor AFTER the block runs, and the producer's
213
+ # `watch:` covers helper-file additions so no priming read is needed here.
219
214
  @helper_table = cache_for(:helper_table, params: {}).call
220
215
  rescue Plugin::AccessDeniedError => e
221
216
  @load_error = "rigor-rails-routes: #{e.message}"
@@ -4,24 +4,18 @@ require "rigor/plugin"
4
4
 
5
5
  # ADR-32 — bundled `rigor-rbs-inline` plugin.
6
6
  #
7
- # Synthesises RBS from project Ruby files that carry
8
- # rbs-inline-shaped comments (`#: () -> T`, `# @rbs name: T`,
9
- # `# @rbs return: T`, attribute `#:`, …) and contributes the
10
- # result to the analysis environment through the
11
- # `source_rbs_synthesizer:` manifest hook.
7
+ # Synthesises RBS from project Ruby files that carry rbs-inline-shaped comments (`#: () -> T`, `# @rbs
8
+ # name: T`, `# @rbs return: T`, attribute `#:`, …) and contributes the result to the analysis environment
9
+ # through the `source_rbs_synthesizer:` manifest hook.
12
10
  #
13
- # By default (WD2) only files starting with the upstream
14
- # `# rbs_inline: enabled` magic comment are processed; a host
15
- # context can flip this off by setting `require_magic_comment:
16
- # false` in the plugin config (WD10).
11
+ # By default (WD2) only files starting with the upstream `# rbs_inline: enabled` magic comment are
12
+ # processed; a host context can flip this off by setting `require_magic_comment: false` in the plugin
13
+ # config (WD10).
17
14
  module Rigor
18
15
  module Plugin
19
- # The plugin gem requires `rbs/inline` at load time; without
20
- # the upstream library the synthesizer can't do its job.
21
- # Wrapped in a begin/rescue so the analyzer still loads if
22
- # the user activated this plugin without installing the
23
- # `rbs-inline` gem (loud-on-activation, fail-soft to no
24
- # contribution).
16
+ # The plugin gem requires `rbs/inline` at load time; without the upstream library the synthesizer can't
17
+ # do its job. Wrapped in a begin/rescue so the analyzer still loads if the user activated this plugin
18
+ # without installing the `rbs-inline` gem (loud-on-activation, fail-soft to no contribution).
25
19
  begin
26
20
  require "prism"
27
21
  require "rbs/inline"
@@ -37,18 +31,13 @@ module Rigor
37
31
  end
38
32
 
39
33
  class RbsInline < Rigor::Plugin::Base
40
- # Synthesizer callable invoked once per project Ruby
41
- # source file by `Environment.for_project` at env-build
42
- # time. Returns the synthesised RBS source as a String,
43
- # or `nil` when the file contributes nothing (no magic
44
- # comment in the default mode, empty annotation set,
45
- # parse error per WD6).
34
+ # Synthesizer callable invoked once per project Ruby source file by `Environment.for_project` at
35
+ # env-build time. Returns the synthesised RBS source as a String, or `nil` when the file contributes
36
+ # nothing (no magic comment in the default mode, empty annotation set, parse error per WD6).
46
37
  class Synthesizer
47
- # @param require_magic_comment [Boolean] when `true`
48
- # (the default, WD2), only files with
49
- # `# rbs_inline: enabled` at the top are processed.
50
- # When `false` (WD10 host-context override), every
51
- # file is treated as if it carried the magic comment.
38
+ # @param require_magic_comment [Boolean] when `true` (the default, WD2), only files with
39
+ # `# rbs_inline: enabled` at the top are processed. When `false` (WD10 host-context override),
40
+ # every file is treated as if it carried the magic comment.
52
41
  def initialize(require_magic_comment:)
53
42
  @require_magic_comment = require_magic_comment
54
43
  freeze
@@ -57,8 +46,7 @@ module Rigor
57
46
  # Return value contract:
58
47
  # - `String` (non-empty) → successful synthesis
59
48
  # - `nil` → no contribution
60
- # - `[:error, message_string]` → parse failed, surface
61
- # info diagnostic per ADR-32 WD6
49
+ # - `[:error, message_string]` → parse failed, surface info diagnostic per ADR-32 WD6
62
50
  def call(source_file_path)
63
51
  return nil unless RBS_INLINE_AVAILABLE
64
52
  return nil unless File.file?(source_file_path)
@@ -67,10 +55,8 @@ module Rigor
67
55
  return nil if source.empty?
68
56
 
69
57
  result = ::Prism.parse(source)
70
- # `opt_in: true` is rbs-inline's "require the magic
71
- # comment" mode (per upstream parser.rb:62). The
72
- # plugin's `require_magic_comment:` config knob maps
73
- # directly onto it.
58
+ # `opt_in: true` is rbs-inline's "require the magic comment" mode (per upstream parser.rb:62).
59
+ # The plugin's `require_magic_comment:` config knob maps directly onto it.
74
60
  parsed = ::RBS::Inline::Parser.parse(result, opt_in: @require_magic_comment)
75
61
  return nil if parsed.nil?
76
62
 
@@ -80,11 +66,9 @@ module Rigor
80
66
 
81
67
  rendered
82
68
  rescue ::StandardError => e
83
- # WD6 fail-soft — surface a structured error tuple so
84
- # the engine's `Environment.for_project` can emit a
85
- # `source-rbs-synthesis-failed` info diagnostic
86
- # naming the file + the upstream error message,
87
- # without crashing analysis.
69
+ # WD6 fail-soft — surface a structured error tuple so the engine's `Environment.for_project` can
70
+ # emit a `source-rbs-synthesis-failed` info diagnostic naming the file + the upstream error
71
+ # message, without crashing analysis.
88
72
  [:error, "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}"]
89
73
  end
90
74
  end
@@ -98,24 +82,19 @@ module Rigor
98
82
  source_rbs_synthesizer: nil # set per-instance below
99
83
  )
100
84
 
101
- # Per-instance synthesizer — built from the manifest's
102
- # default + the project's plugin config. The manifest
103
- # `source_rbs_synthesizer:` is nil at the class level so
104
- # the registry sees the instance's override (returned by
105
- # `#manifest`, which `Plugin::Registry#source_rbs_synthesizers`
106
- # consults via `plugin.manifest.source_rbs_synthesizer`).
85
+ # Per-instance synthesizer — built from the manifest's default + the project's plugin config. The
86
+ # manifest `source_rbs_synthesizer:` is nil at the class level so the registry sees the instance's
87
+ # override (returned by `#manifest`, which `Plugin::Registry#source_rbs_synthesizers` consults via
88
+ # `plugin.manifest.source_rbs_synthesizer`).
107
89
  #
108
- # ADR-32 WD10 — `require_magic_comment` defaults to
109
- # `true`. Setting it to `false` in `.rigor.yml` flips the
110
- # synthesizer into "process every file" mode.
90
+ # ADR-32 WD10 — `require_magic_comment` defaults to `true`. Setting it to `false` in `.rigor.yml`
91
+ # flips the synthesizer into "process every file" mode.
111
92
  def initialize(services:, config: {})
112
93
  super
113
94
  @require_magic_comment = config.fetch("require_magic_comment", true) ? true : false
114
95
  @synthesizer = Synthesizer.new(require_magic_comment: @require_magic_comment)
115
- # Build the per-instance manifest eagerly (before
116
- # `freeze`) so the registry's repeated reads return
117
- # the same object and we don't need to mutate a
118
- # frozen instance later.
96
+ # Build the per-instance manifest eagerly (before `freeze`) so the registry's repeated reads
97
+ # return the same object and we don't need to mutate a frozen instance later.
119
98
  base = self.class.manifest
120
99
  @manifest_with_synth = build_manifest_with_synthesizer(base)
121
100
  freeze
@@ -123,10 +102,9 @@ module Rigor
123
102
 
124
103
  attr_reader :synthesizer
125
104
 
126
- # Override the manifest-level `source_rbs_synthesizer:`
127
- # (which is nil at the class level) with the per-instance
128
- # synthesizer built from the merged config. The registry
129
- # reads this through `plugin.manifest.source_rbs_synthesizer`.
105
+ # Override the manifest-level `source_rbs_synthesizer:` (which is nil at the class level) with the
106
+ # per-instance synthesizer built from the merged config. The registry reads this through
107
+ # `plugin.manifest.source_rbs_synthesizer`.
130
108
  def manifest
131
109
  @manifest_with_synth
132
110
  end
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rigor-rbs-inline — ingests rbs-inline-shaped comments
4
- # (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`, attribute
5
- # `#:`, ivars, generics, override, …) as RBS contributions to
6
- # the analysis environment.
3
+ # rigor-rbs-inline — ingests rbs-inline-shaped comments (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`,
4
+ # attribute `#:`, ivars, generics, override, ) as RBS contributions to the analysis environment.
7
5
  #
8
- # Per ADR-32 the plugin gates per file on the upstream
9
- # `# rbs_inline: enabled` magic comment by default. A host
10
- # context that owns the entire analysis scope (e.g. the
11
- # ADR-29 browser playground) can set `require_magic_comment:
12
- # false` in the plugin config to skip the gate — every file
13
- # the synthesizer sees is treated as if it carried the magic
14
- # comment.
6
+ # Per ADR-32 the plugin gates per file on the upstream `# rbs_inline: enabled` magic comment by default. A
7
+ # host context that owns the entire analysis scope (e.g. the ADR-29 browser playground) can set
8
+ # `require_magic_comment: false` in the plugin config to skip the gate — every file the synthesizer sees is
9
+ # treated as if it carried the magic comment.
15
10
  #
16
11
  # # .rigor.yml
17
12
  # plugins:
@@ -19,6 +14,6 @@
19
14
  # config:
20
15
  # require_magic_comment: false # default true
21
16
  #
22
- # The plugin depends on the upstream `rbs-inline` gem; Rigor's
23
- # core `rigortype` gemspec stays zero-dep per ADR-0 / ADR-32 WD1.
17
+ # The plugin depends on the upstream `rbs-inline` gem; Rigor's core `rigortype` gemspec stays zero-dep per
18
+ # ADR-0 / ADR-32 WD1.
24
19
  require_relative "rigor/plugin/rbs_inline"
@@ -9,16 +9,12 @@ module Rigor
9
9
  class Rspec < Rigor::Plugin::Base
10
10
  # Per-file walker that:
11
11
  #
12
- # 1. Collects every RSpec scope (each `RSpec.describe`
13
- # plus its nested `describe` / `context` blocks)
12
+ # 1. Collects every RSpec scope (each `RSpec.describe` plus its nested `describe` / `context` blocks)
14
13
  # via {ScopeWalker}.
15
- # 2. Reports duplicate `let(:name)` / `subject(:name)`
16
- # declarations within the same scope (the second
17
- # declaration wins at runtimean easy
18
- # copy-paste bug).
19
- # 3. Reports recursive self-references —
20
- # `let(:user) { user.something }` will infinite-loop
21
- # at runtime — an easy oversight.
14
+ # 2. Reports duplicate `let(:name)` / `subject(:name)` declarations within the same scope (the second
15
+ # declaration wins at runtime an easy copy-paste bug).
16
+ # 3. Reports recursive self-references `let(:user) { user.something }` will infinite-loop at
17
+ # runtime — an easy oversight.
22
18
  module Analyzer
23
19
  Diagnostic = Struct.new(:path, :line, :column, :severity, :rule, :message, keyword_init: true)
24
20
 
@@ -49,11 +45,9 @@ module Rigor
49
45
  end
50
46
 
51
47
  def duplicate_diagnostics_for(path, name, decls)
52
- # Report each subsequent occurrence; the first
53
- # one is the "winner" only by literal source
54
- # order, but RSpec lets the LAST declaration win
55
- # at runtime, so flag everything after the first
56
- # so the user can see the full list.
48
+ # Report each subsequent occurrence; the first one is the "winner" only by literal source order,
49
+ # but RSpec lets the LAST declaration win at runtime, so flag everything after the first so the
50
+ # user can see the full list.
57
51
  decls.drop(1).map do |decl|
58
52
  Diagnostic.new(
59
53
  path: path,
@@ -76,8 +70,7 @@ module Rigor
76
70
  end
77
71
  end
78
72
 
79
- # Walks the declaration's block body looking for a
80
- # call to its own name with no explicit receiver.
73
+ # Walks the declaration's block body looking for a call to its own name with no explicit receiver.
81
74
  # Returns true if at least one such call exists.
82
75
  def self_references?(decl)
83
76
  body = decl.block_node&.body
@@ -8,22 +8,17 @@ require_relative "scope_walker"
8
8
  module Rigor
9
9
  module Plugin
10
10
  class Rspec < Rigor::Plugin::Base
11
- # Per-file index of RSpec scope hierarchy. Each
12
- # describe scope carries:
11
+ # Per-file index of RSpec scope hierarchy. Each describe scope carries:
13
12
  #
14
- # - `outer_range`: `Range[Integer]` of line numbers
15
- # covered by the describe's block body (start_line..end_line).
16
- # - `describe_const`: the model constant string named in
17
- # `RSpec.describe SomeModel do` / `describe SomeModel do`,
18
- # or `nil` when the describe's first arg is a String
19
- # (`describe ".foo"`).
20
- # - `lets`: `Hash{Symbol => Prism::BlockNode}` of `let(:name)
21
- # { ... }` and `subject(:name) { ... }` declarations.
22
- # `:subject` is the key for the implicit `subject { ... }`.
13
+ # - `outer_range`: `Range[Integer]` of line numbers covered by the describe's block body
14
+ # (start_line..end_line).
15
+ # - `describe_const`: the model constant string named in `RSpec.describe SomeModel do` / `describe
16
+ # SomeModel do`, or `nil` when the describe's first arg is a String (`describe ".foo"`).
17
+ # - `lets`: `Hash{Symbol => Prism::BlockNode}` of `let(:name) { ... }` and `subject(:name) { ... }`
18
+ # declarations. `:subject` is the key for the implicit `subject { ... }`.
23
19
  #
24
- # Used by the plugin's let-binding `dynamic_return`
25
- # rule to bind `let`-named method-shape calls inside
26
- # `it` bodies to the let block's inferred type.
20
+ # Used by the plugin's let-binding `dynamic_return` rule to bind `let`-named method-shape calls
21
+ # inside `it` bodies to the let block's inferred type.
27
22
  class LetScopeIndex
28
23
  Record = Struct.new(:outer_range, :describe_const, :lets, keyword_init: true) do
29
24
  def contains?(line) = outer_range.cover?(line)
@@ -38,27 +33,22 @@ module Rigor
38
33
  freeze
39
34
  end
40
35
 
41
- # Returns every record whose outer_range contains the
42
- # given line, ordered from outermost to innermost. The
43
- # innermost wins on `let` name collisions per Ruby's
44
- # method-resolution order (RSpec nested `let` shadows
45
- # the outer `let`).
36
+ # Returns every record whose outer_range contains the given line, ordered from outermost to
37
+ # innermost. The innermost wins on `let` name collisions per Ruby's method-resolution order (RSpec
38
+ # nested `let` shadows the outer `let`).
46
39
  def records_at(line)
47
40
  @records.select { |rec| rec.contains?(line) }
48
41
  end
49
42
 
50
- # ADR-52 slice 5a — every `let` / `subject` name declared
51
- # anywhere in the file, across all describe scopes. Feeds the
52
- # plugin's `dynamic_return file_methods:` gate: the engine only
53
- # consults the rule for a call whose name appears here; the
54
- # precise line-scoped resolution stays in `let_block_at`.
43
+ # ADR-52 slice 5a — every `let` / `subject` name declared anywhere in the file, across all describe
44
+ # scopes. Feeds the plugin's `dynamic_return file_methods:` gate: the engine only consults the rule
45
+ # for a call whose name appears here; the precise line-scoped resolution stays in `let_block_at`.
55
46
  # @return [Array<Symbol>]
56
47
  def let_names
57
48
  @records.flat_map { |rec| rec.lets.keys }.uniq
58
49
  end
59
50
 
60
- # Resolves a `let` name at the given line by walking
61
- # records innermost to outermost.
51
+ # Resolves a `let` name at the given line by walking records innermost to outermost.
62
52
  # @return [Prism::BlockNode, nil]
63
53
  def let_block_at(line, name)
64
54
  name_sym = name.to_sym
@@ -68,9 +58,8 @@ module Rigor
68
58
  nil
69
59
  end
70
60
 
71
- # Resolves the `describe`-anchor constant name at the
72
- # given line. The innermost describe with a constant
73
- # anchor wins; describe-with-String anchors are skipped.
61
+ # Resolves the `describe`-anchor constant name at the given line. The innermost describe with a
62
+ # constant anchor wins; describe-with-String anchors are skipped.
74
63
  # @return [String, nil]
75
64
  def describe_const_at(line)
76
65
  records_at(line).reverse.each do |rec|
@@ -92,9 +81,8 @@ module Rigor
92
81
  if describe_call?(node)
93
82
  record = build_record(node, anchor)
94
83
  accumulator << record
95
- # `describe_call?` already guarantees a `Prism::CallNode`; the
96
- # explicit `is_a?` re-states it so the analyzer narrows `node`
97
- # from `Prism::Node` and resolves `#block` (a CallNode method).
84
+ # `describe_call?` already guarantees a `Prism::CallNode`; the explicit `is_a?` re-states it so
85
+ # the analyzer narrows `node` from `Prism::Node` and resolves `#block` (a CallNode method).
98
86
  if node.is_a?(Prism::CallNode) && node.block&.body
99
87
  collect(node.block.body, anchor: record.describe_const || anchor, accumulator: accumulator)
100
88
  end
@@ -123,8 +111,7 @@ module Rigor
123
111
  return false unless ScopeWalker::SCOPE_METHODS.include?(node.name) || node.name == :describe
124
112
  return false unless node.block.is_a?(Prism::BlockNode)
125
113
 
126
- # `RSpec.describe ...` has receiver = RSpec; bare
127
- # `describe ... do` has nil receiver. Accept both.
114
+ # `RSpec.describe ...` has receiver = RSpec; bare `describe ... do` has nil receiver. Accept both.
128
115
  node.receiver.nil? || describe_receiver?(node.receiver)
129
116
  end
130
117
 
@@ -158,11 +145,9 @@ module Rigor
158
145
  end
159
146
  end
160
147
 
161
- # Walks the describe's body shallowly: collects every
162
- # top-level `let(:name) { ... }` / `subject(:name) { ... }`
163
- # / `subject { ... }` declaration. Does NOT recurse into
164
- # nested describe / context blocks — those have their
165
- # own record built by `collect` above.
148
+ # Walks the describe's body shallowly: collects every top-level `let(:name) { ... }` /
149
+ # `subject(:name) { ... }` / `subject { ... }` declaration. Does NOT recurse into nested describe /
150
+ # context blocks those have their own record built by `collect` above.
166
151
  def self.collect_lets(body)
167
152
  return {} unless body.is_a?(Prism::Node)
168
153