rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -28,14 +28,14 @@ module Rigor
28
28
  ALLOWED_KEY_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
29
29
  EXTRA_KEY_POLICIES = %i[open closed].freeze
30
30
  POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
31
- # A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
32
- # anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#erase_key_prefix}.
31
+ # A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#describe} and {#erase_to_rbs};
32
+ # anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#key_prefix}.
33
33
  BARE_RECORD_KEY = /\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/
34
34
 
35
35
  attr_reader :pairs, :required_keys, :optional_keys, :read_only_keys, :extra_keys
36
36
 
37
37
  # @param pairs — ordered map of
38
- # keys to declared types. Keys MUST be Symbol or String;
38
+ # keys to declared types. Keys MUST be one of ALLOWED_KEY_CLASSES;
39
39
  # values MUST be Rigor::Type instances. The hash is duped and
40
40
  # frozen at construction; callers MUST NOT mutate the input
41
41
  # afterwards (mutation does not affect the carrier, but the
@@ -74,7 +74,7 @@ module Rigor
74
74
  return hash_erasure unless closed?
75
75
  return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) || !record_key_spellable?(k) }
76
76
 
77
- rendered = pairs.map { |k, v| "#{erase_key_prefix(k)} #{v.erase_to_rbs}" }
77
+ rendered = pairs.map { |k, v| "#{key_prefix(k)} #{v.erase_to_rbs}" }
78
78
  "{ #{rendered.join(', ')} }"
79
79
  end
80
80
 
@@ -193,46 +193,27 @@ module Rigor
193
193
  def render_entry(key, value, verbosity)
194
194
  prefix = []
195
195
  prefix << "readonly" if read_only_key?(key)
196
- rendered_key = optional_key?(key) ? "?#{render_key(key)}" : render_key(key)
197
- prefix << "#{rendered_key}#{key_separator(key)}"
196
+ prefix << key_prefix(key)
198
197
  "#{prefix.join(' ')} #{value.describe(verbosity)}"
199
198
  end
200
199
 
201
- # `describe` is a human-facing display contract (it feeds diagnostic messages, never a parser),
202
- # so it keeps the compact `"a":` form for a quoted key. Non-(Symbol|String) scalar keys render in
203
- # the natural hashrocket spelling (`1 => 2`, `1.0 => 4`, `nil => 0`) via {#key_separator}; their
204
- # `inspect` is the canonical Ruby literal, so describe stays deterministic. The RBS-erasure path
205
- # uses {#erase_key_prefix} instead, which must emit a parseable key.
206
- def render_key(key)
207
- case key
208
- when Symbol then key.to_s
209
- else key.inspect
210
- end
211
- end
212
-
213
- # Symbol / String keys keep the compact colon form (`a: 1`, `"k": 2`); every other scalar key uses
214
- # the hashrocket (`1 => 2`) — the form the user would have to write in source.
215
- def key_separator(key)
216
- case key
217
- when Symbol, String then ":"
218
- else " =>"
219
- end
220
- end
221
-
222
- # An RBS record entry's key + separator, for the {#erase_to_rbs} path (which only ever sees
223
- # Symbol keys — string-keyed shapes degrade to `Hash[...]` before reaching here). A Symbol whose
224
- # text is a plain identifier is a bare key with a colon (`lang:`, Ruby keywords like `class`
225
- # included). Any other Symbol is a quoted Symbol literal with a fat arrow (`:"data-contrast" =>`):
226
- # RBS rejects a bare non-identifier (`data-contrast:`) and a quoted key with a colon
227
- # (`"data-contrast":`), and reads `"data-contrast" =>` as the String key, so the written record
228
- # declared a key the body never has (#2176). Optional keys carry a leading `?`.
229
- def erase_key_prefix(key)
200
+ # A key and its separator as RBS spells them, with a leading `?` for an optional key. `describe`
201
+ # (the diagnostic display) and {#erase_to_rbs} (which only ever sees Symbol keys — string-keyed
202
+ # shapes degrade to `Hash[...]` before reaching here) share it, so a key reads the same in a
203
+ # message as in the signature it erases to. A Symbol whose text is a plain identifier is a bare
204
+ # key with a colon (`lang:`, Ruby keywords like `class` included). Any other Symbol is a quoted
205
+ # Symbol literal with a fat arrow (`:"data-contrast" =>`): RBS rejects a bare non-identifier
206
+ # (`data-contrast:`) and a quoted key with a colon (`"data-contrast":`), and reads
207
+ # `"data-contrast" =>` as the String key, so the written record declared a key the body never has
208
+ # (#2176). Every other key, a String included, is its `inspect` literal with a fat arrow
209
+ # (`"a-b" =>`, `1 =>`, `nil =>`): the compact `"a-b":` would read as a Symbol key in Ruby, so a
210
+ # String-vs-Symbol key mismatch in a diagnostic was unreadable (#2235).
211
+ def key_prefix(key)
230
212
  optional = optional_key?(key) ? "?" : ""
