rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -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.8"
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,11 +18,11 @@ 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
25
- # trap `Actionpack::STRONG_PARAMS_CHAIN_METHODS` documents at length for `Parameters#[]`. Anything
25
+ # trap `Actionpack::PARAMS_ONLY_CHAIN_METHODS` documents at length for `Parameters#[]`. Anything
26
26
  # else assigns nothing, the ivar stays unseeded, and the template reads it as `Dynamic`, which
27
27
  # taints honestly (ADR-5). A call on that list that returns, or may return, SEVERAL records —
28
28
  # `find(a, b)`, `find([1, 2])`, `create([{…}, {…}])` — is excluded by the same rule; see
@@ -228,7 +228,7 @@ module Rigor
228
228
  # `case`, a `rescue`, a loop, or a block that may not run — does not contribute, because a seed
229
229
  # is a claim about what the template will FIND, and `@user = User.find(1) if params[:pick]`
230
230
  # leaves `@user` nil on the other path. A non-nil nominal standing in for a runtime nil is the
231
- # `Parameters#[]` trap (`Actionpack::STRONG_PARAMS_CHAIN_METHODS`): the flow rules act on it and
231
+ # `Parameters#[]` trap (`Actionpack::PARAMS_ONLY_CHAIN_METHODS`): the flow rules act on it and
232
232
  # report live branches, which is exactly what a reader of a template would see with
233
233
  # `view_type_checks:` on. The unseeded ivar reads `Dynamic` instead and taints honestly (ADR-5).
234
234
  #
@@ -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"] },
@@ -475,21 +473,21 @@ module Rigor
475
473
  # `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
476
474
  # `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
477
475
  # so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
478
- # `.each`) to unprotected. These three methods return another `Parameters`, so gate on a
479
- # `Parameters` receiver and re-type the result as the same lenient nominal — the chain stays a
480
- # concrete receiver end-to-end and `coverage --protection` counts the sites as protected.
481
- #
482
- # `require` may return a scalar for a flat key at runtime (`params.require(:id) → String`), but
483
- # typing it as the RBS-less `Parameters` is FP-safe by the same argument as the readers: every
484
- # method on a Parameters value stays engine-lenient (no `undefined-method`), and this types the
485
- # container only, never a caller's argument (ADR-5). The GitLab strong-params survey (108 leaked
486
- # `.permit` sites) is the demand.
487
- # Issue #534 — `expect` (Rails 8's require+permit) and `slice` joined 2026-09-01, from the corpus
488
- # sweep's pair counts (63 and 38 sites on mastodon). Both share `require`'s safety shape: they
489
- # never return nil (`expect` raises like `require`; `slice` always returns a Parameters), so the
490
- # non-nil lenient nominal cannot fold a flow rule wrong.
491
- #
492
- # ## The admission rule for this table
476
+ # `.each`) to unprotected. The methods that return another `Parameters` (`PARAMS_ONLY_CHAIN_METHODS`)
477
+ # gate on a `Parameters` receiver and re-type the result as the same
478
+ # lenient nominal — the sites after them stay concrete receivers and `coverage --protection` counts
479
+ # them as protected. `require` / `expect` are the exception below: the receiver of the call right
480
+ # after one is the carrier, which is not a concrete receiver.
481
+ #
482
+ # `require` and `expect` may return a scalar at runtime (`params.require(:id) → String`), so the
483
+ # earlier "RBS-less nominal is FP-safe" reading of them (2026-07, #534) is withdrawn by #2947: it held
484
+ # only while Parameters shipped no RBS. Once the collection's declaration is loaded, a nominal answer
485
+ # reports a scalar's method as `call.undefined-method`. They answer the gradual carrier
486
+ # `Dynamic[Parameters]` (`SCALAR_CAPABLE_CHAIN_METHODS`), which keeps the chain resolvable and is quiet
487
+ # with or without the RBS. The GitLab strong-params survey (108 leaked `.permit` sites) is the demand.
488
+ # Issue #534 — `expect` and `slice` joined 2026-09-01 (63 and 38 sites on mastodon).
489
+ #
490
+ # ## The admission rule for the nominal table (`PARAMS_ONLY_CHAIN_METHODS`)
493
491
  #
494
492
  # A method belongs here **iff its Rails implementation returns an `ActionController::Parameters`
495
493
  # on every path** — either a fresh `new_instance_with_inherited_permitted_status(...)` or `self`.
@@ -497,9 +495,9 @@ module Rigor
497
495
  # entry FP-safe, because the RBS-less nominal buys leniency on the *method surface* only:
498
496
  #
