rigortype 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -7,49 +7,40 @@ require "rigor/source/literals"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class Actionpack < Rigor::Plugin::Base
10
- # Per-node validators for the Action Pack DSL inside controller
11
- # files. ADR-37: the engine owns the single AST walk (the plugin
12
- # declares `node_rule(Prism::CallNode)`), so each method here
13
- # validates ONE call node and returns location-bearing
14
- # {Violation}s the `node_rule` block positions them via
15
- # `Plugin::Base#diagnostic`. The enclosing-controller context the
16
- # filter / render phases need is read from the node-rule
17
- # `NodeContext` ancestors rather than re-derived by a forward walk.
10
+ # Per-node validators for the Action Pack DSL inside controller files. ADR-37: the engine owns the
11
+ # single AST walk (the plugin declares `node_rule(Prism::CallNode)`), so each method here
12
+ # validates ONE call node and returns location-bearing {Violation}s — the `node_rule` block
13
+ # positions them via `Plugin::Base#diagnostic`. The enclosing-controller context the filter /
14
+ # render phases need is read from the node-rule `NodeContext` ancestors rather than re-derived by
15
+ # a forward walk.
18
16
  #
19
17
  # The recogniser keys on call-method-name suffix:
20
18
  #
21
19
  # - `users_path`, `edit_user_path(@user)` → `_path` family.
22
20
  # - `users_url`, `edit_user_url(@user)` → `_url` family.
23
21
  #
24
- # Any call whose name doesn't end in `_path` / `_url` is
25
- # silently passed through. Calls with an explicit non-self
26
- # receiver (`other_helper.users_path`) are also skipped
27
- # the helper is implicit-self in real controllers, and a
28
- # custom-receiver call is almost certainly someone's own
22
+ # Any call whose name doesn't end in `_path` / `_url` is silently passed through. Calls with an
23
+ # explicit non-self receiver (`other_helper.users_path`) are also skipped the helper is
24
+ # implicit-self in real controllers, and a custom-receiver call is almost certainly someone's own
29
25
  # method that happens to share the suffix.
30
26
  module Analyzer
31
27
  SUFFIXES = %w[_path _url].freeze
32
28
 
33
- # Phase 2 — filter-chain DSL methods. Each takes a
34
- # variadic list of filter names (Symbols / Strings) plus
35
- # optional `only:` / `except:` / `if:` / `unless:`
36
- # modifiers. Only the filter NAMES are validated; the
37
- # `only:`/`except:` action-name arguments are not (deferred).
29
+ # Phase 2 — filter-chain DSL methods. Each takes a variadic list of filter names (Symbols /
30
+ # Strings) plus optional `only:` / `except:` / `if:` / `unless:` modifiers. Only the filter
31
+ # NAMES are validated; the `only:`/`except:` action-name arguments are not (deferred).
38
32
  FILTER_DSL_METHODS = %i[
39
33
  before_action after_action around_action
40
34
  skip_before_action skip_after_action skip_around_action
41
35
  prepend_before_action prepend_after_action prepend_around_action
42
36
  ].freeze
43
37
 
44
- # Phase 3 — render-target template extensions checked in
45
- # priority order. Covers the engines used by surveyed
46
- # projects: ERB (Rails default `.html.erb`, `.text.erb`),
47
- # HAML (Mastodon, Solidus admin `.html.haml`), Slim, and
48
- # JSON (`.json.jbuilder` plus `.json.erb` for hand-rolled API
49
- # responses). When a template exists under any of these
50
- # extensions, the missing-template diagnostic stays silent.
51
- # A configurable extension list is deferred; this set is wide
52
- # enough to cover surveyed real-world projects without FPs.
38
+ # Phase 3 — render-target template extensions checked in priority order. Covers the engines used
39
+ # by surveyed projects: ERB (Rails default `.html.erb`, `.text.erb`), HAML (Mastodon, Solidus
40
+ # admin `.html.haml`), Slim, and JSON (`.json.jbuilder` plus `.json.erb` for hand-rolled API
41
+ # responses). When a template exists under any of these extensions, the missing-template
42
+ # diagnostic stays silent. A configurable extension list is deferred; this set is wide enough to
43
+ # cover surveyed real-world projects without FPs.
53
44
  RENDER_TEMPLATE_EXTENSIONS = %w[
54
45
  .html.erb
55
46
  .text.erb
@@ -62,39 +53,31 @@ module Rigor
62
53
  .xml.erb
63
54
  ].freeze
