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
@@ -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,37 @@ 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
+ # Max edit distance between a non-column permit key and a real column for the key to count as a
62
+ # typo (and fire `unknown-permit-key`) rather than a legitimate virtual attribute. A tight bound
63
+ # keeps genuine typos (`emial`→`email`, distance 2) while never firing on a virtual attribute,
64
+ # whose name is nothing like any column.
65
+ PERMIT_KEY_TYPO_MAX_DISTANCE = 2
66
+
67
+ # One Action Pack observation. Carries no path — the caller (the `node_rule` block) positions
68
+ # it via `Plugin::Base#diagnostic`. `location` is the Prism location the diagnostic should point
69
+ # at (a call's `message_loc` for the call itself, or a sub-argument's location for
70
+ # `unknown-*-key`-style diagnostics that point at the offending Symbol).
78
71
  Violation = Data.define(:location, :message, :severity, :rule)
79
72
 
80
73
  module_function
81
74
 
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.
75
+ # Phase 4 — route-helper consumption. The helper-existence / arity check for ONE call node.
76
+ # Only the **info-level** route resolution (`helper-call`) is the value this plugin adds — it
77
+ # surfaces the resolved HTTP method + path + action alongside the call site, which
78
+ # `rigor-rails-routes` does not. Unknown-helper and wrong-arity diagnostics are produced
79
+ # canonically by `rigor-rails-routes`'s own analyzer (same `:helper_table` source of truth), so
80
+ # emitting them here would double every call-site error. Real-world impact pre-fix: ~301
81
+ # duplicates on Mastodon and ~119 on Redmine, exactly stacked with the rails-routes diagnostics.
82
+ # Returns `[]` for unknown / arity-mismatch shapes.
93
83
  #
94
84
  # @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`.
85
+ # @param helper_table [Hash{String => Hash}] each entry carries `name`, `arity`,
86
+ # `acceptable_arities`, `path`, `http_method`, `action`.
98
87
  # @return [Array<Violation>]
99
88
  def helper_violations_for(call_node:, helper_table:)
100
89
  return [] unless implicit_helper_call?(call_node)
@@ -103,24 +92,20 @@ module Rigor
103
92
  return [] if entry.nil?
104
93
 
105
94
  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.
95
+ # The `:helper_table` fact entry carries both `:arity` (the first registered entry's arity)
96
+ # and `:acceptable_arities` (the full Array — populated for uncountable-noun resources like
97
+ # `resources :news` which share a helper name across index/show). Honour the full set; fall
98
+ # back to the single arity for consumers compiled against an older fact shape.
112
99
  acceptable = entry[:acceptable_arities] || [entry[:arity]]
113
100
  return [] unless acceptable.include?(actual_arity)
114
101
 
115
102
  [helper_call_violation(call_node, entry)]
116
103
  end
117
104
 
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
105
+ # Phase 2 — filter-chain validation for ONE filter-DSL call (`before_action :name`, …). The
106
+ # enclosing controller is read from the node-rule `NodeContext` ancestors, looked up in the
107
+ # controller index to get the effective method set (including one level of inheritance), and
108
+ # each filter name reference is validated against it. Calls outside a known controller
124
109
  # contribute nothing.
125
110
  #
126
111
  # @param call_node [Prism::Node]
@@ -136,13 +121,11 @@ module Rigor
136
121
 
137
122
  methods = controller_index.effective_methods_for(class_name)
138
123
  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.
124
+ # When the controller (or its parent) `include`s a module the discoverer couldn't resolve —
125
+ # typically a gem-shipped concern such as `Devise::Controllers::Helpers` or
126
+ # `Pundit::Authorization` any `before_action :name` MIGHT be defined in that unresolved
127
+ # module. Suppress `unknown-filter-method` in that case rather than FPing on legitimate
128
+ # gem-provided callback names.
146
129
  ambiguous_filters = controller_index.unresolved_include?(class_name)
147
130
 
148
131
  filter_name_args(call_node).filter_map do |arg_node|
@@ -156,12 +139,10 @@ module Rigor
156
139
  end
157
140
  end
158
141
 
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
142
+ # Phase 1 — strong-parameter validation for ONE `permit` call. Recognises the
143
+ # `params.require(:symbol).permit(:key, :key, ...)` chain and validates each `:key` against the
144
+ # AR model's column list (looked up via the model_index fact published by `rigor-activerecord`).
145
+ # Calls whose `:require` argument is a non-literal Symbol are passed through; namespaced models
165
146
  # (`params.require(:admin_user)` → `Admin::User`) are deferred.