231
- if BARE_RECORD_KEY.match?(key.to_s)
232
- "#{optional}#{key}:"
233
- else
234
- "#{optional}:#{key.to_s.inspect} =>"
235
- end
213
+ return "#{optional}#{key.inspect} =>" unless key.is_a?(Symbol)
214
+ return "#{optional}#{key}:" if BARE_RECORD_KEY.match?(key.to_s)
215
+
216
+ "#{optional}:#{key.to_s.inspect} =>"
236
217
  end
237
218
 
238
219
  # `String#inspect` escapes `#{`, `#$` and `#@` as `\#`, which RBS's parser does not read back as
@@ -49,7 +49,7 @@ module Rigor
49
49
  end
50
50
  end
51
51
 
52
- # ADR-1 § "RBS round-trip is lossless" + the value-lattice rule `untyped | T = untyped` (every `T`
52
+ # ADR-1 § "Erasure Must Be Conservative" + the value-lattice rule `untyped | T = untyped` (every `T`
53
53
  # is gradually consistent with `untyped`). When any union member erases to `"untyped"`, the whole
54
54
  # union erases to `"untyped"` — the RBS surface has no carrier for "Dynamic-origin alongside a
55
55
  # static facet", and the gradual-consistency contract guarantees the substitution is sound at every
@@ -22,8 +22,7 @@ module Rigor
22
22
  # Freeze the String field so the resulting Data object is `Ractor.shareable?` regardless of
23
23
  # whether the caller passed a `# frozen_string_literal: true` constant or a dynamically built
24
24
  # String. The same discipline applies to every other TypeNode value object — they live in the
25
- # parser's hot path and are the natural carriers to flow through future Ractor boundaries
26
- # (see CURRENT_WORK Open Items #8).
25
+ # parser's hot path and are the natural carriers to flow through future Ractor boundaries.
27
26
  super(name: name.frozen? ? name : name.dup.freeze)
28
27
  end
29
28
  end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # resolver in the chain plus the built-in registry through the
17
17
  # same entry point.
18
18
  #
19
- # Constructed once per `Analysis::Runner.run` from
19
+ # Built by `Environment#build_name_scope` from
20
20
  # `Plugin::Registry#type_node_resolvers`. The chain is
21
21
  # immutable and re-entrant; the parser may consult it many
22
22
  # times for the same node.
@@ -7,8 +7,7 @@ module Rigor
7
7
  # `Pick[T, "name"]`, `pick_of[Shape, "a" | "b"]`, and downstream
8
8
  # plugin resolvers that accept literal key selectors.
9
9
  #
10
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
11
- # #2): the RBS::Extended grammar previously could not tokenise
10
+ # ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
12
11
  # `"name"` inside a type-arg position. The resolver translates
13
12
  # this node to a `Type::Constant` carrying the string value.
14
13
  # Slice 1 supports double-quoted strings without escape
@@ -7,8 +7,7 @@ module Rigor
7
7
  # `Pick[T, :name]`, `pick_of[Shape, :a | :b]`, and downstream
8
8
  # plugin resolvers that accept literal key selectors.
9
9
  #
10
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
11
- # #2): the RBS::Extended grammar previously could not tokenise
10
+ # ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
12
11
  # `:name` inside a type-arg position; this addition closes the
13
12
  # gap so `ImportedRefinements.parse` produces a uniform AST.
14
13
  # The resolver translates this node to a `Type::Constant`
@@ -5,8 +5,7 @@ module Rigor
5
5
  # Union AST node. Carries the nodes of a `T1 | T2 | …` union
6
6
  # expression authored at type-arg position.
7
7
  #
8
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
9
- # #2): together with {SymbolLiteral} / {StringLiteral} this
8
+ # ADR-13 follow-up: together with {SymbolLiteral} / {StringLiteral} this
10
9
  # closes the parser-side gap that prevented `Pick[T, :a | :b]`
11
10
  # / `Pick[T, "a" | "b"]` from tokenising. The resolver pass
12
11
  # recurses through each node and folds them via
@@ -16,10 +15,8 @@ module Rigor
16
15
  # shadowing `Data#members`, which is the introspection method
17
16
  # every `Data.define` mix-in inherits.
18
17
  #
19
- # Nodes are themselves any of the {TypeNode} carriers — the
20
- # parser builds a flat list (left-associative); nested
21
- # `Union` nodes are permitted but the parser does not
22
- # synthesise them by itself.
18
+ # The parser builds a flat list (left-associative); the constructor
19
+ # rejects a nested `Union` node.
23
20
  class Union < Data.define(:nodes)