64
55
 
65
- # Phase 1 — strong-parameter call shapes that begin a
66
- # validatable chain. The walker matches the
67
- # `params.require(:user).permit(:name, :email)` chain
68
- # by looking for `:permit` call sites whose receiver is
69
- # `params.require(:symbol)`.
56
+ # Phase 1 — strong-parameter call shapes that begin a validatable chain. The walker matches the
57
+ # `params.require(:user).permit(:name, :email)` chain by looking for `:permit` call sites whose
58
+ # receiver is `params.require(:symbol)`.
70
59
  STRONG_PARAMS_RECEIVER_NAMES = %i[require permit_params strong_params].freeze
71
60
 
72
- # One Action Pack observation. Carries no path — the caller
73
- # (the `node_rule` block) positions it via
74
- # `Plugin::Base#diagnostic`. `location` is the Prism location the
75
- # diagnostic should point at (a call's `message_loc` for the call
76
- # itself, or a sub-argument's location for `unknown-*-key`-style
77
- # diagnostics that point at the offending Symbol).
61
+ # One Action Pack observation. Carries no path — the caller (the `node_rule` block) positions
62
+ # it via `Plugin::Base#diagnostic`. `location` is the Prism location the diagnostic should point
63
+ # at (a call's `message_loc` for the call itself, or a sub-argument's location for
64
+ # `unknown-*-key`-style diagnostics that point at the offending Symbol).
78
65
  Violation = Data.define(:location, :message, :severity, :rule)
79
66
 
80
67
  module_function
81
68
 
82
- # Phase 4 — route-helper consumption. The helper-existence /
83
- # arity check for ONE call node. Only the **info-level** route
84
- # resolution (`helper-call`) is the value this plugin adds it
85
- # surfaces the resolved HTTP method + path + action alongside the
86
- # call site, which `rigor-rails-routes` does not. Unknown-helper
87
- # and wrong-arity diagnostics are produced canonically by
88
- # `rigor-rails-routes`'s own analyzer (same `:helper_table` source
89
- # of truth), so emitting them here would double every call-site
90
- # error. Real-world impact pre-fix: ~301 duplicates on Mastodon and
91
- # ~119 on Redmine, exactly stacked with the rails-routes
92
- # diagnostics. Returns `[]` for unknown / arity-mismatch shapes.
69
+ # Phase 4 — route-helper consumption. The helper-existence / arity check for ONE call node.
70
+ # Only the **info-level** route resolution (`helper-call`) is the value this plugin adds — it
71
+ # surfaces the resolved HTTP method + path + action alongside the call site, which
72
+ # `rigor-rails-routes` does not. Unknown-helper and wrong-arity diagnostics are produced
73
+ # canonically by `rigor-rails-routes`'s own analyzer (same `:helper_table` source of truth), so
74
+ # emitting them here would double every call-site error. Real-world impact pre-fix: ~301
75
+ # duplicates on Mastodon and ~119 on Redmine, exactly stacked with the rails-routes diagnostics.
76
+ # Returns `[]` for unknown / arity-mismatch shapes.
93
77
  #
94
78
  # @param call_node [Prism::Node]
95
- # @param helper_table [Hash{String => Hash}] each entry carries
96
- # `name`, `arity`, `acceptable_arities`, `path`, `http_method`,
97
- # `action`.
79
+ # @param helper_table [Hash{String => Hash}] each entry carries `name`, `arity`,
80
+ # `acceptable_arities`, `path`, `http_method`, `action`.
98
81
  # @return [Array<Violation>]
99
82
  def helper_violations_for(call_node:, helper_table:)
100
83
  return [] unless implicit_helper_call?(call_node)
@@ -103,24 +86,20 @@ module Rigor
103
86
  return [] if entry.nil?
104
87
 
105
88
  actual_arity = positional_arg_count(call_node)
106
- # The `:helper_table` fact entry carries both `:arity` (the
107
- # first registered entry's arity) and `:acceptable_arities`
108
- # (the full Array populated for uncountable-noun resources
109
- # like `resources :news` which share a helper name across
110
- # index/show). Honour the full set; fall back to the single
111
- # arity for consumers compiled against an older fact shape.
89
+ # The `:helper_table` fact entry carries both `:arity` (the first registered entry's arity)
90
+ # and `:acceptable_arities` (the full Array — populated for uncountable-noun resources like
91
+ # `resources :news` which share a helper name across index/show). Honour the full set; fall
92
+ # back to the single arity for consumers compiled against an older fact shape.
112
93
  acceptable = entry[:acceptable_arities] || [entry[:arity]]
