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
@@ -6,61 +6,44 @@ require_relative "minitest/assertion_analyzer"
6
6
 
7
7
  module Rigor
8
8
  module Plugin
9
- # rigor-minitest — narrows locals through Minitest /
10
- # Test::Unit assertions.
9
+ # rigor-minitest — narrows locals through Minitest / Test::Unit assertions.
11
10
  #
12
- # Recognises every supported `assert_*` / `refute_*` /
13
- # `assert_not_*` shape (Minitest core + Test::Unit aliases),
14
- # plus the Minitest/spec `_(x).must_*` / `.wont_*` matchers
15
- # (`expect(x)` / `value(x)` wrappers covered too). For each
16
- # recognised assertion the plugin emits a Rigor
17
- # `post_return_fact` whose `:local`-kind target narrows the
18
- # named local on the post-call edge so downstream calls in
19
- # the same `it` / `test_` / `def test_*` body resolve
20
- # against the narrowed type.
11
+ # Recognises every supported `assert_*` / `refute_*` / `assert_not_*` shape (Minitest core + Test::Unit
12
+ # aliases), plus the Minitest/spec `_(x).must_*` / `.wont_*` matchers (`expect(x)` / `value(x)` wrappers
13
+ # covered too). For each recognised assertion the plugin emits a Rigor `post_return_fact` whose
14
+ # `:local`-kind target narrows the named local on the post-call edge so downstream calls in the same
15
+ # `it` / `test_` / `def test_*` body resolve against the narrowed type.
21
16
  #
22
17
  # ## Why a single plugin covers two frameworks
23
18
  #
24
- # Minitest and Test::Unit share the `assert_*` / `refute_*`
25
- # surface (Test::Unit's `assert_not_*` aliases are
26
- # recognised by the same rule table). The matchers_vaccine
27
- # gem layers RSpec-style matcher composition on top of
28
- # Minitest's `must` API and is covered by the spec-style
29
- # recogniser without additional wiring.
19
+ # Minitest and Test::Unit share the `assert_*` / `refute_*` surface (Test::Unit's `assert_not_*` aliases
20
+ # are recognised by the same rule table). The matchers_vaccine gem layers RSpec-style matcher composition
21
+ # on top of Minitest's `must` API and is covered by the spec-style recogniser without additional wiring.
30
22
  #
31
23
  # ## Configuration
32
24
  #
33
- # No configuration knobs. Activate via
34
- # `plugins: ["rigor-minitest"]` in `.rigor.yml`.
25
+ # No configuration knobs. Activate via `plugins: ["rigor-minitest"]` in `.rigor.yml`.
35
26
  #
36
27
  # ## Limitations
37
28
  #
38
- # - **No `assert_raises(T) { ... }`** — that's a block-shape
39
- # matcher and Rigor's narrowing model is for
29
+ # - **No `assert_raises(T) { ... }`** — that's a block-shape matcher and Rigor's narrowing model is for
40
30
  # straight-line locals.
41
- # - **No `assert_predicate(x, :foo?)`** — needs a
42
- # predicate-state carrier Rigor doesn't model today.
43
- # - **No `assert_respond_to(x, :m)`** — same shape gap
44
- # (respond-to-set carrier).
45
- # - **No legacy bare `x.must_be_kind_of(T)`** — the
46
- # receiver IS the value, so the analyzer has nothing to
31
+ # - **No `assert_predicate(x, :foo?)`** — needs a predicate-state carrier Rigor doesn't model today.
32
+ # - **No `assert_respond_to(x, :m)`** same shape gap (respond-to-set carrier).
33
+ # - **No legacy bare `x.must_be_kind_of(T)`** — the receiver IS the value, so the analyzer has nothing to
47
34
  # narrow against. Users should migrate to `_(x).must_*`.
48
- # - **No `assert_in_delta` / `assert_operator`** — float-
49
- # range / generic operator narrowing is future work.
50
- # - **No `assert_throws(:tag) { ... }` / `assert_raises`** —
51
- # block-form expectations need a separate slice.
35
+ # - **No `assert_in_delta` / `assert_operator`** — float-range / generic operator narrowing is future work.
36
+ # - **No `assert_throws(:tag) { ... }` / `assert_raises`** block-form expectations need a separate slice.
52
37
  class Minitest < Rigor::Plugin::Base
