rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -19,9 +19,9 @@ module Rigor
19
19
  # - Reads the instance-side `def` nodes and records each one as an action method, capturing the
20
20
  # arity envelope.
21
21
  # - For each (class, action) pair, attempts to read every candidate view template under
22
- # `app/views/<mailer_underscore>/<action>.{html,text}.erb`. Existing templates feed the
22
+ # `app/views/<mailer_underscore>/<action>.{html,text}.{erb,haml,slim}`. Existing templates feed the
23
23
  # IoBoundary's cache descriptor (so the cache invalidates when the template changes); missing
24
- # templates are recorded so the plugin can surface a diagnostic on the mailer class definition.
24
+ # templates are recorded so the plugin can surface a diagnostic on the action's `def`.
25
25
  #
26
26
  # Limitations (intentional for v0.1.0):
27
27
  #
@@ -30,9 +30,6 @@ module Rigor
30
30
  # - Action methods are read from the syntactic instance-side `def` list. Methods built via
31
31
  # `define_method`, `private`, or non-action helpers (e.g. methods starting with `_`) are out of
32
32
  # scope. The discoverer filters obvious non-actions (`initialize`, names prefixed with `_`).
33
- # - Adding a brand-new view file under `app/views/<mailer>/` will NOT invalidate the cached index
34
- # until something the mailer file touches changes. This is the standard read-tracking trade-off
35
- # — only files we successfully read get digested into the descriptor.
36
33
  class MailerDiscoverer
37
34
  DEFAULT_VIEWS_ROOT = "app/views"
38
35
  VIEW_FORMATS = %w[html text].freeze
@@ -56,22 +53,22 @@ module Rigor
56
53
  # Two-pass: first collect every module's defs (for the include-following step), then build
57
54
  # per-class entries that pull in actions from include'd modules. GitLab's `Notify` mailer
58
55
  # derives every action from `Emails::*` concerns under `app/mailers/emails/`.
59
- module_actions = {} # module_fqn => Hash<Symbol, ActionEntry>
60
- class_visits = [] # collected (class_name, path, def_nodes, includes)
56
+ module_actions = {} # fully-qualified module name => its action entries, keyed by method name
57
+ class_visits = [] # collected (class_name, path, def_nodes, includes, view_exempt)
61
58
 
62
59
  ruby_files_under(@search_paths).each do |path|
63
60
  contents = read_safely(path)
64
61
  next if contents.nil?
65
62
 
66
63
  tree = Prism.parse(contents).value
67
- walk_for_mailers(tree, []) do |class_name, def_nodes, includes|
68
- class_visits << [class_name, path, def_nodes, includes]
64
+ walk_for_mailers(tree, []) do |class_name, def_nodes, includes, view_exempt|
65
+ class_visits << [class_name, path, def_nodes, includes, view_exempt]
69
66
  end
70
67
  collect_module_actions(tree, [], module_actions)
71
68
  end
72
69
 
73
- entries = class_visits.map do |class_name, path, def_nodes, includes|
74
- build_class_entry(class_name, path, def_nodes, includes, module_actions)
70
+ entries = class_visits.map do |class_name, path, def_nodes, includes, view_exempt|
71
+ build_class_entry(class_name, path, def_nodes, includes, module_actions, view_exempt)
75
72
  end
76
73
  MailerIndex.new(entries)
77
74
  end
@@ -112,9 +109,9 @@ module Rigor
112
109
  full_name = declared_constant_name(class_local_name, lexical_path)
113
110
  superclass = strip_root(constant_path_name(node.superclass)) if node.superclass
114
111
  if superclass && @base_classes.include?(superclass)
115
- def_nodes = collect_action_defs(node.body)
112
+ def_nodes, view_exempt = collect_action_defs(node.body)
116
113
  includes = collect_includes(node.body)
117
- yield full_name, def_nodes, includes
114
+ yield full_name, def_nodes, includes, view_exempt
118
115
  end
119
116
 
120
117
  walk_for_mailers(node.body, [full_name], &) if node.body
@@ -139,8 +136,7 @@ module Rigor
139
136
  end
140
137
 
141
138
  # Walks the AST collecting every module's instance-side def nodes by fully-qualified module
142
- # name. The same `collect_action_defs` filter applies (private / `_`-prefixed / callback-target
143
- # methods skipped).
139
+ # name. The same `collect_action_defs` filter applies (private / `_`-prefixed skipped).
144
140
  def collect_module_actions(node, lexical_path, accumulator)
145
141
  return if node.nil?
146
142
 
@@ -151,8 +147,7 @@ module Rigor
151
147
 
152
148
  full_name = declared_constant_name(local_name, lexical_path)
153
149
  if node.body