113
94
  return [] unless acceptable.include?(actual_arity)
114
95
 
115
96
  [helper_call_violation(call_node, entry)]
116
97
  end
117
98
 
118
- # Phase 2 — filter-chain validation for ONE filter-DSL call
119
- # (`before_action :name`, …). The enclosing controller is read
120
- # from the node-rule `NodeContext` ancestors, looked up in the
121
- # controller index to get the effective method set (including one
122
- # level of inheritance), and each filter name reference is
123
- # validated against it. Calls outside a known controller
99
+ # Phase 2 — filter-chain validation for ONE filter-DSL call (`before_action :name`, …). The
100
+ # enclosing controller is read from the node-rule `NodeContext` ancestors, looked up in the
101
+ # controller index to get the effective method set (including one level of inheritance), and
102
+ # each filter name reference is validated against it. Calls outside a known controller
124
103
  # contribute nothing.
125
104
  #
126
105
  # @param call_node [Prism::Node]
@@ -136,13 +115,11 @@ module Rigor
136
115
 
137
116
  methods = controller_index.effective_methods_for(class_name)
138
117
  spell_checker = DidYouMean::SpellChecker.new(dictionary: methods.map(&:to_s))
139
- # When the controller (or its parent) `include`s a module the
140
- # discoverer couldn't resolve — typically a gem-shipped concern
141
- # such as `Devise::Controllers::Helpers` or
142
- # `Pundit::Authorization` any `before_action :name` MIGHT be
143
- # defined in that unresolved module. Suppress
144
- # `unknown-filter-method` in that case rather than FPing on
145
- # legitimate gem-provided callback names.
118
+ # When the controller (or its parent) `include`s a module the discoverer couldn't resolve —
119
+ # typically a gem-shipped concern such as `Devise::Controllers::Helpers` or
120
+ # `Pundit::Authorization` any `before_action :name` MIGHT be defined in that unresolved
121
+ # module. Suppress `unknown-filter-method` in that case rather than FPing on legitimate
122
+ # gem-provided callback names.
146
123
  ambiguous_filters = controller_index.unresolved_include?(class_name)
147
124
 
148
125
  filter_name_args(call_node).filter_map do |arg_node|
@@ -156,12 +133,10 @@ module Rigor
156
133
  end
157
134
  end
158
135
 
159
- # Phase 1 — strong-parameter validation for ONE `permit` call.
160
- # Recognises the `params.require(:symbol).permit(:key, :key, ...)`
161
- # chain and validates each `:key` against the AR model's column
162
- # list (looked up via the model_index fact published by
163
- # `rigor-activerecord`). Calls whose `:require` argument is a
164
- # non-literal Symbol are passed through; namespaced models
136
+ # Phase 1 — strong-parameter validation for ONE `permit` call. Recognises the
137
+ # `params.require(:symbol).permit(:key, :key, ...)` chain and validates each `:key` against the
138
+ # AR model's column list (looked up via the model_index fact published by `rigor-activerecord`).
139
+ # Calls whose `:require` argument is a non-literal Symbol are passed through; namespaced models
165
140
  # (`params.require(:admin_user)` → `Admin::User`) are deferred.
166
141
  #
167
142
  # @param call_node [Prism::Node]
@@ -185,23 +160,19 @@ module Rigor
185
160
  end
186
161
  end
187
162
 
188
- # Phase 3 — render-target validation for ONE `render` call. Derive
189
- # the candidate view template path(s) from the controller class
190
- # name + the render argument shape, then check existence under the
191
- # configured `view_search_paths` (default `["app/views"]`).
192
- # Recognised call shapes:
163
+ # Phase 3 — render-target validation for ONE `render` call. Derive the candidate view template
164
+ # path(s) from the controller class name + the render argument shape, then check existence
165
+ # under the configured `view_search_paths` (default `["app/views"]`). Recognised call shapes:
193
166
  #
194
167
  # - `render :symbol` — `<views>/<controller_path>/<symbol>.html.erb`
195
168
  # - `render "string/path"` — `<views>/<string_path>.html.erb`
196
169
  # - `render partial: "name"` — `<views>/<controller_path>/_<name>.html.erb`
197
170
  # - `render partial: "string/path"` — `<views>/<string_path with _ prefix>.html.erb`
198
171
  #