53
38
  manifest(
54
39
  id: "minitest",
55
40
  version: "0.1.0",
56
41
  description: "Narrows locals through Minitest / Test::Unit `assert_*` / `refute_*` " \
57
42
  "and Minitest/spec `_(x).must_*` / `.wont_*` matchers.",
58
- # ADR-38 — `def setup` runs before every `def test_*`, so
59
- # ivars it assigns are initialised by the time a test body
60
- # reads them. Declaring it an additional initializer stops
61
- # the read-before-write nil widening that would otherwise
62
- # type `@conn` (set in `setup`, read in a test) as
63
- # `T | nil` and surface a false nil-receiver diagnostic.
43
+ # ADR-38 — `def setup` runs before every `def test_*`, so ivars it assigns are initialised by the
44
+ # time a test body reads them. Declaring it an additional initializer stops the read-before-write
45
+ # nil widening that would otherwise type `@conn` (set in `setup`, read in a test) as `T | nil` and
46
+ # surface a false nil-receiver diagnostic.
64
47
  additional_initializers: [
65
48
  Rigor::Plugin::AdditionalInitializer.new(
66
49
  receiver_constraint: "Minitest::Test", methods: [:setup]
@@ -74,10 +57,8 @@ module Rigor
74
57
  ]
75
58
  )
76
59
 
77
- # ADR-37 slice 2 — emits `post_return_facts` for every recognised
78
- # assertion, method-gated by the engine. The engine routes
79
- # `:local`-kind facts through
80
- # `StatementEvaluator#apply_local_post_return_fact`.
60
+ # ADR-37 slice 2 — emits `post_return_facts` for every recognised assertion, method-gated by the
61
+ # engine. The engine routes `:local`-kind facts through `StatementEvaluator#apply_local_post_return_fact`.
81
62
  narrowing_facts methods: AssertionAnalyzer::SUPPORTED_METHODS do |call_node, scope|
82
63
  AssertionAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
83
64
  end
@@ -6,41 +6,32 @@ require "prism"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class Pundit < Rigor::Plugin::Base
9
- # Walks a parsed file's AST looking for Pundit
10
- # entry-point calls and validates each against the
9
+ # Walks a parsed file's AST looking for Pundit entry-point calls and validates each against the
11
10
  # {PolicyIndex}.
12
11
  #
13
12
  # Recognised shapes:
14
13
  #
15
- # - `authorize(record, :action)` — record's inferred
16
- # type → `<Type>Policy#<action>?` lookup. Both the
14
+ # - `authorize(record, :action)` — record's inferred type → `<Type>Policy#<action>?` lookup. Both the
17
15
  # policy class and the predicate must exist.
18
- # - `authorize(record)` — without an action argument,
19
- # we only validate that `<Type>Policy` exists. The
20
- # action name is determined at runtime from the
21
- # controller's current action; static validation
16
+ # - `authorize(record)` — without an action argument, we only validate that `<Type>Policy` exists. The
17
+ # action name is determined at runtime from the controller's current action; static validation
22
18
  # isn't possible without controller context.
23
- # - `policy(record)` / `policy_scope(scope)` — same
24
- # `<Type>Policy` existence check.
19
+ # - `policy(record)` / `policy_scope(scope)` — same `<Type>Policy` existence check.
25
20
  #
26
- # When the first argument's inferred type is NOT a
27
- # `Nominal[T]` (e.g. an untyped local variable), the
28
- # call is silently passed through. The plugin only
29
- # validates what it can prove from the static type
21
+ # When the first argument's inferred type is NOT a `Nominal[T]` (e.g. an untyped local variable), the
22
+ # call is silently passed through. The plugin only validates what it can prove from the static type
30
23
  # carrier.
31
24
  module Analyzer
32
25
  ENTRY_METHODS = %i[authorize policy policy_scope].freeze
33
26
 
34
- # One policy violation: the kebab-case `rule`, `severity`, and
35
- # human-readable `message`. Carries no path/location — the caller
36
- # (the `node_rule` block) positions it via `Plugin::Base#diagnostic`.
27
+ # One policy violation: the kebab-case `rule`, `severity`, and human-readable `message`. Carries no
28
+ # path/location — the caller (the `node_rule` block) positions it via `Plugin::Base#diagnostic`.
37
29
  Violation = Struct.new(:rule, :severity, :message, keyword_init: true)
38
30
 
39
31
  module_function
40
32
 
41
- # The policy violations for a single call node (0..2 of them), or
42
- # `[]` when the node is not a Pundit entry call or its record type
43
- # is not statically determinable. ADR-37: the engine owns the
33
+ # The policy violations for a single call node (0..2 of them), or `[]` when the node is not a
34
+ # Pundit entry call or its record type is not statically determinable. ADR-37: the engine owns the
44
35
  # walk, so this is per-node logic.
45
36
  #
46
37
  # @param call_node [Prism::Node]
@@ -71,15 +62,11 @@ module Rigor
71
62
  ENTRY_METHODS.include?(node.name) && node.receiver.nil?
72
63
  end
73
64
 
74
- # Resolves the first-argument expression to a policy
75
- # class name. The candidates are:
65
+ # Resolves the first-argument expression to a policy class name. The candidates are:
76
66
  # - `Foo` (a constant) → `FooPolicy`
77
- # - `Foo.method(...)` whose inferred type is
78
- # `Nominal[Bar]` → `BarPolicy`
79
- # - any other expression whose inferred type is
80
- # `Nominal[Bar]` → `BarPolicy`
81
- # Returns `nil` when the type isn't statically
82
- # determinable.
67
+ # - `Foo.method(...)` whose inferred type is `Nominal[Bar]` → `BarPolicy`
68
+ # - any other expression whose inferred type is `Nominal[Bar]` → `BarPolicy`
69
+ # Returns `nil` when the type isn't statically determinable.
83
70
  def derive_policy_class_name(record_node, scope)
84
71
  if record_node.is_a?(Prism::ConstantReadNode) || record_node.is_a?(Prism::ConstantPathNode)
85
72
  constant_name = constant_receiver_name(record_node)
@@ -118,10 +105,8 @@ module Rigor
118
105
  )