166
147
  #
167
148
  # @param call_node [Prism::Node]
@@ -175,33 +156,28 @@ module Rigor
175
156
  return [] if entry.nil? # unknown model — skip; the model lookup is best-effort.
176
157
 
177
158
  columns = entry[:columns]
178
- spell_checker = DidYouMean::SpellChecker.new(dictionary: columns)
179
- literal_permit_keys(call_node).map do |key_node, key_name|
159
+ literal_permit_keys(call_node).filter_map do |key_node, key_name|
180
160
  if columns.include?(key_name)
181
161
  permit_call_violation(call_node, model_class, key_name)
182
162
  else
183
- unknown_permit_key_violation(key_node, model_class, key_name, spell_checker)
163
+ unknown_permit_key_violation(key_node, model_class, key_name, columns)
184
164
  end
185
165
  end
186
166
  end
187
167
 
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:
168
+ # Phase 3 — render-target validation for ONE `render` call. Derive the candidate view template
169
+ # path(s) from the controller class name + the render argument shape, then check existence
170
+ # under the configured `view_search_paths` (default `["app/views"]`). Recognised call shapes:
193
171
  #
194
172
  # - `render :symbol` — `<views>/<controller_path>/<symbol>.html.erb`
195
173
  # - `render "string/path"` — `<views>/<string_path>.html.erb`
196
174
  # - `render partial: "name"` — `<views>/<controller_path>/_<name>.html.erb`
197
175
  # - `render partial: "string/path"` — `<views>/<string_path with _ prefix>.html.erb`
198
176
  #
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.
177
+ # `render layout:`, `render plain:`, `render json:`, `render text:`, `render inline:`, `render
178
+ # :nothing => true`, etc. are pass-through (no template lookup). Implicit-render (a controller
179
+ # method that doesn't call `render`) is also skipped — Phase 3 validates explicit renders only,
180
+ # since the implicit path would false-positive on `redirect_to` / `head` / early returns.
205
181
  #
206
182
  # @param call_node [Prism::Node]
207
183
  # @param ancestors [Array<Prism::Node>] the lexical ancestor chain
@@ -215,41 +191,32 @@ module Rigor
215
191
  class_name = enclosing_controller_name(ancestors)
216
192
  return [] if class_name.nil?
217
193
 
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).
194
+ # Prefer the file-path-derived controller path when the source lives under `app/controllers/`
195
+ # — Rails autoload is the runtime authority on `Admin::Users::RolesController` vs
196
+ # `Users::RolesController` (a declaration like `module Admin; class Users::RolesController`
197
+ # resolves to whichever `Users` constant Ruby finds first, and the file path is the
198
+ # disambiguator Rails picks). Fall back to the AST-derived class name for files outside
199
+ # `app/controllers/` (libraries, test fixtures).
227
200
  controller_path = controller_path_from_file(path) || controller_path_for(class_name)
228
201
  return [] if controller_path.nil?
229
202
 
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.
203
+ # Render checks silence when the controller (or its ancestor chain) inherits from a
204
+ # gem-shipped parent (Devise::ConfirmationsController, Doorkeeper::ApplicationsController, …).
205
+ # The gem ships its own views; the local subclass calling `render :show` resolves through the
206
+ # gem's view path, which our static analyser doesn't know about.
237
207
  if controller_index&.known?(class_name) &&
238
208
  controller_index.unresolved_include?(class_name)
239
209
  return []
240
210
  end
241
211
 
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.
212
+ # Abstract base controllers: a `*BaseController` (`Admin::BaseController`,
213
+ # `Settings::Preferences::BaseController`, …) typically has no view of its own; its `render
214
+ # :show` bodies are resolved by Rails against the calling subclass's controller path at
215
+ # request time, NOT the base's. Same for parent controllers whose view directory exists but
216
+ # contains only subdirectories (Mastodon's `Admin::SettingsController` whose
217
+ # `app/views/admin/settings/` holds about/, appearance/, but no top-level templates). Skip
218
+ # render checks for these — the diagnostic would be a false positive against intentional Rails
219
+ # abstract-base layouts.
253
220
  return [] if abstract_base_controller?(class_name, controller_path, view_search_roots)
254
221
 
255
222
  target = render_target_for(call_node, controller_path)
@@ -259,27 +226,22 @@ module Rigor
259
226
  violation ? [violation] : []