499
497
  # - Method-surface rules DO decline on it. `call.undefined-method` bails at
500
- # `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb:720) because Rigor
501
- # ships no RBS for Parameters, and the argument / arity rules gate on the same predicate
502
- # (: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
503
501
  # use of a Parameters-typed value (`.to_i`, `.strip`, string interpolation) resolves lenient
504
502
  # -to-Dynamic rather than to a diagnostic.
505
503
  # - **Flow rules do NOT.** Truthiness folding, ternary arm selection and `.nil?` folding key on
@@ -542,7 +540,7 @@ module Rigor
542
540
  # and still carries the chain — `params[:user][:name]` types `Parameters?` and
543
541
  # `params[:user].permit(:name)` types `Parameters`, because the dispatcher strips the nil arm
544
542
  # for the receiver gate. Idiomatic *chained* reads stay silent too: `call.possible-nil-receiver`
545
- # 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
546
544
  # `params[:q].strip` cannot fire. What it does fire on is the assigned form — `q = params[:q];
547
545
  # q.strip` — at **error** severity, once per unguarded use. Guarded forms (`return if q.nil?`,
548
546
  # `if q`, `q&.strip`, `|| ""`) all narrow correctly and stay silent, so this is not a broken
@@ -564,20 +562,60 @@ module Rigor
564
562
  # The control specs stay, and they still bite: they pin the HAZARD (no truthiness fold, no `bot`
565
563
  # branch, no possible-nil firing), not the Dynamic answer, so they go red if either typing's
566
564
  # hazard ever returns — and they now carry a positive assertion that `params[:k]` types
567
- # `Parameters?`, so a silent regression to `Dynamic` or to the non-nil nominal fails them too.
568
- STRONG_PARAMS_CHAIN_METHODS = %i[
569
- require permit permit! expect slice
565
+ # `Dynamic[ActionController::Parameters?]` (#2430), so a silent regression to bare `Dynamic[top]`
566
+ # or to the non-nil nominal fails them too.
567
+ # The links that return an `ActionController::Parameters` on every path (checked against Rails 8.x
568
+ # `strong_parameters.rb`). Only these may be typed as the nominal.
569
+ PARAMS_ONLY_CHAIN_METHODS = %i[
570
+ permit permit! slice
570
571
  except without extract! slice!
571
572
  merge merge! reverse_merge reverse_merge! with_defaults with_defaults!
572
573
  compact compact_blank deep_dup
573
574
  ].freeze
574
575
 
575
- dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, _scope|
576
+ # Issue #2947 — `require` and `expect` are NOT in the nominal table above. Rails returns the value itself:
577
+ # `require` is `self[key]` (a scalar, or an Array for an Array of keys) and `expect` is `values.first`
578
+ # for one key, else an Array. The "RBS-less nominal is lenient" argument holds only while Parameters
579
+ # ships no RBS; with the collection's declaration loaded, a nominal answer reports `.to_sym` on a
580
+ # scalar as `call.undefined-method`. They answer the gradual carrier `Dynamic[Parameters]` instead:
581
+ # quiet at every boundary, and the chain keeps resolving through the carrier-gated rule.
582
+ SCALAR_CAPABLE_CHAIN_METHODS = %i[require expect].freeze
583
+
584
+ STRONG_PARAMS_CHAIN_METHODS = (PARAMS_ONLY_CHAIN_METHODS + SCALAR_CAPABLE_CHAIN_METHODS).freeze
585
+
586
+ dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: PARAMS_ONLY_CHAIN_METHODS do |call_node, _scope|
576
587
  next nil unless call_node.is_a?(Prism::CallNode)
577
588
 
578
589
  Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
579
590
  end
580
591
 
592
+ dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: SCALAR_CAPABLE_CHAIN_METHODS do |call_node, _scope|
593
+ next nil unless call_node.is_a?(Prism::CallNode)
594
+
595
+ Rigor::Type::Combinator.dynamic(Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]))
596
+ end
597
+
598
+ # Issue #2430 / #2947 — the chain on a carrier receiver (`params[:user].permit(:name)`,
599
+ # `params.require(:user).permit(:name)`). A carrier often holds an Array or a String at runtime, and
600
+ # those define many of the chain names with other returns (`compact`, `slice`, `without`, `deep_dup`,
601
+ # `extract!`, ...), so only `permit` / `permit!` — which no core or ActiveSupport class defines — can
602
+ # answer the plain nominal there: such a call raises NoMethodError or returns a real Parameters, and
603
+ # the plain nominal keeps the sites after it concrete receivers for `coverage --protection`. Every
604
+ # other chain method stays the gradual carrier.
605
+ CARRIER_NOMINAL_CHAIN_METHODS = %i[permit permit!].freeze
606
+
607
+ dynamic_return methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, scope|
608
+ next nil unless call_node.is_a?(Prism::CallNode)
609
+
610
+ receiver = call_node.receiver
611
+ next nil unless receiver
612
+ next nil unless controller_scope?(scope)
613
+ next nil unless params_carrier_facet?(scope.type_of(receiver))
614
+
615
+ nominal = Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
616
+ CARRIER_NOMINAL_CHAIN_METHODS.include?(call_node.name) ? nominal : Rigor::Type::Combinator.dynamic(nominal)
617
+ end
618
+
581
619
  # Issue #534 item 1, reopened by #574. `#[]` is the single largest named-receiver pair on both survey