24
21
  def initialize(nodes:)
25
22
  unless nodes.is_a?(Array) && nodes.size >= 2 && nodes.all? { |m| valid_member?(m) }
@@ -46,16 +46,7 @@ module Rigor
46
46
  key = (["self.class"] + fields.map(&:to_s)).join(", ")
47
47
 
48
48
  # Codegen (not per-call reflection) so the generated `==` / `hash` read fields
49
- # directly and run at hand-written speed on the inference hot path. For
50
- # `value_fields :class_name, :type_args` the emitted source is:
51
- #
52
- # def ==(other)
53
- # other.is_a?(self.class) && class_name == other.class_name && type_args == other.type_args
54
- # end
55
- # alias_method :eql?, :==
56
- # def hash
57
- # [self.class, class_name, type_args].hash
58
- # end
49
+ # directly and run at hand-written speed on the inference hot path.
59
50
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
60
51
  def ==(other)
61
52
  other.is_a?(self.class)#{guard}
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.4.6"
4
+ VERSION = "0.4.7"
5
5
  end
@@ -167,8 +167,8 @@ module Rigor
167
167
  end
168
168
 
169
169
  # Walks the channel body recursively (so `stream_from` / `stream_for` calls inside `subscribed`
170
- # / helper methods are picked up). Returns `[Array<String>, bool]` — the literal stream names +
171
- # whether any dynamic registration was seen.
170
+ # / helper methods are picked up). Returns the literal stream names and whether any dynamic
171
+ # registration was seen, as a pair.
172
172
  def collect_stream_registrations(node, names: [], dynamic: false)
173
173
  return [names, dynamic] if node.nil?
174
174
 
@@ -11,8 +11,8 @@ module Rigor
11
11
  # `subscription.perform("action_name", data)`.
12
12
  # - `stream_names` — the set of literal-string stream names registered via `stream_from "name"`
13
13
  # calls inside the channel body. Dynamic registrations (`stream_from interpolated_string`) are
14
- # recorded separately as `dynamic_streams: true` so the analyzer can suppress unknown-stream
15
- # warnings on any channel that has at least one dynamic registration.
14
+ # recorded separately as `dynamic_streams: true`; when any channel has one, the analyzer suppresses
15
+ # unknown-stream warnings for the whole run.
16
16
  class ChannelIndex
17
17
  Entry = Data.define(:class_name, :file_path, :action_methods, :stream_names, :dynamic_streams) do
18
18
  def includes_action?(name)
@@ -59,7 +59,6 @@ module Rigor
59
59
  @by_name.keys
60
60
  end
61
61
 
62
- # All literal stream names registered across every discovered channel.
63
62
  def all_stream_names
64
63
  @entries.flat_map { |e| e.stream_names.to_a }.to_set
65
64
  end
@@ -12,7 +12,7 @@ module Rigor
12
12
  # rigor-actioncable — validates ActionCable `<Channel>.broadcast_to(...)` and
13
13
  # `ActionCable.server.broadcast(stream_name, ...)` call sites against the discovered channel index.
14
14
  #
15
- # Tier 3F of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
15
+ # Tier 3F of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
16
16
  # Statically discovers channel classes by walking `channel_search_paths` and parsing each file with
17
17
  # Prism — no `actioncable` runtime dependency.
18
18
  #
@@ -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).
@@ -220,40 +215,44 @@ module Rigor
220
215
  # `private` / `protected` (until a `public` switches back), named by `private :foo` /
221
216
  # `protected :foo`, or defined inline (`private def foo`) — Rails builds `action_methods` from the
222
217
  # public instance methods. `public :foo` after a `protected` section makes `foo` an action again.
223
- # - methods named as a callback target (`before_action :name`, `after_action`, `around_action`)
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.
224
221
  #
225
222
  # Pre-fix, Mastodon's `AdminMailer#process_params` / `set_instance` / `set_locale` /
226
223
  # `set_important_headers!` all surfaced as missing-view because the bare `private` keyword
227
224
  # wasn't honoured. ~19 false positives across Mastodon's mailers.
228
- CALLBACK_DECLARATIONS = %i[before_action after_action around_action].freeze
229
- private_constant :CALLBACK_DECLARATIONS
230
-
231
- VISIBILITY_KEYWORDS = %i[public private protected].freeze
225
+ VISIBILITY_KEYWORDS = %i[public private protected module_function].freeze
232
226
  private_constant :VISIBILITY_KEYWORDS
233
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.
234
230
  def collect_action_defs(body)
235
- return [] if body.nil?
231
+ return [[], Set[]] if body.nil?
236
232
 