260
227
  end
261
228
 
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
229
+ # Resolves the qualified name of the controller class the node sits in from its lexical
230
+ # ancestors (the node-rule `NodeContext#ancestors`, outermost first). The OUTERMOST `ClassNode`
231
+ # is the controller — matching the old forward walk, which returned the first `ClassNode`
232
+ # reachable from the file root and validated nested-class call sites against it. Enclosing
233
+ # `ModuleNode`s above it contribute the namespace, so the nested-module declaration shape
270
234
  #
271
235
  # module Admin
272
236
  # class DomainBlocksController < BaseController
273
237
  # end
274
238
  # end
275
239
  #
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.
240
+ # is recovered as `Admin::DomainBlocksController` — the same name the `ControllerDiscoverer`
241
+ # registers under. Without this, a bare inner-class name would be used for index lookups +
242
+ # `controller_path_for`, silently skipping filter validation and pointing render template paths
243
+ # at the wrong directory (Mastodon's `admin/domain_blocks` rendered as bare `domain_blocks`).
244
+ # Returns nil when no enclosing class exists.
283
245
  def enclosing_controller_name(ancestors)
284
246
  class_index = ancestors.index { |n| n.is_a?(Prism::ClassNode) }
285
247
  return nil if class_index.nil?
@@ -296,10 +258,9 @@ module Rigor
296
258
  path ? path.split("::") : []
297
259
  end
298
260
 
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.
261
+ # Resolves a class-name AST node against an enclosing namespace chain. A `ConstantPathNode`
262
+ # (e.g. `class Admin::Foo`) is already absolute and ignores the chain; a `ConstantReadNode`
263
+ # (bare `class Foo` inside `module Admin`) is qualified against it.
303
264
  def qualified_name_with_enclosing(node, enclosing)
304
265
  return nil unless node.is_a?(Prism::Node)
305
266
 
@@ -334,9 +295,8 @@ module Rigor
334
295
  node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :render
335
296
  end
336
297
 
337
- # Drops the trailing keyword hash (`only:` / `except:` / `if:` /
338
- # `unless:`) so the modifier args don't get treated as filter
339
- # names.
298
+ # Drops the trailing keyword hash (`only:` / `except:` / `if:` / `unless:`) so the modifier args
299
+ # don't get treated as filter names.
340
300
  def filter_name_args(call_node)
341
301
  args = call_node.arguments&.arguments || []
342
302
  args = args[0..-2] if args.last.is_a?(Prism::KeywordHashNode)
@@ -351,9 +311,8 @@ module Rigor
351
311
  if methods.include?(filter_name.to_sym)
352
312
  filter_call_violation(call_node, filter_name)
353
313
  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.
314
+ # An unresolved include shadows our judgment — emit the recognized-filter info anyway so
315
+ # the call site is still indexed, but skip the error.
357
316
  filter_call_violation(call_node, filter_name)
358
317
  else
359
318
  unknown_filter_violation(arg_node, call_node, filter_name, spell_checker)
@@ -373,11 +332,9 @@ module Rigor
373
332
  def model_class_for_permit(permit_call)
374
333
  require_call = permit_call.receiver
375
334
  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.
335
+ # `:user` → `User`, `:order_item` → `OrderItem`. ADR-39: the shared inflector camelizes
336
+ # through the real `ActiveSupport::Inflector` when present (so the multi-word case the former
337
+ # `split("_").map(&:capitalize)` only approximated is authoritative), falling back otherwise.
381
338
  Rigor::Plugin::Inflector.camelize(symbol_node.value.to_s)
382
339
  end
383
340
 
@@ -395,39 +352,44 @@ module Rigor
395
352
  )
396
353
  end
397
354
 