154
- def_nodes = collect_action_defs(node.body)
155
- entries = def_nodes.to_h { |def_node| [def_node.name, build_action_entry(def_node)] }
150
+ entries = collect_action_defs(node.body).first.to_h { |def_node| [def_node.name, build_action_entry(def_node)] }
156
151
  # UNIONed, never assigned: a concern module reopened in a second file adds actions rather
157
152
  # than replacing the ones the first declaration spelled (same rule as {MailerIndex}'s
158
153
  # per-class merge).
@@ -216,80 +211,102 @@ module Rigor
216
211
  # - `initialize`
217
212
  # - methods starting with `_` (Ruby convention for private/internal)
218
213
  # - `def self.<name>` (singleton-side)
219
- # - methods after a bare `private` (or `public` → `private` transition) — these are internal
220
- # helpers, not actions
221
- # - methods named as a `private :foo` argument
222
- # - methods named as a callback target (`before_action :name`, `after_action`, `around_action`)
214
+ # - methods that are not public at the end of the class body, read in source order: after a bare
215
+ # `private` / `protected` (until a `public` switches back), named by `private :foo` /
216
+ # `protected :foo`, or defined inline (`private def foo`) — Rails builds `action_methods` from the
217
+ # public instance methods. `public :foo` after a `protected` section makes `foo` an action again.
218
+ # Methods named by a callback or `helper_method` macro (see {#collect_callback_names}) stay actions:
219
+ # Rails dispatches any public instance method, so `Mailer.target` still runs. They are returned as the
220
+ # view-exempt names, which exempt them from the missing-view check only.
223
221
  #
224
222
  # Pre-fix, Mastodon's `AdminMailer#process_params` / `set_instance` / `set_locale` /
225
223
  # `set_important_headers!` all surfaced as missing-view because the bare `private` keyword
226
224
  # wasn't honoured. ~19 false positives across Mastodon's mailers.
227
- CALLBACK_DECLARATIONS = %i[before_action after_action around_action].freeze
228
- private_constant :CALLBACK_DECLARATIONS
225
+ VISIBILITY_KEYWORDS = %i[public private protected module_function].freeze
226
+ private_constant :VISIBILITY_KEYWORDS
229
227
 
228
+ # Returns `[action_def_nodes, view_exempt_names]`. The view-exempt names are the callback and
229
+ # `helper_method` targets of the body: they remain actions, and only the missing-view check skips them.
230
230
  def collect_action_defs(body)
231
- return [] if body.nil?
231
+ return [[], Set[]] if body.nil?
232
232
 
233
- private_names, callback_names = collect_visibility_and_callbacks(body)
234
- visibility = :public
233
+ callback_names = collect_callback_names(body)
234
+ def_nodes, final_visibility = scan_visibility(body)
235
235
 
236
- body.compact_child_nodes.flat_map do |node|
237
- visibility = next_visibility(node, visibility)
238
- next [] unless node.is_a?(Prism::DefNode)
239
- next [] if node.receiver.is_a?(Prism::SelfNode)
240
- next [] if node.name == :initialize
241
- next [] if node.name.to_s.start_with?("_")
242
- next [] if visibility == :private
243
- next [] if private_names.include?(node.name)
244
- next [] if callback_names.include?(node.name)
236
+ actions = def_nodes.select do |node|
237
+ next false if node.receiver.is_a?(Prism::SelfNode)
238
+ next false if node.name == :initialize
239
+ next false if node.name.to_s.start_with?("_")
245
240
 
246
- [node]
241
+ # Rails builds `action_methods` from the PUBLIC instance methods, so only a method whose final
242
+ # visibility is public is an action.
243
+ final_visibility[node.name] == :public
247
244
  end
245
+ [actions, callback_names]
248
246
  end
249
247
 
250
- # First pass over the class body: collect (a) names passed to `private :foo` / `protected :foo`
251
- # (explicit visibility-on-existing-method form), and (b) Symbol arguments to callback
252
- # declarations (`before_action :setup`, etc.).
253
- def collect_visibility_and_callbacks(body)
254
- private_names = []
255
- callback_names = []
256
-
248
+ # Symbol arguments to the macros the manifest's `per_invocation_non_action_macros` lists (`before_action
249
+ # :setup`, `after_deliver :record`, `helper_method :fmt`). The `skip_*` forms are left out, since
250
+ # `skip_before_action :x` names a callback a parent defined and does not exempt `x` from anything (#2319).
251
+ def collect_callback_names(body)
252
+ macros = Actionmailer.manifest.per_invocation_non_action_macros.reject { |macro| macro.start_with?("skip_") }
253
+ names = []
257
254
  body.rigor_each_child do |node|