119
106
  end
120
107
 
121
- # Validates the `authorize(record, :action)` form.
122
- # Returns nil when the call has no second argument
123
- # (the runtime infers it from the controller — out
124
- # of scope here) or when the second argument isn't
108
+ # Validates the `authorize(record, :action)` form. Returns nil when the call has no second argument
109
+ # (the runtime infers it from the controller out of scope here) or when the second argument isn't
125
110
  # a literal symbol / string.
126
111
  def action_violation(call_node, policy_entry)
127
112
  args = call_node.arguments&.arguments || []
@@ -7,22 +7,17 @@ require_relative "policy_index"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class Pundit < Rigor::Plugin::Base
10
- # Walks the configured policy-search paths via the
11
- # plugin's `IoBoundary`, parses each `.rb` file with
12
- # Prism, and collects classes whose immediate
13
- # superclass is one of the configured base classes.
10
+ # Walks the configured policy-search paths via the plugin's `IoBoundary`, parses each `.rb` file with
11
+ # Prism, and collects classes whose immediate superclass is one of the configured base classes.
14
12
  #
15
- # For each discovered policy class, the discoverer
16
- # collects every instance-side `def name?` predicate
17
- # method. Non-predicate methods (`initialize`,
18
- # `resolve`, helper methods) are ignored — Pundit's
13
+ # For each discovered policy class, the discoverer collects every instance-side `def name?` predicate
14
+ # method. Non-predicate methods (`initialize`, `resolve`, helper methods) are ignored — Pundit's
19
15
  # `authorize` only ever calls predicate methods.
20
16
  #
21
17
  # Limitations (intentional for v0.1.0):
22
18
  #
23
19
  # - Direct-superclass match only.
24
- # - Predicate methods are read from the syntactic
25
- # `def` list. Methods built via `define_method` /
20
+ # - Predicate methods are read from the syntactic `def` list. Methods built via `define_method` /
26
21
  # inherited from a sibling concern are out of scope.
27
22
  class PolicyDiscoverer
28
23
  def initialize(io_boundary:, search_paths:, base_classes:)
@@ -120,8 +115,7 @@ module Rigor
120
115
  end
121
116
  end
122
117
 
123
- # Returns symbolic predicate names (`:update?`,
124
- # `:show?`, …) defined on the policy. Only
118
+ # Returns symbolic predicate names (`:update?`, `:show?`, …) defined on the policy. Only
125
119
  # instance-side names that end in `?` are recorded.
126
120
  def collect_predicate_methods(body)
127
121
  return [] if body.nil?
@@ -3,14 +3,11 @@
3
3
  module Rigor
4
4
  module Plugin
5
5
  class Pundit < Rigor::Plugin::Base