398
- def unknown_permit_key_violation(key_node, model_class, key_name, spell_checker)
399
- base = "Action Pack permit `#{key_name}` is not a column on `#{model_class}`."
400
- suggestion = spell_checker.correct(key_name).first
401
- message = suggestion ? "#{base} Did you mean `:#{suggestion}`?" : base
355
+ # A permit key that is not a column is only a violation when it is a near-miss TYPO of a real
356
+ # column. Permitting a non-column is ordinary Rails: a Devise virtual attribute (`password`,
357
+ # `remember_me`, `otp_attempt`), a state-machine `*_event`, an `attr_accessor` setter, or a nested
358
+ # `*_attributes` key are all mass-assignable but absent from the schema. So fire only when a column
359
+ # sits within a tight edit distance of the key (a typo like `emial`→`email`); a key with no close
360
+ # column is presumed a deliberate virtual attribute and passes silently. Firing on every non-column
361
+ # key false-positives across the whole strong-params surface of a Devise/state-machine app.
362
+ def unknown_permit_key_violation(key_node, model_class, key_name, columns)
363
+ nearest = columns.min_by { |col| DidYouMean::Levenshtein.distance(key_name, col) }
364
+ return nil if nearest.nil?
365
+ return nil if DidYouMean::Levenshtein.distance(key_name, nearest) > PERMIT_KEY_TYPO_MAX_DISTANCE
366
+
402
367
  Violation.new(
403
368
  location: key_node.location,
404
- message: message, severity: :error, rule: "unknown-permit-key"
369
+ message: "Action Pack permit `#{key_name}` is not a column on `#{model_class}`. " \
370
+ "Did you mean `:#{nearest}`?",
371
+ severity: :error, rule: "unknown-permit-key"
405
372
  )
406
373
  end
407
374
 
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.
375
+ # Convert `<root>/app/controllers/admin/users/roles_controller.rb` to `admin/users/roles`.
376
+ # Returns nil if the path is not under an `app/controllers/` segment.
411
377
  def controller_path_from_file(path)
412
378
  match = path.match(%r{(?:\A|/)app/controllers/(.+)_controller\.rb\z})
413
379
  match&.[](1)
414
380
  end
415
381
 
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
382
+ # An abstract base controller — its `render` bodies don't validate against its own
383
+ # controller_path because Rails resolves at request time against the actual subclass. Two
384
+ # conservative heuristics:
385
+ # (1) The class name ends with `BaseController`. Strong Rails-convention signal
386
+ # (Settings::Preferences::BaseController, Admin::BaseController, …).
387
+ # (2) The controller's view directory exists but contains ONLY subdirectories (no template
388
+ # files at its top). Mastodon's Admin::SettingsController whose
389
+ # app/views/admin/settings/ holds only about/, appearance/, the parent of nested
427
390
  # 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).
391
+ # Deliberately NOT triggered by "no view directory at all" because that fires the diagnostic we
392
+ # DO want for genuinely-missing views (the typo / forgot-to-create case).
431
393
  def abstract_base_controller?(class_name, controller_path, view_search_roots)
432
394
  return true if class_name.end_with?("BaseController")
433
395
 
@@ -440,12 +402,10 @@ module Rigor
440
402
  end
441
403
  end
442
404
 
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).
405
+ # Returns `[kind, view_relative_path]` where kind is `:template` or `:partial`, and
406
+ # view_relative_path is the path under view_search_roots WITHOUT extension (the extension
407
+ # family is appended at lookup time). Returns nil for shapes Phase 3 doesn't validate (`layout:`
408
+ # / `plain:` / `json:` / `text:` / `inline:` / `:nothing` / no parseable target).
449
409
  def render_target_for(call_node, controller_path)
450
410
  args = call_node.arguments&.arguments || []
451
411
  return nil if args.empty?
@@ -454,9 +414,8 @@ module Rigor
454
414
  # `render partial: "..."` — the keyword form.
455
415
  return partial_target_from_kwargs(first, controller_path) if first.is_a?(Prism::KeywordHashNode)
456
416
 
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.
417
+ # `render :symbol` / `render "path"`. A trailing KeywordHashNode is allowed (e.g. `render
418
+ # :show, status: :ok`); the leading positional carries the template name.
460
419
  template_target_from_positional(first, controller_path)
461
420
  end
462
421
 
@@ -487,15 +446,13 @@ module Rigor
487
446
 
488
447
  # `UsersController` → "users".
489
448
  # `Admin::WidgetsController` → "admin/widgets".
490
- # Returns nil for class names that don't end with the `Controller`
491
- # suffix.
449
+ # Returns nil for class names that don't end with the `Controller` suffix.
492
450
  def controller_path_for(class_name)
493
451
  return nil unless class_name.end_with?("Controller")
494
452
 
495
453
  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
454
+ # ADR-39: the shared inflector flattens `::` → `/` and applies Rails' real underscore (the
455
+ # per-segment hand-rolled version produced the same result for the common case but missed
499
456
  # acronym handling). `Admin::Widgets` → `admin/widgets`.
500
457
  Rigor::Plugin::Inflector.underscore(stripped)
501
458
  end