258
- next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
255
+ next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && macros.include?(node.name)
259
256
 
260
- args = (node.arguments&.arguments || []).filter_map do |arg|
261
- Rigor::Source::Literals.symbol(arg)
257
+ (node.arguments&.arguments || []).each do |arg|
258
+ symbol = Rigor::Source::Literals.symbol(arg)
259
+ names << symbol if symbol
262
260
  end
263
- next if args.empty?
261
+ end
262
+ names.to_set
263
+ end
264
264
 
265
- case node.name
266
- when :private, :protected then private_names.concat(args)
267
- when *CALLBACK_DECLARATIONS then callback_names.concat(args)
265
+ # Walks the class body in source order and returns `[def_nodes, final_visibility]`: every `def` the body
266
+ # spells (a direct statement or the inline argument of a visibility call, `private def x`), and the
267
+ # visibility each method name ends with. A bare `public` / `private` / `protected` / `module_function` sets
268
+ # the default for the defs after it; `private :x` / `public :x` / `protected def x` set that one method, whichever side of
269
+ # its `def` they fall on in the source, and the last statement to name a method wins.
270
+ def scan_visibility(body)
271
+ def_nodes = []
272
+ final_visibility = {}
273
+ default = :public
274
+
275
+ body.compact_child_nodes.each do |node|
276
+ case node
277
+ when Prism::DefNode
278
+ def_nodes << node
279
+ final_visibility[node.name] = default
280
+ when Prism::CallNode
281
+ default = apply_visibility_call(node, default, def_nodes, final_visibility)
268
282
  end
269
283
  end
270
284
 
271
- [private_names.to_set, callback_names.to_set]
285
+ [def_nodes, final_visibility]
272
286
  end
273
287
 
274
- # Returns the new visibility scope state after observing `node`. Bare `private` / `protected` /
275
- # `public` switch state; the `private :foo` arg-bearing form does NOT (already handled by
276
- # `collect_visibility_and_callbacks`).
277
- def next_visibility(node, current)
278
- return current unless node.is_a?(Prism::CallNode)
279
- return current unless node.receiver.nil?
280
- return current unless (args = node.arguments&.arguments).nil? || args.empty?
281
-
282
- case node.name
283
- when :private then :private
284
- when :protected then :protected
285
- when :public then :public
286
- else current
288
+ # Applies one receiverless `public` / `private` / `protected` / `module_function` call and returns the
289
+ # (possibly new) default.
290
+ def apply_visibility_call(node, default, def_nodes, final_visibility)
291
+ return default unless node.receiver.nil? && VISIBILITY_KEYWORDS.include?(node.name)
292
+
293
+ args = node.arguments&.arguments || []
294
+ return node.name if args.empty?
295
+
296
+ args.flat_map { |arg| arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg] }.each do |arg|
297
+ if arg.is_a?(Prism::DefNode)
298
+ def_nodes << arg
299
+ final_visibility[arg.name] = node.name
300
+ elsif (name = Rigor::Source::Literals.symbol_or_string(arg))
301
+ final_visibility[name] = node.name
302
+ end
287
303
  end
304
+ default
288
305
  end
289
306
 
290
- def build_class_entry(class_name, file_path, def_nodes, includes = [], module_actions = {})
307
+ def build_class_entry(class_name, file_path, def_nodes, includes = [], module_actions = {}, view_exempt = Set[])
291
308
  actions = def_nodes.to_h do |def_node|
292
- entry = build_action_entry(def_node)
309
+ entry = build_action_entry(def_node, view_exempt: view_exempt.include?(def_node.name))
293
310
  [entry.method_name, entry]
294
311
  end
295
312
 
@@ -309,7 +326,7 @@ module Rigor
309
326
  end
310
327
  end
311
328
 
312
- missing_views = actions.keys.reject { |action| view_exists?(class_name, action) }
329
+ missing_views = actions.reject { |_, entry| entry.view_exempt }.keys.reject { |action| view_exists?(class_name, action) }
313
330
 
314
331
  MailerIndex::ClassEntry.new(
315
332
  class_name: class_name,
@@ -320,7 +337,7 @@ module Rigor
320
337
  )
321
338
  end
322
339
 
323
- def build_action_entry(def_node)
340
+ def build_action_entry(def_node, view_exempt: false)
324
341
  parameters = def_node.parameters
325
342
  location = def_node.name_loc
326
343
 
@@ -329,7 +346,8 @@ module Rigor
329
346
  method_name: def_node.name,
330
347
  min_arity: 0, max_arity: 0,
331
348
  def_line: location.start_line,
332
- def_column: location.start_column + 1
349
+ def_column: location.start_column + 1,
350
+ view_exempt: view_exempt
333
351
  )