6
- # Frozen catalogue of discovered Pundit policy classes
7
- # keyed by policy class name (e.g. `"PostPolicy"`).
8
- # Each entry tracks the set of predicate methods
9
- # defined on the policy (instance-side `def name?`)
6
+ # Frozen catalogue of discovered Pundit policy classes keyed by policy class name (e.g. `"PostPolicy"`).
7
+ # Each entry tracks the set of predicate methods defined on the policy (instance-side `def name?`)
10
8
  # plus the source file path.
11
9
  #
12
- # The analyzer maps a record's inferred type
13
- # (`Nominal[Post]`) to the policy class name
10
+ # The analyzer maps a record's inferred type (`Nominal[Post]`) to the policy class name
14
11
  # (`"PostPolicy"`) and looks up the predicate.
15
12
  class PolicyIndex
16
13
  Entry = Data.define(:policy_class_name, :file_path, :predicate_methods) do
@@ -22,8 +19,7 @@ module Rigor
22
19
  predicate_methods.to_a.sort
23
20
  end
24
21
 
25
- # Normalises an action symbol / string by ensuring
26
- # a trailing `?`. `:update` and `:update?` both
22
+ # Normalises an action symbol / string by ensuring a trailing `?`. `:update` and `:update?` both
27
23
  # resolve to `update?`.
28
24
  def normalize(name)
29
25
  string = name.to_s
@@ -8,13 +8,11 @@ require_relative "pundit/analyzer"
8
8
 
9
9
  module Rigor
10
10
  module Plugin
11
- # rigor-pundit — validates Pundit `authorize` /
12
- # `policy` / `policy_scope` calls against the project's
11
+ # rigor-pundit — validates Pundit `authorize` / `policy` / `policy_scope` calls against the project's
13
12
  # `app/policies/` tree.
14
13
  #
15
14
  # Tier 3B of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
16
- # Statically discovers policy classes by walking
17
- # `policy_search_paths` and parsing each file with
15
+ # Statically discovers policy classes by walking `policy_search_paths` and parsing each file with
18
16
  # Prism — no `pundit` runtime dependency.
19
17
  #
20
18
  # ## Configuration
@@ -27,34 +25,22 @@ module Rigor
27
25
  #
28
26
  # ## What it checks
29
27
  #
30
- # 1. **Policy class existence** — `authorize(record, ...)`
31
- # looks up `<inferred-type>Policy` in the index.
32
- # Missing policies emit `unknown-policy-class` with
33
- # a did-you-mean suggestion.
34
- # 2. **Predicate method existence** — for the
35
- # `authorize(record, :action)` form, validates that
36
- # `<Policy>#<action>?` is defined. Missing methods
37
- # emit `unknown-policy-method` listing the known
28
+ # 1. **Policy class existence** — `authorize(record, ...)` looks up `<inferred-type>Policy` in the index.
29
+ # Missing policies emit `unknown-policy-class` with a did-you-mean suggestion.
30
+ # 2. **Predicate method existence** — for the `authorize(record, :action)` form, validates that
31
+ # `<Policy>#<action>?` is defined. Missing methods emit `unknown-policy-method` listing the known
38
32
  # predicates.
39
33
  #
40
34
  # ## Limitations (v0.1.0)
41
35
  #
42
- # - Records whose inferred type is NOT a `Nominal[T]`
43
- # (untyped local variables, untyped instance
44
- # variables) are silently passed through. The plugin
45
- # only validates what it can prove from the static
46
- # carrier.
47
- # - The two-argument form
48
- # `authorize(record, :action_symbol)` is the only
49
- # one validated. The implicit form
50
- # `authorize(record)` (which Pundit resolves at
51
- # runtime against the controller's current action) is
52
- # passed through with the policy-class check only.
53
- # - Direct-superclass match for `policy_base_classes`.
54
- # Indirect inheritance (`AdminPolicy <
55
- # ApplicationPolicy`) needs `AdminPolicy` listed in
56
- # `policy_base_classes` if subclasses inherit from
57
- # it.
36
+ # - Records whose inferred type is NOT a `Nominal[T]` (untyped local variables, untyped instance
37
+ # variables) are silently passed through. The plugin only validates what it can prove from the
38
+ # static carrier.
39
+ # - The two-argument form `authorize(record, :action_symbol)` is the only one validated. The implicit
40
+ # form `authorize(record)` (which Pundit resolves at runtime against the controller's current action)
41
+ # is passed through with the policy-class check only.
42
+ # - Direct-superclass match for `policy_base_classes`. Indirect inheritance (`AdminPolicy <
43
+ # ApplicationPolicy`) needs `AdminPolicy` listed in `policy_base_classes` if subclasses inherit from it.
58
44
  class Pundit < Rigor::Plugin::Base