199
- # `render layout:`, `render plain:`, `render json:`, `render
200
- # text:`, `render inline:`, `render :nothing => true`, etc. are
201
- # pass-through (no template lookup). Implicit-render (a controller
202
- # method that doesn't call `render`) is also skipped Phase 3
203
- # validates explicit renders only, since the implicit path would
204
- # false-positive on `redirect_to` / `head` / early returns.
172
+ # `render layout:`, `render plain:`, `render json:`, `render text:`, `render inline:`, `render
173
+ # :nothing => true`, etc. are pass-through (no template lookup). Implicit-render (a controller
174
+ # method that doesn't call `render`) is also skipped — Phase 3 validates explicit renders only,
175
+ # since the implicit path would false-positive on `redirect_to` / `head` / early returns.
205
176
  #
206
177
  # @param call_node [Prism::Node]
207
178
  # @param ancestors [Array<Prism::Node>] the lexical ancestor chain
@@ -215,41 +186,32 @@ module Rigor
215
186
  class_name = enclosing_controller_name(ancestors)
216
187
  return [] if class_name.nil?
217
188
 
218
- # Prefer the file-path-derived controller path when the source
219
- # lives under `app/controllers/` — Rails autoload is the runtime
220
- # authority on `Admin::Users::RolesController` vs
221
- # `Users::RolesController` (a declaration like `module Admin;
222
- # class Users::RolesController` resolves to whichever `Users`
223
- # constant Ruby finds first, and the file path is the
224
- # disambiguator Rails picks). Fall back to the AST-derived class
225
- # name for files outside `app/controllers/` (libraries, test
226
- # fixtures).
189
+ # Prefer the file-path-derived controller path when the source lives under `app/controllers/`
190
+ # — Rails autoload is the runtime authority on `Admin::Users::RolesController` vs
191
+ # `Users::RolesController` (a declaration like `module Admin; class Users::RolesController`
192
+ # resolves to whichever `Users` constant Ruby finds first, and the file path is the
193
+ # disambiguator Rails picks). Fall back to the AST-derived class name for files outside
194
+ # `app/controllers/` (libraries, test fixtures).
227
195
  controller_path = controller_path_from_file(path) || controller_path_for(class_name)
228
196
  return [] if controller_path.nil?
229
197
 
230
- # Render checks silence when the controller (or its ancestor
231
- # chain) inherits from a gem-shipped parent
232
- # (Devise::ConfirmationsController,
233
- # Doorkeeper::ApplicationsController, …). The gem ships its own
234
- # views; the local subclass calling `render :show` resolves
235
- # through the gem's view path, which our static analyser doesn't
236
- # know about.
198
+ # Render checks silence when the controller (or its ancestor chain) inherits from a
199
+ # gem-shipped parent (Devise::ConfirmationsController, Doorkeeper::ApplicationsController, …).
200
+ # The gem ships its own views; the local subclass calling `render :show` resolves through the
201
+ # gem's view path, which our static analyser doesn't know about.
237
202
  if controller_index&.known?(class_name) &&
238
203
  controller_index.unresolved_include?(class_name)
239
204
  return []
240
205
  end
241
206
 
242
- # Abstract base controllers: a `*BaseController` (`Admin::
243
- # BaseController`, `Settings::Preferences::BaseController`, …)
244
- # typically has no view of its own; its `render :show` bodies
245
- # are resolved by Rails against the calling subclass's
246
- # controller path at request time, NOT the base's. Same for
247
- # parent controllers whose view directory exists but contains
248
- # only subdirectories (Mastodon's `Admin::SettingsController`
249
- # whose `app/views/admin/settings/` holds about/, appearance/,
250
- # … but no top-level templates). Skip render checks for these —
251
- # the diagnostic would be a false positive against intentional
252
- # Rails abstract-base layouts.
207
+ # Abstract base controllers: a `*BaseController` (`Admin::BaseController`,
208
+ # `Settings::Preferences::BaseController`, …) typically has no view of its own; its `render
209
+ # :show` bodies are resolved by Rails against the calling subclass's controller path at
210
+ # request time, NOT the base's. Same for parent controllers whose view directory exists but
211
+ # contains only subdirectories (Mastodon's `Admin::SettingsController` whose
212
+ # `app/views/admin/settings/` holds about/, appearance/, but no top-level templates). Skip
213
+ # render checks for these — the diagnostic would be a false positive against intentional Rails
214
+ # abstract-base layouts.
253
215
  return [] if abstract_base_controller?(class_name, controller_path, view_search_roots)