334
352
  end
335
353
 
@@ -342,7 +360,8 @@ module Rigor
342
360
  min_arity: required_count,
343
361
  max_arity: rest_present ? Float::INFINITY : required_count + optional_count,
344
362
  def_line: location.start_line,
345
- def_column: location.start_column + 1
363
+ def_column: location.start_column + 1,
364
+ view_exempt: view_exempt
346
365
  )
347
366
  end
348
367
 
@@ -11,7 +11,9 @@ module Rigor
11
11
  # the mailer file)
12
12
  # - the list of `(action, location)` pairs whose view templates are missing from `app/views/`
13
13
  class MailerIndex
14
- ActionEntry = Data.define(:method_name, :min_arity, :max_arity, :def_line, :def_column) do
14
+ # `view_exempt` marks a callback or `helper_method` target: still an action (Rails dispatches it), but
15
+ # left out of the missing-view check.
16
+ ActionEntry = Data.define(:method_name, :min_arity, :max_arity, :def_line, :def_column, :view_exempt) do
15
17
  def arity_label
16
18
  return "#{min_arity}+" if max_arity == Float::INFINITY
17
19
  return min_arity.to_s if min_arity == max_arity
@@ -67,7 +69,7 @@ module Rigor
67
69
  end
68
70
 
69
71
  # @param file_path — absolute path of a mailer file (canonicalised — see plugin entry's
70
- # `harvest`)
72
+ # `canonical_path`)
71
73
  def find_by_file(file_path)
72
74
  @entries.find { |entry| entry.file_path == file_path }
73
75
  end
@@ -11,7 +11,7 @@ module Rigor
11
11
  module Plugin
12
12
  # rigor-actionmailer — validates `Mailer.action(args)` call sites and detects missing view templates.
13
13
  #
14
- # Tier 1C of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
14
+ # Tier 1C of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
15
15
  # Statically discovers mailer classes by walking `mailer_search_paths` and parsing each file with
16
16
  # Prism — no `action_mailer` runtime dependency.
17
17
  #
@@ -38,8 +38,6 @@ module Rigor
38
38
  # - Direct-superclass match only.
39
39
  # - Action methods are read from the syntactic instance-side `def` list. `define_method` actions are
40
40
  # out of scope.
41
- # - Adding a brand-new view file does not invalidate the cache until something the mailer file
42
- # touches changes.
43
41
  class Actionmailer < Rigor::Plugin::Base