237
233
  callback_names = collect_callback_names(body)
238
234
  def_nodes, final_visibility = scan_visibility(body)
239
235
 
240
- def_nodes.select do |node|
236
+ actions = def_nodes.select do |node|
241
237
  next false if node.receiver.is_a?(Prism::SelfNode)
242
238
  next false if node.name == :initialize
243
239
  next false if node.name.to_s.start_with?("_")
244
- next false if callback_names.include?(node.name)
245
240
 
246
241
  # Rails builds `action_methods` from the PUBLIC instance methods, so only a method whose final
247
242
  # visibility is public is an action.
248
243
  final_visibility[node.name] == :public
249
244
  end
245
+ [actions, callback_names]
250
246
  end
251
247
 
252
- # Symbol arguments to callback declarations (`before_action :setup`, etc.).
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).
253
251
  def collect_callback_names(body)
252
+ macros = Actionmailer.manifest.per_invocation_non_action_macros.reject { |macro| macro.start_with?("skip_") }
254
253
  names = []
255
254
  body.rigor_each_child do |node|
256
- next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && CALLBACK_DECLARATIONS.include?(node.name)
255
+ next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && macros.include?(node.name)
257
256
 
258
257
  (node.arguments&.arguments || []).each do |arg|
259
258
  symbol = Rigor::Source::Literals.symbol(arg)
@@ -265,8 +264,8 @@ module Rigor
265
264
 
266
265
  # Walks the class body in source order and returns `[def_nodes, final_visibility]`: every `def` the body
267
266
  # spells (a direct statement or the inline argument of a visibility call, `private def x`), and the
268
- # visibility each method name ends with. A bare `public` / `private` / `protected` sets the default for
269
- # the defs after it; `private :x` / `public :x` / `protected def x` set that one method, whichever side of
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
270
269
  # its `def` they fall on in the source, and the last statement to name a method wins.
271
270
  def scan_visibility(body)
272
271
  def_nodes = []
@@ -286,7 +285,8 @@ module Rigor
286
285
  [def_nodes, final_visibility]
287
286
  end
288
287
 
289
- # Applies one receiverless `public` / `private` / `protected` call and returns the (possibly new) default.
288
+ # Applies one receiverless `public` / `private` / `protected` / `module_function` call and returns the
289
+ # (possibly new) default.
290
290
  def apply_visibility_call(node, default, def_nodes, final_visibility)
291
291
  return default unless node.receiver.nil? && VISIBILITY_KEYWORDS.include?(node.name)
292
292
 
@@ -304,9 +304,9 @@ module Rigor
304
304
  default
305
305
  end
306
306
 
307
- 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[])
308
308
  actions = def_nodes.to_h do |def_node|
309
- entry = build_action_entry(def_node)
309
+ entry = build_action_entry(def_node, view_exempt: view_exempt.include?(def_node.name))
310
310
  [entry.method_name, entry]
311
311
  end
312
312
 
@@ -326,7 +326,7 @@ module Rigor
326
326
  end
327
327
  end
328
328
 
329
- 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) }
330
330
 
331
331
  MailerIndex::ClassEntry.new(
332
332
  class_name: class_name,
@@ -337,7 +337,7 @@ module Rigor
337
337
  )
338
338
  end
339
339
 
340
- def build_action_entry(def_node)
340
+ def build_action_entry(def_node, view_exempt: false)
341
341
  parameters = def_node.parameters
342
342
  location = def_node.name_loc
343
343
 
@@ -346,7 +346,8 @@ module Rigor
346
346
  method_name: def_node.name,
347
347
  min_arity: 0, max_arity: 0,
348
348
  def_line: location.start_line,
349
- def_column: location.start_column + 1
349
+ def_column: location.start_column + 1,
350
+ view_exempt: view_exempt
350
351
  )
351
352
  end
352
353
 
@@ -359,7 +360,8 @@ module Rigor
359
360
  min_arity: required_count,
360
361
  max_arity: rest_present ? Float::INFINITY : required_count + optional_count,
361
362
  def_line: location.start_line,
362
- def_column: location.start_column + 1
363
+ def_column: location.start_column + 1,
364
+ view_exempt: view_exempt
363
365
  )
364
366
  end
365
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",
@@ -55,7 +53,11 @@ module Rigor
55
53
  #
56
54
  # Bumped 2026-10-23 (#2308) — a method after a bare `protected` is no longer an action, so a cached 0.5.0
57
55
  # index can list protected helpers as actions (and as missing-view candidates).
58
- version: "0.5.1",
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",
59
61
  description: "Validates ActionMailer call shape and view template existence.",
60
62
  config_schema: {
61
63
  "mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
@@ -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