254
216
 
255
217
  target = render_target_for(call_node, controller_path)
@@ -259,27 +221,22 @@ module Rigor
259
221
  violation ? [violation] : []
260
222
  end
261
223
 
262
- # Resolves the qualified name of the controller class the node
263
- # sits in from its lexical ancestors (the node-rule
264
- # `NodeContext#ancestors`, outermost first). The OUTERMOST
265
- # `ClassNode` is the controller matching the old forward walk,
266
- # which returned the first `ClassNode` reachable from the file
267
- # root and validated nested-class call sites against it. Enclosing
268
- # `ModuleNode`s above it contribute the namespace, so the
269
- # nested-module declaration shape
224
+ # Resolves the qualified name of the controller class the node sits in from its lexical
225
+ # ancestors (the node-rule `NodeContext#ancestors`, outermost first). The OUTERMOST `ClassNode`
226
+ # is the controller — matching the old forward walk, which returned the first `ClassNode`
227
+ # reachable from the file root and validated nested-class call sites against it. Enclosing
228
+ # `ModuleNode`s above it contribute the namespace, so the nested-module declaration shape
270
229
  #
271
230
  # module Admin
272
231
  # class DomainBlocksController < BaseController
273
232
  # end
274
233
  # end
275
234
  #
276
- # is recovered as `Admin::DomainBlocksController` — the same name
277
- # the `ControllerDiscoverer` registers under. Without this, a bare
278
- # inner-class name would be used for index lookups +
279
- # `controller_path_for`, silently skipping filter validation and
280
- # pointing render template paths at the wrong directory
281
- # (Mastodon's `admin/domain_blocks` rendered as bare
282
- # `domain_blocks`). Returns nil when no enclosing class exists.
235
+ # is recovered as `Admin::DomainBlocksController` — the same name the `ControllerDiscoverer`
236
+ # registers under. Without this, a bare inner-class name would be used for index lookups +
237
+ # `controller_path_for`, silently skipping filter validation and pointing render template paths
238
+ # at the wrong directory (Mastodon's `admin/domain_blocks` rendered as bare `domain_blocks`).
239
+ # Returns nil when no enclosing class exists.
283
240
  def enclosing_controller_name(ancestors)
284
241
  class_index = ancestors.index { |n| n.is_a?(Prism::ClassNode) }
285
242
  return nil if class_index.nil?
@@ -296,10 +253,9 @@ module Rigor
296
253
  path ? path.split("::") : []
297
254
  end
298
255
 
299
- # Resolves a class-name AST node against an enclosing namespace
300
- # chain. A `ConstantPathNode` (e.g. `class Admin::Foo`) is already
301
- # absolute and ignores the chain; a `ConstantReadNode` (bare
302
- # `class Foo` inside `module Admin`) is qualified against it.
256
+ # Resolves a class-name AST node against an enclosing namespace chain. A `ConstantPathNode`
257
+ # (e.g. `class Admin::Foo`) is already absolute and ignores the chain; a `ConstantReadNode`
258
+ # (bare `class Foo` inside `module Admin`) is qualified against it.
303
259
  def qualified_name_with_enclosing(node, enclosing)
304
260
  return nil unless node.is_a?(Prism::Node)
305
261
 
@@ -334,9 +290,8 @@ module Rigor
334
290
  node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :render
335
291
  end
336
292
 
337
- # Drops the trailing keyword hash (`only:` / `except:` / `if:` /
338
- # `unless:`) so the modifier args don't get treated as filter
339
- # names.
293
+ # Drops the trailing keyword hash (`only:` / `except:` / `if:` / `unless:`) so the modifier args
294
+ # don't get treated as filter names.
340
295
  def filter_name_args(call_node)
341
296
  args = call_node.arguments&.arguments || []
342
297
  args = args[0..-2] if args.last.is_a?(Prism::KeywordHashNode)
@@ -351,9 +306,8 @@ module Rigor
351
306
  if methods.include?(filter_name.to_sym)
352
307
  filter_call_violation(call_node, filter_name)
353
308
  elsif ambiguous_filters
354
- # An unresolved include shadows our judgment — emit the
355
- # recognized-filter info anyway so the call site is still
356
- # indexed, but skip the error.
309
+ # An unresolved include shadows our judgment — emit the recognized-filter info anyway so
310
+ # the call site is still indexed, but skip the error.
357
311
  filter_call_violation(call_node, filter_name)
358
312
  else