44
42
  manifest(
45
43
  id: "actionmailer",
@@ -52,13 +50,36 @@ module Rigor
52
50
  # Bumped 2026-09-02 (#621) — mailer keys are de-rooted at the producer and a reopened class's
53
51
  # actions are UNIONed rather than clobbered, so a cached 0.4.0 index can be missing actions this
54
52
  # version knows about.
55
- version: "0.5.0",
53
+ #
54
+ # Bumped 2026-10-23 (#2308) — a method after a bare `protected` is no longer an action, so a cached 0.5.0
55
+ # index can list protected helpers as actions (and as missing-view candidates).
56
+ #
57
+ # Bumped 2026-10-26 (#2319) — a method named by a callback or `helper_method` macro (`prepend_before_action`,
58
+ # `after_deliver`, `helper_method`, ...) no longer draws a missing-view row, and a method defined after a bare
59
+ # `module_function` is no longer an action, so a cached 0.5.1 index can carry those rows and actions.
60
+ version: "0.5.2",
56
61
  description: "Validates ActionMailer call shape and view template existence.",
57
62
  config_schema: {
58
63
  "mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
59
64
  "mailer_base_classes" => { kind: :array, default: %w[ApplicationMailer ActionMailer::Base] },
60
65
  "views_root" => { kind: :string, default: "app/views" }
61
66
  },
67
+ # ADR-125 (#575, #2308) — a mailer instance serves exactly one action (`Mailer.welcome(...)` builds a
68
+ # fresh instance and runs that one method), so ivar writes in two different public actions are never live
69
+ # together and `def.ivar-write-mismatch` stops comparing them. `ActionMailer::Base` has no signature here;
70
+ # the rule matches an undeclared superclass by its written name.
71
+ per_invocation_classes: ["ActionMailer::Base"],
72
+ # A public method these macros name is a callback or a view helper, not an action: it runs inside
73
+ # whichever action triggers it (AbstractController::Callbacks / Helpers), so its writes stay compared with
74
+ # every action's. The actionpack list, plus the `*_deliver` callbacks (ActionMailer::Callbacks), which run
75
+ # on the action's own instance.
76
+ per_invocation_non_action_macros: %i[
77
+ before_action prepend_before_action append_before_action skip_before_action
78
+ after_action prepend_after_action append_after_action skip_after_action
79
+ around_action prepend_around_action append_around_action skip_around_action
80
+ before_deliver after_deliver around_deliver
81
+ helper_method
82
+ ],
62
83
  # ADR-103 WD10 (#387) — see {Effects} for what each row is and why.
63
84
  effect_root: "rails",
64
85
  effect_labels: ["rails.actionmailer.deliver"],
@@ -102,7 +102,7 @@ module Rigor
102
102
 
103
103
  # Phase 2 — filter-chain validation for ONE filter-DSL call (`before_action :name`, …). The
104
104
  # enclosing controller is read from the node-rule `NodeContext` ancestors, looked up in the
105
- # controller index to get the effective method set (including one level of inheritance), and
105
+ # controller index to get the effective method set (including inherited methods), and
106
106
  # each filter name reference is validated against it. Calls outside a known controller
107
107
  # contribute nothing.
108
108
  #
@@ -6,8 +6,7 @@ module Rigor
6
6
  # Per-run frozen index of discovered controllers AND concerns (modules) in
7
7
  # `controller_search_paths`. Phase 2 (filter-chain validation) consults the index at every call
8
8
  # site to check that `before_action :name` references a method that actually exists on the
9
- # controller, its parent (one level of inheritance), or any module the controller or its ancestor
10
- # chain transitively `include`s.
9
+ # controller, any ancestor, or any module the controller or an ancestor transitively `include`s.
11
10
  #
12
11
  # Real-world example (Mastodon):
13
12
  # AccountsController < ApplicationController
@@ -169,9 +168,6 @@ module Rigor
169
168
  # and returning the first candidate that has an entry in the index. When nothing resolves,
170
169
  # return the original name unchanged — the caller treats unresolved entries as "gem-shipped
171
170
  # concerns we cannot see" (the `unresolved_include?` predicate).
172
- #
173
- # `name` may already be qualified (`Foo::Bar`); we only try lexical prefixing when the
174
- # unqualified first segment doesn't match a top-level entry.
175
171
  def resolve_constant_lexically(name, enclosing)
176
172
  return nil if name.nil?
177
173
 
@@ -83,7 +83,6 @@ module Rigor
83
83
  end.to_h
84
84
  end
85
85
 
86
- # Every implicit-self `render` call node under `node`, at any depth.
87
86
  def each_render(node, &)
88
87
  return unless node.is_a?(Prism::Node)
89
88
 
@@ -229,7 +229,8 @@ module Rigor
229
229
  # been measured. Resolved ONCE per process: a failed resolution is as memoised as a successful
230
230
  # one, so a project without Erubi does not pay a `$LOAD_PATH` walk per template.
231
231
  #
232
- # `Isolation.require_with_target_bundle` is [ADR-90](../../../../../../docs/adr/90-target-library-resolution-from-project-bundle.md)'s
232
+ # `Isolation.require_with_target_bundle` is
233
+ # [ADR-90](../../../../../../docs/adr/90-target-library-resolution-from-project-bundle.md)'s
233
234
  # path — Rigor's own gem env first, then the analysed project's bundle, `$LOAD_PATH` appended and
234
235
  # only on a failed require. Erubi is not a Rigor dependency and is never added to one. Any load
235
236
  # failure is equally "no Erubi here"; stdlib `ERB` answers the same question, so nothing is
@@ -18,7 +18,7 @@ module Rigor
18
18
  # ## The inference is deliberately narrow
19
19
  #
20
20
  # Only `@user = Model.find(...)`-shaped assignments contribute, and only for a closed set of
21
- # constructors and finders that **cannot return nil** — `find`, `find_by!`, `sole`, `first!`,
21
+ # constructors and finders that **cannot return nil** — `find`, `find_by!`, `find_sole_by`, `sole`, `first!`,
22
22
  # `last!`, `new`, `create`, `create!`. A seeded type is a claim the engine acts on, and a wrong one
23
23
  # is worse than none: seeding `@user` from `find_by` (which returns nil at runtime) would hand the
24
24
  # flow rules a non-nil nominal and license folds that draw diagnostics on correct templates — the
@@ -20,7 +20,7 @@ module Rigor
20
20
  # fact published by `rigor-rails-routes` (ADR-9 cross-plugin API) and validates every implicit-self
21
21
  # `*_path` / `*_url` call inside files under `controller_search_paths` (default `app/controllers`).
22
22
  #
23
- # Tier 2 of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
23
+ # Tier 2 of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
24
24
  # Phase 1 (strong-parameters → AR column validation), Phase 2 (filter chains), and Phase 3 (render
25
25
  # targets) ship as separate slices; each phase composes additively under the same plugin id.
26
26
  #
@@ -37,14 +37,10 @@ module Rigor
37
37
  #
38
38
  # ## What it checks
39
39
  #
40
- # - **Helper existence** — every `*_path` / `*_url` call inside a controller file is looked up in
41
- # the helper table. Missing entries emit `unknown-helper` with a `DidYouMean` suggestion drawn
42
- # from the table.
43
- # - **Helper arity** — the call's positional-argument count is matched against the helper's
44
- # recorded arity (a trailing `KeywordHashNode` like `users_path(format: :json)` is excluded; same
45
- # convention `rigor-rails-routes` uses). Mismatches emit `wrong-helper-arity`.
46
- # - **Trace** — recognised helpers also emit a `helper-call` info diagnostic naming the action and
47
- # path, mirroring the trace shape of the upstream plugin.
40
+ # - **Trace** — recognised `*_path` / `*_url` helpers emit a `helper-call` info diagnostic naming
41
+ # the action and path. Unknown helpers (`unknown-helper`) and arity mismatches (`wrong-arity`)
42
+ # are reported by `rigor-rails-routes`, which owns the helper table, so this plugin does not
43
+ # repeat them.
48
44
  #
49
45
  # ## Limitations
50
46
  #
@@ -88,7 +84,9 @@ module Rigor
88
84
  # the engine reads as "a template exists here and has no unit". The bump is belt-and-braces: what
89
85
  # actually invalidates is the claim itself, through the glob row in `TemplateUnits#digest` and the
90
86
  # `claimed_globs` equality the carry checks. A manifest version is in no cache key of its own.
91
- version: "1.5.0",
87
+ # Bumped 2026-10-27 (#2430) — `params[...]` answers the gradual `Dynamic[Parameters | nil]` rather than
88
+ # the bare union, so a cached 1.5.0 run carries the non-gradual answer at every `params[:k]` argument.
89
+ version: "1.6.0",
92
90
  description: "Validates Action Pack route-helper calls and filter chains inside controllers, and types the request-context readers (`params` / `session` / `request` / `flash`) and their chains.",
93
91
  config_schema: {
94
92
  "controller_search_paths" => { kind: :array, default: ["app/controllers"] },
@@ -146,6 +144,19 @@ module Rigor
146
144
  # `sig/action_view.rbs` so the name resolves, open so the helper surface stays lenient.
147
145
  "ActionView::Base"
148
146
  ],
147
+ # ADR-125 (#575) — Rails instantiates a fresh controller per request and runs exactly one action on
148
+ # it, so ivar writes in two different public actions are never live together and
149
+ # `def.ivar-write-mismatch` stops comparing them. Neither root has a signature (see `sig/action_controller.rbs`
150
+ # for why); the rule matches an undeclared superclass by its written name.
151
+ per_invocation_classes: ["ActionController::Base", "ActionController::API"],
152
+ # A public method these macros name is a callback or a view helper, not an action: it runs inside
153
+ # whichever action triggers it, so its writes stay compared with every action's (#575's A1 decision).
154
+ per_invocation_non_action_macros: %i[
155
+ before_action prepend_before_action append_before_action skip_before_action
156
+ after_action prepend_after_action append_after_action skip_after_action
157
+ around_action prepend_around_action append_around_action skip_around_action
158
+ helper_method
159
+ ],
149
160
  # ADR-103 WD10 / WD14 (#387) — see {Effects} for what each row is and why.
150
161
  effect_root: "rails",
151
162
  effect_labels: %w[
@@ -462,7 +473,7 @@ module Rigor
462
473
  # `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
463
474
  # `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
464
475
  # so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
465
- # `.each`) to unprotected. These three methods return another `Parameters`, so gate on a
476
+ # `.each`) to unprotected. These methods return another `Parameters`, so gate on a
466
477
  # `Parameters` receiver and re-type the result as the same lenient nominal — the chain stays a
467
478
  # concrete receiver end-to-end and `coverage --protection` counts the sites as protected.
468
479
  #
@@ -484,9 +495,9 @@ module Rigor
484
495
  # entry FP-safe, because the RBS-less nominal buys leniency on the *method surface* only:
485
496
  #
486
497
  # - Method-surface rules DO decline on it. `call.undefined-method` bails at
487
- # `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb:720) because Rigor
488
- # ships no RBS for Parameters, and the argument / arity rules gate on the same predicate
489
- # (:1151, :2126). So `params.slice(:a).whatever_rails_adds` never fires, and a scalar-shaped
498
+ # `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb) because Rigor
499
+ # ships no RBS for Parameters, and the argument / arity rules gate on the same predicate. So
500
+ # `params.slice(:a).whatever_rails_adds` never fires, and a scalar-shaped
490
501
  # use of a Parameters-typed value (`.to_i`, `.strip`, string interpolation) resolves lenient
491
502
  # -to-Dynamic rather than to a diagnostic.
492
503
  # - **Flow rules do NOT.** Truthiness folding, ternary arm selection and `.nil?` folding key on
@@ -529,7 +540,7 @@ module Rigor
529
540
  # and still carries the chain — `params[:user][:name]` types `Parameters?` and
530
541
  # `params[:user].permit(:name)` types `Parameters`, because the dispatcher strips the nil arm
531
542
  # for the receiver gate. Idiomatic *chained* reads stay silent too: `call.possible-nil-receiver`
532
- # restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`'s local-read restriction), so
543
+ # restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`), so
533
544
  # `params[:q].strip` cannot fire. What it does fire on is the assigned form — `q = params[:q];
534
545
  # q.strip` — at **error** severity, once per unguarded use. Guarded forms (`return if q.nil?`,
535
546
  # `if q`, `q&.strip`, `|| ""`) all narrow correctly and stay silent, so this is not a broken
@@ -551,7 +562,8 @@ module Rigor
551
562
  # The control specs stay, and they still bite: they pin the HAZARD (no truthiness fold, no `bot`
552
563
  # branch, no possible-nil firing), not the Dynamic answer, so they go red if either typing's
553
564
  # hazard ever returns — and they now carry a positive assertion that `params[:k]` types
554
- # `Parameters?`, so a silent regression to `Dynamic` or to the non-nil nominal fails them too.
565
+ # `Dynamic[ActionController::Parameters?]` (#2430), so a silent regression to bare `Dynamic[top]`
566
+ # or to the non-nil nominal fails them too.
555
567
  STRONG_PARAMS_CHAIN_METHODS = %i[
556
568
  require permit permit! expect slice
557
569
  except without extract! slice!
@@ -565,6 +577,16 @@ module Rigor
565
577
  Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
566
578
  end
567
579
 
580
+ # Issue #2430 — the chain on a `params[...]` carrier (`params[:user].permit(:name)`). The result stays
581
+ # a `Dynamic` of the same nominal, so it crosses a declared boundary as the carrier does.
582
+ dynamic_return methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, scope|
583
+ next nil unless call_node.is_a?(Prism::CallNode) && call_node.receiver
584
+ next nil unless controller_scope?(scope)
585
+ next nil unless params_carrier_facet?(scope.type_of(call_node.receiver))
586
+
587
+ Rigor::Type::Combinator.dynamic(Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]))
588
+ end
589
+
568
590
  # Issue #534 item 1, reopened by #574. `#[]` is the single largest named-receiver pair on both survey
569
591
  # apps (redmine 581, mastodon ~475). The honest contract is `Parameters | nil` — a missing key really
570
592
  # returns nil — and the block comment above records why that typing was REJECTED in 2026-09-01: it put
@@ -580,15 +602,53 @@ module Rigor
580
602
  #
581
603
  # The non-nil `-> Parameters` typing stays rejected on its own merits, unchanged: it makes every
582
604
  # `params[:k]` condition always-truthy and folds a ternary over one to its true arm.
605
+ #
606
+ # Issue #2430 — the answer is a GRADUAL carrier, `Dynamic[Parameters | nil]`, not the bare union.
607
+ # A declared boundary (a `String` parameter, a `-> String` return) rejects the bare union, because
608
+ # `Acceptance` passes only a `Dynamic` through every boundary, and a scalar param is a `String` at
609
+ # runtime. The static facet keeps the strong-params chain typed: the carrier-gated rules below
610
+ # dispatch on the facet, so `params[:user][:name]` and `params[:user].permit(:name)` keep resolving.
611
+ # The control hazards (no truthiness fold, no `bot`, no possible-nil on `q = params[:q]`) are
612
+ # unchanged, because the facet is the same `Parameters | nil` union.
583
613
  dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: %i[[]] do |call_node, _scope|
584
614
  next nil unless call_node.is_a?(Prism::CallNode)
585
615
 
586
- Rigor::Type::Combinator.union(
587
- Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
588
- Rigor::Type::Combinator.constant_of(nil)
616
+ params_read_type
617
+ end
618
+
619
+ # Issue #2430 — `Dynamic` is not a `Nominal`, so the receiver-gated rules never see a carrier
620
+ # receiver. These two rules gate on the carrier's facet instead, and only inside a controller: a
621
+ # carrier is minted by the `params` reader, which is controller-only, so no other scope can hold one.
622
+ dynamic_return methods: %i[[]] do |call_node, scope|
623
+ next nil unless call_node.is_a?(Prism::CallNode) && call_node.receiver
624
+ next nil unless controller_scope?(scope)
625
+ next nil unless params_carrier_facet?(scope.type_of(call_node.receiver))
626
+
627
+ params_read_type
628
+ end
629
+
630
+ # The `params` carrier's static facet as a `Dynamic`: `Parameters | nil`, gradual at every boundary.
631
+ def params_read_type
632
+ Rigor::Type::Combinator.dynamic(
633
+ Rigor::Type::Combinator.union(
634
+ Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
635
+ Rigor::Type::Combinator.constant_of(nil)
636
+ )
589
637
  )
590
638
  end
591
639
 
640
+ # True when `type` is a `Dynamic` whose facet is `Parameters` or `Parameters | nil`: the carrier a
641
+ # `params[...]` read answers, and the value a chained `[]` / strong-params call receives.
642
+ def params_carrier_facet?(type)
643
+ return false unless type.is_a?(Rigor::Type::Dynamic)
644
+
645
+ facet = type.static_facet
646
+ members = facet.is_a?(Rigor::Type::Union) ? facet.members : [facet]
647
+ non_nil = members.reject { |member| member.is_a?(Rigor::Type::Constant) && member.value.nil? }
648
+ non_nil.size == 1 && non_nil.first.is_a?(Rigor::Type::Nominal) &&
649
+ non_nil.first.class_name == REQUEST_CONTEXT_READER_TYPES[:params]
650
+ end
651
+
592
652
  # Phase 5c (2026-09-02, issue #534 "same lane") — the rest of the request-context surface the
593
653
  # corpus sweep measured after the #578 / #585 batch: `request.post?` 28 and `request.xhr?` 27 on
594
654
  # the two apps, `flash.now` inside the redmine 147 / mastodon 29 flash cluster. Each is typed from
@@ -35,7 +35,7 @@ module Rigor
35
35
 
36
36
  # Where a Rails app declares its adapter. Per-environment files are read too, because an app that
37
37
  # sets the adapter only in `config/environments/production.rb` is common; agreement across every
38
- # file that names one is what licenses the narrowing (see {.transport_for}).
38
+ # file that names one is what licenses the narrowing (see {.detect_adapter}).
39
39
  CONFIG_PATHS = ["config/application.rb", "config/environments"].freeze
40
40
 
41
41
  SETTING = /queue_adapter\s*=\s*[:"']([a-z_]+)["']?/
@@ -13,7 +13,7 @@ module Rigor
13
13
  # rigor-activejob — validates `Job.perform_later(...)` / `.perform_now(...)` / `.perform(...)`
14
14
  # argument arity against the discovered `#perform` definitions.
15
15
  #
16
- # Tier 1D of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
16
+ # Tier 1D of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
17
  # Statically discovers ActiveJob subclasses by walking the configured `job_search_paths` and
18
18
  # parsing each file with Prism — no `active_job` runtime dependency.
19
19
  #
@@ -97,8 +97,11 @@ module Rigor
97
97
 
98
98
  # Cached separately from `:job_index` because the two invalidate on different files: editing a
99
99
  # schedule changes which jobs are reached without touching `app/jobs` at all. The `watch:` roots the
100
- # glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
101
- producer :recurring_jobs, watch: -> { [[".", *@recurring_paths]] } do |_params|
100
+ # glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
101
+ # configured path is escaped to a literal name, so `recurring[1].yml` is watched as that file, and an
102
+ # absolute path is watched under its own directory (`RootedGlob.split_file`, #2222).
103
+ producer :recurring_jobs,
104
+ watch: -> { @recurring_paths.map { |path| Rigor::RootedGlob.split_file(path) } } do |_params|
102
105
  RecurringScan.new(
103
106
  io_boundary: io_boundary,
104
107
  recurring_paths: @recurring_paths
@@ -20,8 +20,9 @@ module Rigor
20
20
  #
21
21
  # Declined — no root, never a guess: a polymorphic association (no single target), a non-literal
22
22
  # `class_name:` / `**opts`, `anonymous_class:`, a `with_options` group with non-literal options, and a
23
- # `through:` association without a literal `class_name:` / `source_type:`. Without them a `through:` target is the SOURCE association's class on the through model, and
24
- # that association (declared there, hence in this same index) roots it itself.
23
+ # `through:` association without a literal `class_name:` / `source_type:`. Without them a `through:`
24
+ # target is the SOURCE association's class on the through model, and that association (declared
25
+ # there, hence in this same index) roots it itself.
25
26
  #
26
27
  # A computed name equal to the owner's own demodulized name declines too (Rails 7.2+ tries `::Name`
27
28
  # first; the target is then the owner itself or a top-level class).