59
45
  manifest(
60
46
  id: "pundit",
@@ -79,10 +65,9 @@ module Rigor
79
65
  @policy_base_classes = Array(config.fetch("policy_base_classes")).map(&:to_s)
80
66
  end
81
67
 
82
- # File-level only: the load-error emission. The per-call policy
83
- # validation runs over the engine-owned walk via the node_rule
84
- # below (ADR-37). The index is lazily loaded + memoised by
85
- # `producer_value`, so both surfaces share one load.
68
+ # File-level only: the load-error emission. The per-call policy validation runs over the engine-owned
69
+ # walk via the node_rule below (ADR-37). The index is lazily loaded + memoised by `producer_value`,
70
+ # so both surfaces share one load.
86
71
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
87
72
  index = producer_value(:policy_index)
88
73
  return [load_error_diagnostic(path)] if index.nil? && producer_error(:policy_index)
@@ -1,26 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Convenience entry point. `require "rigor-rails"` requires
4
- # every Tier 1+2 Rails ecosystem plugin in one go, so projects
5
- # that prefer a single require statement (some `spec_helper`
6
- # patterns, ad-hoc scripts) do not have to list seven require
7
- # lines.
3
+ # Convenience entry point. `require "rigor-rails"` requires every Tier 1+2 Rails ecosystem plugin in one go,
4
+ # so projects that prefer a single require statement (some `spec_helper` patterns, ad-hoc scripts) do not
5
+ # have to list seven require lines.
8
6
  #
9
- # Note: requiring this entry point does NOT mark every plugin as
10
- # active. The Rigor plugin loader walks `.rigor.yml`'s `plugins:`
11
- # list and instantiates only the plugins enumerated there. This
12
- # is per ADR-12 WD1's "Gemfile-convenience meta-gem" pattern —
13
- # users still control which plugins participate in analysis via
14
- # `.rigor.yml`.
7
+ # Note: requiring this entry point does NOT mark every plugin as active. The Rigor plugin loader walks
8
+ # `.rigor.yml`'s `plugins:` list and instantiates only the plugins enumerated there. This is per ADR-12
9
+ # WD1's "Gemfile-convenience meta-gem" pattern users still control which plugins participate in analysis
10
+ # via `.rigor.yml`.
15
11
  #
16
- # Sub-plugins ARE registered with `Rigor::Plugin` when this file
17
- # loads (each gem's entry point side-effects a `Plugin.register`
18
- # call); the loader's lookup phase finds them by id when listed.
12
+ # Sub-plugins ARE registered with `Rigor::Plugin` when this file loads (each gem's entry point side-effects
13
+ # a `Plugin.register` call); the loader's lookup phase finds them by id when listed.
19
14
  #
20
- # Adding the gem to a project's Gemfile without listing any
21
- # plugin in `.rigor.yml` is harmless: the requires happen on
22
- # `Bundler.require`, but no plugin's `init` / `prepare` / hooks
23
- # run.
15
+ # Adding the gem to a project's Gemfile without listing any plugin in `.rigor.yml` is harmless: the requires
16
+ # happen on `Bundler.require`, but no plugin's `init` / `prepare` / hooks run.
24
17
 
25
18
  require "rigor-rails-routes"
26
19
  require "rigor-rails-i18n"
@@ -6,85 +6,64 @@ require "prism"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class RailsI18n < Rigor::Plugin::Base
9
- # Walks a parsed file's AST looking for `t(...)` /
10
- # `I18n.t(...)` / `I18n.translate(...)` calls with a
11
- # literal-string first argument. Calls with a non-literal
12
- # key (variable, expression) are silently passed through —
13
- # the plugin only validates what it can prove statically.
9
+ # Walks a parsed file's AST looking for `t(...)` / `I18n.t(...)` / `I18n.translate(...)` calls with a
10
+ # literal-string first argument. Calls with a non-literal key (variable, expression) are silently
11
+ # passed through the plugin only validates what it can prove statically.
14
12
  #
15
13
  # ## What gets emitted per recognised call
16
14
  #
17
- # - `plugin.rails-i18n.translation-call` (info) names the
18
- # key and the locales it resolves in.
19
- # - `plugin.rails-i18n.unknown-key` (error) when the key
20
- # is missing from every loaded locale; the message
21
- # includes a did-you-mean suggestion drawn from the
22
- # index.
23
- # - `plugin.rails-i18n.missing-locale` (warning) when the
24
- # key resolves in some configured locales but is absent
25
- # from at least one. Suppressed when the call passes
26
- # `default:` (the user has signalled they're aware of
27
- # the partial coverage).
28
- # - `plugin.rails-i18n.wrong-interpolation` (error) when
29
- # the call's interpolation hash uses keys that don't
30
- # match the value's `%{var}` placeholders, or omits a
31
- # required placeholder.
15
+ # - `plugin.rails-i18n.translation-call` (info) names the key and the locales it resolves in.
16
+ # - `plugin.rails-i18n.unknown-key` (error) when the key is missing from every loaded locale; the
17
+ # message includes a did-you-mean suggestion drawn from the index.
18
+ # - `plugin.rails-i18n.missing-locale` (warning) when the key resolves in some configured locales but
19
+ # is absent from at least one. Suppressed when the call passes `default:` (the user has signalled
20
+ # they're aware of the partial coverage).
21
+ # - `plugin.rails-i18n.wrong-interpolation` (error) when the call's interpolation hash uses keys that
22
+ # don't match the value's `%{var}` placeholders, or omits a required placeholder.
32
23
  module Analyzer
33
24
  TRANSLATE_METHODS = %i[t translate].freeze
34
25
 
35
- # Methods that are always I18n receivers (`I18n.t`,
36
- # `::I18n.t`).
26
+ # Methods that are always I18n receivers (`I18n.t`, `::I18n.t`).
37
27
  I18N_RECEIVER_NAMES = %w[I18n ::I18n].freeze
38
28
 
39
- # Matches controller file paths so lazy keys (`.key`)
40
- # can be expanded to `<controller_scope>.<action>.<key>`.
41
- # Captures the path segment between `controllers/` and
29
+ # Matches controller file paths so lazy keys (`.key`) can be expanded to
30
+ # `<controller_scope>.<action>.<key>`. Captures the path segment between `controllers/` and
42
31
  # `_controller.rb` (e.g. `users`, `admin/users`).
43
32
  CONTROLLER_PATH_RE = %r{(?:^|/)controllers/(.+)_controller\.rb$}
44
33
 
45
- # Matches Rails view-template file paths to derive the
46
- # I18n "virtual path" the scope that Rails uses for
47
- # lazy `t('.key')` lookups inside a template.
34
+ # Matches Rails view-template file paths to derive the I18n "virtual path" — the scope that Rails
35
+ # uses for lazy `t('.key')` lookups inside a template.
48
36
  #
49
- # Captures the path segment between `views/` and the
50
- # format+variant+handler suffix, then strips a leading
51
- # underscore from each segment — Rails templates resolve
52
- # `_form.html.erb` as "form", not "_form".
37
+ # Captures the path segment between `views/` and the format+variant+handler suffix, then strips a
38
+ # leading underscore from each segment — Rails templates resolve `_form.html.erb` as "form", not
39
+ # "_form".
53
40
  #
54
41
  # app/views/setting/index.html.erb → setting.index
55
42
  # app/views/admin/users/new.html.erb → admin.users.new
56
43
  # app/views/home/index.html+mobile.erb → home.index
57
44
  # app/views/users/_form.html.erb → users.form
58
45
  #
59
- # The view-scope lazy expansion replaces the action
60
- # part with the view's virtual path:
46
+ # The view-scope lazy expansion replaces the action part with the view's virtual path:
61
47
  # `<%= t('.title') %>` → `setting.index.title`
62
48
  VIEW_SCOPE_RE = %r{views/(.+?)\.(?:\w+)(?:\+\w+)?\.\w+\z}
63
49
 
64
- # Regex to extract lazy-key arguments from ERB / HTML
65
- # template content. Matches `t('.key')`, `t(".key")`,
66
- # `I18n.t('.key')`, and `I18n.translate('.key')` with a
67
- # leading dot on the key string. Captures only the key
68
- # part after the dot.
50
+ # Regex to extract lazy-key arguments from ERB / HTML template content. Matches `t('.key')`,
51
+ # `t(".key")`, `I18n.t('.key')`, and `I18n.translate('.key')` with a leading dot on the key string.
52
+ # Captures only the key part after the dot.
69
53
  LAZY_T_KEY_RE = /\b(?:I18n\.)?(?:t|translate)\s*\(\s*(?:"\.([^"\\]*)"|'\.([^'\\]*)')/
70
54
 
71
- # Reserved option keys — these are recognised by I18n
72
- # itself and not treated as interpolation variables.
55
+ # Reserved option keys — these are recognised by I18n itself and not treated as interpolation
56
+ # variables.
73
57
  RESERVED_OPTION_KEYS = %i[
74
58
  default scope locale count raise throw fallback
75
59
  fallback_in_progress separator deep_interpolation
76
60
  ].to_set.freeze
77
61
 
78
- # Key prefixes Rails / `rails-i18n` ship in every
79
- # locale by default. Projects whose own locale files
80
- # don't redeclare them still get them at runtime (via
81
- # the `rails-i18n` gem's bundled locale catalogues).
82
- # `t('date.order')` is the canonical Mastodon case
83
- # used by `Settings::Date::Order` and date-of-birth
84
- # selects, never authored project-side. Skip
85
- # `unknown-key` for these; downstream interpolation
86
- # checks have nothing to validate without a leaf entry
87
- # so they decline silently too.
62
+ # Key prefixes Rails / `rails-i18n` ship in every locale by default. Projects whose own locale
63
+ # files don't redeclare them still get them at runtime (via the `rails-i18n` gem's bundled locale
64
+ # catalogues). `t('date.order')` is the canonical Mastodon case used by `Settings::Date::Order`
65
+ # and date-of-birth selects, never authored project-side. Skip `unknown-key` for these; downstream
66
+ # interpolation checks have nothing to validate without a leaf entry so they decline silently too.
88
67
  RAILS_SHIPPED_KEY_PREFIXES = %w[
89
68
  date.
90
69
  time.
@@ -101,9 +80,8 @@ module Rigor
101
80
  activerecord.errors.models.
102
81
  ].freeze
103
82
 
104
- # One translation-call observation. Carries no path/location —
105
- # the caller (the `node_rule` block) positions it via
106
- # `Plugin::Base#diagnostic`.
83
+ # One translation-call observation. Carries no path/location — the caller (the `node_rule` block)
84
+ # positions it via `Plugin::Base#diagnostic`.
107
85
  Violation = Struct.new(:rule, :severity, :message, keyword_init: true)
108
86
 
109
87
  module_function
@@ -118,12 +96,10 @@ module Rigor
118
96
  # @param locale_index [LocaleIndex]
119
97
  # @param configured_locales [Array<String>]
120
98
  # @return [Array<Diagnostic>]
121
- # The translation violations for a single call node, or `[]`
122
- # when it is not a recognised `t` / `translate` call with a
123
- # literal key. ADR-37: the engine owns the walk; `action` (the
124
- # enclosing method, for lazy `t('.key')` expansion) comes from
125
- # the node-rule `NodeContext`, and `controller_scope` from the
126
- # file path.
99
+ # The translation violations for a single call node, or `[]` when it is not a recognised `t` /
100
+ # `translate` call with a literal key. ADR-37: the engine owns the walk; `action` (the enclosing
101
+ # method, for lazy `t('.key')` expansion) comes from the node-rule `NodeContext`, and
102
+ # `controller_scope` from the file path.
127
103
  #
128
104
  # @param call_node [Prism::Node]
129
105
  # @param locale_index [LocaleIndex]
@@ -143,11 +119,11 @@ module Rigor
143
119
  options = options_hash(call_node)
144
120
  entry = locale_index.find(literal_key)
145
121
  if entry.nil?
146
- # CLDR pluralization namespace: `t('accounts.posts', count: n)`
147
- # resolves through a `.one` / `.other` child — not missing.
122
+ # CLDR pluralization namespace: `t('accounts.posts', count: n)` resolves through a `.one` /
123
+ # `.other` child — not missing.
148
124
  return [] if locale_index.pluralization_namespace?(literal_key)
149
- # Rails / rails-i18n ship `date.order`, `time.am`, etc. in
150
- # every locale at runtime even when project files omit them.
125
+ # Rails / rails-i18n ship `date.order`, `time.am`, etc. in every locale at runtime even when
126
+ # project files omit them.
151
127
  return [] if rails_shipped_key?(literal_key)
152
128
 
153
129
  return [unknown_key_violation(literal_key, locale_index)]
@@ -162,9 +138,8 @@ module Rigor
162
138
  violations
163
139
  end
164
140
 
165
- # Derives the Rails controller scope from the file path,
166
- # e.g. `app/controllers/admin/users_controller.rb` → `admin.users`.
167
- # Returns nil for non-controller paths.
141
+ # Derives the Rails controller scope from the file path, e.g.
142
+ # `app/controllers/admin/users_controller.rb` → `admin.users`. Returns nil for non-controller paths.
168
143
  def controller_scope_from_path(path)
169
144
  m = CONTROLLER_PATH_RE.match(path.to_s)
170
145
  return nil unless m
@@ -198,12 +173,10 @@ module Rigor
198
173
  violations
199
174
  end
200
175
 
201
- # Expands a lazy key (starting with `.`) to its full
202
- # dotted path using the controller scope and action name.
203
- # Returns the raw key unchanged for absolute keys.
204
- # Returns nil for lazy keys outside a controller context
205
- # or without an enclosing action — these are silently
206
- # skipped to avoid false positives.
176
+ # Expands a lazy key (starting with `.`) to its full dotted path using the controller scope and
177
+ # action name. Returns the raw key unchanged for absolute keys. Returns nil for lazy keys outside a
178
+ # controller context or without an enclosing action — these are silently skipped to avoid false
179
+ # positives.
207
180
  def expand_key(raw_key, controller_scope:, action:)
208
181
  return raw_key unless raw_key.start_with?(".")
209
182
  return nil unless controller_scope && action
@@ -219,9 +192,8 @@ module Rigor
219
192
  I18N_RECEIVER_NAMES.include?(receiver_name)
220
193
  end
221
194
 
222
- # Extracts the literal-string first argument when
223
- # present. Returns nil for variable / expression keys —
224
- # only literal keys are statically validated.
195
+ # Extracts the literal-string first argument when present. Returns nil for variable / expression
196
+ # keys only literal keys are statically validated.
225
197
  def literal_key_for(call_node)
226
198
  args = call_node.arguments&.arguments || []
227
199
  return nil if args.empty?
@@ -232,11 +204,9 @@ module Rigor
232
204
  first.unescaped
233
205
  end
234
206
 
235
- # Pulls the interpolation hash from the call's
236
- # arguments. The trailing `Hash` argument (or
237
- # `Prism::KeywordHashNode`) carries both reserved I18n
238
- # options (`default:`, `scope:`, …) and interpolation
239
- # variables. Returns:
207
+ # Pulls the interpolation hash from the call's arguments. The trailing `Hash` argument (or
208
+ # `Prism::KeywordHashNode`) carries both reserved I18n options (`default:`, `scope:`, …) and
209
+ # interpolation variables. Returns:
240
210
  # {
241
211
  # :has_default => bool,
242
212
  # :all_keys => Set<Symbol> (every assoc key in the hash),
@@ -244,15 +214,11 @@ module Rigor
244
214
  # :hash_node => Prism::Node (or nil)
245
215
  # }
246
216
  #
247
- # Note: a reserved option key (e.g. `count:`) can
248
- # also serve as an interpolation value when the
249
- # locale's leaf string has `%{count}`. The analyzer
250
- # therefore checks the missing-placeholder set
251
- # against `all_keys` (so `count:` satisfies a
252
- # `%{count}` placeholder) and the
253
- # extra-placeholder set against `non_reserved` (so
254
- # `default:` / `scope:` are never reported as
255
- # extra interpolation arguments).
217
+ # Note: a reserved option key (e.g. `count:`) can also serve as an interpolation value when the
218
+ # locale's leaf string has `%{count}`. The analyzer therefore checks the missing-placeholder set
219
+ # against `all_keys` (so `count:` satisfies a `%{count}` placeholder) and the extra-placeholder set
220
+ # against `non_reserved` (so `default:` / `scope:` are never reported as extra interpolation
221
+ # arguments).
256
222
  def options_hash(call_node)
257
223
  args = call_node.arguments&.arguments || []
258
224
  last = args.last
@@ -275,8 +241,8 @@ module Rigor
275
241
  end
276
242
 
277
243
  def collect_assoc_keys(hash_node)
278
- # Both `Prism::HashNode` and `Prism::KeywordHashNode`
279
- # expose `#elements`, so a single path handles both.
244
+ # Both `Prism::HashNode` and `Prism::KeywordHashNode` expose `#elements`, so a single path
245
+ # handles both.
280
246
  hash_node.elements.filter_map do |element|
281
247
  next nil unless element.is_a?(Prism::AssocNode)
282
248