359
313
  unknown_filter_violation(arg_node, call_node, filter_name, spell_checker)
@@ -373,11 +327,9 @@ module Rigor
373
327
  def model_class_for_permit(permit_call)
374
328
  require_call = permit_call.receiver
375
329
  symbol_node = require_call.arguments.arguments.first
376
- # `:user` → `User`, `:order_item` → `OrderItem`. ADR-39: the
377
- # shared inflector camelizes through the real
378
- # `ActiveSupport::Inflector` when present (so the multi-word
379
- # case the former `split("_").map(&:capitalize)` only
380
- # approximated is authoritative), falling back otherwise.
330
+ # `:user` → `User`, `:order_item` → `OrderItem`. ADR-39: the shared inflector camelizes
331
+ # through the real `ActiveSupport::Inflector` when present (so the multi-word case the former
332
+ # `split("_").map(&:capitalize)` only approximated is authoritative), falling back otherwise.
381
333
  Rigor::Plugin::Inflector.camelize(symbol_node.value.to_s)
382
334
  end
383
335
 
@@ -405,29 +357,24 @@ module Rigor
405
357
  )
406
358
  end
407
359
 
408
- # Convert `<root>/app/controllers/admin/users/roles_controller.rb`
409
- # to `admin/users/roles`. Returns nil if the path is not under an
410
- # `app/controllers/` segment.
360
+ # Convert `<root>/app/controllers/admin/users/roles_controller.rb` to `admin/users/roles`.
361
+ # Returns nil if the path is not under an `app/controllers/` segment.
411
362
  def controller_path_from_file(path)
412
363
  match = path.match(%r{(?:\A|/)app/controllers/(.+)_controller\.rb\z})
413
364
  match&.[](1)
414
365
  end
415
366
 
416
- # An abstract base controller — its `render` bodies don't validate
417
- # against its own controller_path because Rails resolves at
418
- # request time against the actual subclass. Two conservative
419
- # heuristics:
420
- # (1) The class name ends with `BaseController`. Strong
421
- # Rails-convention signal (Settings::Preferences::
422
- # BaseController, Admin::BaseController, …).
423
- # (2) The controller's view directory exists but contains ONLY
424
- # subdirectories (no template files at its top). Mastodon's
425
- # Admin::SettingsController whose app/views/admin/settings/
426
- # holds only about/, appearance/, … — the parent of nested
367
+ # An abstract base controller — its `render` bodies don't validate against its own
368
+ # controller_path because Rails resolves at request time against the actual subclass. Two
369
+ # conservative heuristics:
370
+ # (1) The class name ends with `BaseController`. Strong Rails-convention signal
371
+ # (Settings::Preferences::BaseController, Admin::BaseController, …).
372
+ # (2) The controller's view directory exists but contains ONLY subdirectories (no template
373
+ # files at its top). Mastodon's Admin::SettingsController whose
374
+ # app/views/admin/settings/ holds only about/, appearance/, the parent of nested
427
375
  # controllers.
428
- # Deliberately NOT triggered by "no view directory at all" because
429
- # that fires the diagnostic we DO want for genuinely-missing views
430
- # (the typo / forgot-to-create case).
376
+ # Deliberately NOT triggered by "no view directory at all" because that fires the diagnostic we
377
+ # DO want for genuinely-missing views (the typo / forgot-to-create case).
431
378
  def abstract_base_controller?(class_name, controller_path, view_search_roots)
432
379
  return true if class_name.end_with?("BaseController")
433
380
 
@@ -440,12 +387,10 @@ module Rigor
440
387
  end
441
388
  end
442
389
 
443
- # Returns `[kind, view_relative_path]` where kind is `:template`
444
- # or `:partial`, and view_relative_path is the path under
445
- # view_search_roots WITHOUT extension (the extension family is
446
- # appended at lookup time). Returns nil for shapes Phase 3 doesn't
447
- # validate (`layout:` / `plain:` / `json:` / `text:` / `inline:` /
448
- # `:nothing` / no parseable target).
390
+ # Returns `[kind, view_relative_path]` where kind is `:template` or `:partial`, and
391
+ # view_relative_path is the path under view_search_roots WITHOUT extension (the extension
392
+ # family is appended at lookup time). Returns nil for shapes Phase 3 doesn't validate (`layout:`
393
+ # / `plain:` / `json:` / `text:` / `inline:` / `:nothing` / no parseable target).
449
394
  def render_target_for(call_node, controller_path)
450
395
  args = call_node.arguments&.arguments || []