582
620
  # apps (redmine 581, mastodon ~475). The honest contract is `Parameters | nil` — a missing key really
583
621
  # returns nil — and the block comment above records why that typing was REJECTED in 2026-09-01: it put
@@ -593,15 +631,56 @@ module Rigor
593
631
  #
594
632
  # The non-nil `-> Parameters` typing stays rejected on its own merits, unchanged: it makes every
595
633
  # `params[:k]` condition always-truthy and folds a ternary over one to its true arm.
634
+ #
635
+ # Issue #2430 — the answer is a GRADUAL carrier, `Dynamic[Parameters | nil]`, not the bare union.
636
+ # A declared boundary (a `String` parameter, a `-> String` return) rejects the bare union, because
637
+ # `Acceptance` passes only a `Dynamic` through every boundary, and a scalar param is a `String` at
638
+ # runtime. The static facet keeps the strong-params chain typed: the carrier-gated rules below
639
+ # dispatch on the facet, so `params[:user][:name]` and `params[:user].permit(:name)` keep resolving.
640
+ # The control hazards (no truthiness fold, no `bot`, no possible-nil on `q = params[:q]`) are
641
+ # unchanged, because the facet is the same `Parameters | nil` union.
596
642
  dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: %i[[]] do |call_node, _scope|
597
643
  next nil unless call_node.is_a?(Prism::CallNode)
598
644
 
599
- Rigor::Type::Combinator.union(
600
- Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
601
- Rigor::Type::Combinator.constant_of(nil)
645
+ params_read_type
646
+ end
647
+
648
+ # Issue #2430 — `Dynamic` is not a `Nominal`, so the receiver-gated rules never see a carrier
649
+ # receiver. These two rules gate on the carrier's facet instead, and only inside a controller: a
650
+ # carrier is minted by the `params` reader, which is controller-only, so no other scope can hold one.
651
+ dynamic_return methods: %i[[]] do |call_node, scope|
652
+ next nil unless call_node.is_a?(Prism::CallNode)
653
+
654
+ receiver = call_node.receiver
655
+ next nil unless receiver
656
+ next nil unless controller_scope?(scope)
657
+ next nil unless params_carrier_facet?(scope.type_of(receiver))
658
+
659
+ params_read_type
660
+ end
661
+
662
+ # The `params` carrier's static facet as a `Dynamic`: `Parameters | nil`, gradual at every boundary.
663
+ def params_read_type
664
+ Rigor::Type::Combinator.dynamic(
665
+ Rigor::Type::Combinator.union(
666
+ Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
667
+ Rigor::Type::Combinator.constant_of(nil)
668
+ )
602
669
  )
603
670
  end
604
671
 
672
+ # True when `type` is a `Dynamic` whose facet is `Parameters` or `Parameters | nil`: the carrier a
673
+ # `params[...]` read answers, and the value a chained `[]` / strong-params call receives.
674
+ def params_carrier_facet?(type)
675
+ return false unless type.is_a?(Rigor::Type::Dynamic)
676
+
677
+ facet = type.static_facet
678
+ members = facet.is_a?(Rigor::Type::Union) ? facet.members : [facet]
679
+ non_nil = members.reject { |member| member.is_a?(Rigor::Type::Constant) && member.value.nil? }
680
+ non_nil.size == 1 && non_nil.first.is_a?(Rigor::Type::Nominal) &&
681
+ non_nil.first.class_name == REQUEST_CONTEXT_READER_TYPES[:params]
682
+ end
683
+
605
684
  # Phase 5c (2026-09-02, issue #534 "same lane") — the rest of the request-context surface the
606
685
  # corpus sweep measured after the #578 / #585 batch: `request.post?` 28 and `request.xhr?` 27 on
607
686
  # the two apps, `flash.now` inside the redmine 147 / mastodon 29 flash cluster. Each is typed from