451
396
  return nil if args.empty?
@@ -454,9 +399,8 @@ module Rigor
454
399
  # `render partial: "..."` — the keyword form.
455
400
  return partial_target_from_kwargs(first, controller_path) if first.is_a?(Prism::KeywordHashNode)
456
401
 
457
- # `render :symbol` / `render "path"`. A trailing KeywordHashNode
458
- # is allowed (e.g. `render :show, status: :ok`); the leading
459
- # positional carries the template name.
402
+ # `render :symbol` / `render "path"`. A trailing KeywordHashNode is allowed (e.g. `render
403
+ # :show, status: :ok`); the leading positional carries the template name.
460
404
  template_target_from_positional(first, controller_path)
461
405
  end
462
406
 
@@ -487,15 +431,13 @@ module Rigor
487
431
 
488
432
  # `UsersController` → "users".
489
433
  # `Admin::WidgetsController` → "admin/widgets".
490
- # Returns nil for class names that don't end with the `Controller`
491
- # suffix.
434
+ # Returns nil for class names that don't end with the `Controller` suffix.
492
435
  def controller_path_for(class_name)
493
436
  return nil unless class_name.end_with?("Controller")
494
437
 
495
438
  stripped = class_name.delete_suffix("Controller")
496
- # ADR-39: the shared inflector flattens `::` → `/` and applies
497
- # Rails' real underscore (the per-segment hand-rolled version
498
- # produced the same result for the common case but missed
439
+ # ADR-39: the shared inflector flattens `::` → `/` and applies Rails' real underscore (the
440
+ # per-segment hand-rolled version produced the same result for the common case but missed
499
441
  # acronym handling). `Admin::Widgets` → `admin/widgets`.
500
442
  Rigor::Plugin::Inflector.underscore(stripped)
501
443
  end
@@ -7,15 +7,11 @@ require_relative "controller_index"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class Actionpack < Rigor::Plugin::Base
10
- # Walks `controller_search_paths` building a
11
- # {ControllerIndex} of `(class_name, methods,
12
- # parent_class_name)` triples. Used by Phase 2 (filter
13
- # chains) to validate that `before_action :name`
14
- # references a method defined on the controller or its
15
- # ancestor chain.
10
+ # Walks `controller_search_paths` building a {ControllerIndex} of `(class_name, methods,
11
+ # parent_class_name)` triples. Used by Phase 2 (filter chains) to validate that `before_action
12
+ # :name` references a method defined on the controller or its ancestor chain.
16
13
  #
17
- # Two declaration shapes are recognised and qualified
18
- # the same way:
14
+ # Two declaration shapes are recognised and qualified the same way:
19
15
  #
20
16
  # class Admin::AccountsController < BaseController
21
17
  # # → registered as "Admin::AccountsController"
@@ -26,25 +22,18 @@ module Rigor
26
22
  # end
27
23
  # end
28
24
  #
29
- # Pre-fix the nested form ignored the enclosing `module
30
- # Admin` and registered the inner class as bare
31
- # `"AccountsController"`, overwriting the top-level
32
- # `app/controllers/accounts_controller.rb` entry. Mastodon
33
- # has both shapes for the same logical name (top-level
34
- # `AccountsController` + `Admin::AccountsController` +
35
- # `Api::V1::AccountsController`) and the overwrite caused
36
- # the wrong inheritance / method set to flow downstream,
37
- # producing dozens of false `unknown-filter-method`
25
+ # Pre-fix the nested form ignored the enclosing `module Admin` and registered the inner class as
26
+ # bare `"AccountsController"`, overwriting the top-level `app/controllers/accounts_controller.rb`
27
+ # entry. Mastodon has both shapes for the same logical name (top-level `AccountsController` +
28
+ # `Admin::AccountsController` + `Api::V1::AccountsController`) and the overwrite caused the wrong
29
+ # inheritance / method set to flow downstream, producing dozens of false `unknown-filter-method`
38
30
  # diagnostics.
39
31
  #
40
- # Multi-level inheritance is also walked: `Admin::AccountsController
41
- # < BaseController < ApplicationController` resolves
42
- # methods from all three. Cycle-safe via a visited set.
43
- # The parent-name lookup is **lexically scoped** a bare
44
- # `BaseController` reference inside `module Admin` first
45
- # tries `Admin::BaseController`, then falls through to
46
- # top-level `BaseController`, matching Ruby's constant-
47
- # resolution semantics.
32
+ # Multi-level inheritance is also walked: `Admin::AccountsController < BaseController <
33
+ # ApplicationController` resolves methods from all three. Cycle-safe via a visited set. The
34
+ # parent-name lookup is **lexically scoped** a bare `BaseController` reference inside `module
35
+ # Admin` first tries `Admin::BaseController`, then falls through to top-level `BaseController`,
36
+ # matching Ruby's constant-resolution semantics.
48
37
  class ControllerDiscoverer
49
38
  def initialize(io_boundary:, search_paths:)
50
39
  @io_boundary = io_boundary
@@ -84,15 +73,12 @@ module Rigor
84
73
  nil
85
74
  end
86
75
 
87
- # Walks every `ClassNode` / `ModuleNode` in the AST,
88
- # yielding `[declaration_node, enclosing_namespace_array]`
89
- # for each. `enclosing` is the chain of module / class
90
- # qualifiers OUTSIDE the yielded declaration (e.g. for
91
- # `module Admin; class AccountsController; end; end`
92
- # the inner class yields with enclosing
93
- # `["Admin"]`). The yielded declaration's own segment
94
- # is added to the chain for its body's recursion so
95
- # constants nested two levels deep qualify correctly.
76
+ # Walks every `ClassNode` / `ModuleNode` in the AST, yielding `[declaration_node,
77
+ # enclosing_namespace_array]` for each. `enclosing` is the chain of module / class qualifiers
78
+ # OUTSIDE the yielded declaration (e.g. for `module Admin; class AccountsController; end; end`
79
+ # the inner class yields with enclosing `["Admin"]`). The yielded declaration's own segment is
80
+ # added to the chain for its body's recursion so constants nested two levels deep qualify
81
+ # correctly.
96
82
  def walk_declarations(node, namespace:, &)
97
83
  return unless node.is_a?(Prism::Node)
98
84
 
@@ -109,11 +95,9 @@ module Rigor
109
95
  end
110
96
  end
111
97
 
112
- # The segments contributed by a declaration to the
113
- # qualifier chain of its body. For `class A::B` this is
114
- # `["A", "B"]`; for `module Outer` it is `["Outer"]`.
115
- # Returns `[]` for an anonymous declaration the walker
116
- # cannot qualify.
98
+ # The segments contributed by a declaration to the qualifier chain of its body. For `class A::B`
99
+ # this is `["A", "B"]`; for `module Outer` it is `["Outer"]`. Returns `[]` for an anonymous
100
+ # declaration the walker cannot qualify.
117
101
  def namespace_segments_for(declaration_node)
118
102
  path = qualified_name_for(declaration_node.constant_path)
119
103
  path ? path.split("::") : []
@@ -133,13 +117,10 @@ module Rigor
133
117
  )
134
118
  end
135
119
 
136
- # Resolves the declared name against the enclosing
137
- # namespace chain. A `ConstantPathNode` (e.g.
138
- # `Admin::Foo` in `class Admin::Foo`) is already
139
- # absolute and ignores the enclosing chain. A
140
- # `ConstantReadNode` (bare `Foo` in `module Admin;
141
- # class Foo`) is qualified against the chain so the
142
- # name becomes `Admin::Foo`.
120
+ # Resolves the declared name against the enclosing namespace chain. A `ConstantPathNode` (e.g.
121
+ # `Admin::Foo` in `class Admin::Foo`) is already absolute and ignores the enclosing chain. A
122
+ # `ConstantReadNode` (bare `Foo` in `module Admin; class Foo`) is qualified against the chain so
123
+ # the name becomes `Admin::Foo`.
143
124
  def qualified_name_with_enclosing(node, enclosing)
144
125
  return nil unless node.is_a?(Prism::Node)
145
126
 
@@ -165,11 +146,9 @@ module Rigor
165
146
  accumulator
166
147
  end
167
148
 
168
- # Collects the qualified-constant targets passed to
169
- # `include` calls inside the body. Stops at nested
170
- # `ClassNode` / `ModuleNode` boundaries so a class
171
- # declared inside a concern doesn't pull the concern's
172
- # includes into itself.
149
+ # Collects the qualified-constant targets passed to `include` calls inside the body. Stops at
150
+ # nested `ClassNode` / `ModuleNode` boundaries so a class declared inside a concern doesn't pull
151
+ # the concern's includes into itself.
173
152
  def collect_include_targets(node, accumulator = [])
174
153
  return accumulator unless node.is_a?(Prism::Node)
175
154
  return accumulator if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)