rigortype 0.3.8 → 0.4.0

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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -6,6 +6,11 @@ require_relative "actionpack/analyzer"
6
6
  require_relative "actionpack/effects"
7
7
  require_relative "actionpack/controller_discoverer"
8
8
  require_relative "actionpack/controller_index"
9
+ require_relative "actionpack/controller_scan"
10
+ require_relative "actionpack/erb_compiler"
11
+ require_relative "actionpack/render_locals"
12
+ require_relative "actionpack/view_assigns"
13
+ require_relative "actionpack/view_units"
9
14
 
10
15
  module Rigor
11
16
  module Plugin
@@ -55,6 +60,7 @@ module Rigor
55
60
  class Actionpack < Rigor::Plugin::Base
56
61
  manifest(
57
62
  id: "actionpack",
63
+ target_gems: ["actionpack"],
58
64
  # ADR-37: the four phases (helper / filter / render / strong-params) run per-call over the
59
65
  # engine-owned walk; the enclosing controller is read from the node-rule `NodeContext`
60
66
  # ancestors. Nested-module qualification is preserved — `module Admin; class
@@ -68,16 +74,78 @@ module Rigor
68
74
  # Bumped 2026-09-02 (#621) — a reopened controller's `def`s are UNIONed rather than clobbered by
69
75
  # the later file in the glob, so a cached 1.0.0 index can be missing the filter targets the merge
70
76
  # restores.
71
- version: "1.1.0",
77
+ # Bumped 2026-09-17 (#393) — the plugin gained `template_globs:` and compiles `app/views/**/*.erb`
78
+ # into template units, so the answer for a project with views is a different answer entirely and
79
+ # a cached 1.2.0 run has no view rows in it.
80
+ # Bumped 2026-09-10 (#534 item 7) — the plugin gained a bundled `sig/` (ADR-25) naming the
81
+ # `ActionController` namespace and the errors controllers rescue, so the constants stop resolving
82
+ # to `untyped`. `ActionController::Parameters` and the `ActionDispatch` readers stay undeclared;
83
+ # `sig/action_controller.rbs` documents why that absence is load-bearing.
84
+ # Bumped 2026-09-17 (#1047) — the plugin now traces render-site `locals:` into a partial's unit and
85
+ # compiles LAYOUTS (a `yield` rewrite), so a cached 1.3.0 run is missing both the layout units and
86
+ # every seeded local.
87
+ # Bumped 2026-09-18 (#1065) — the claim grew the non-ERB handlers, which the plugin declines and
88
+ # the engine reads as "a template exists here and has no unit". The bump is belt-and-braces: what
89
+ # actually invalidates is the claim itself, through the glob row in `TemplateUnits#digest` and the
90
+ # `claimed_globs` equality the carry checks. A manifest version is in no cache key of its own.
91
+ version: "1.5.0",
72
92
  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.",
73
93
  config_schema: {
74
94
  "controller_search_paths" => { kind: :array, default: ["app/controllers"] },
75
- "view_search_paths" => { kind: :array, default: ["app/views"] }
95
+ "view_search_paths" => { kind: :array, default: ["app/views"] },
96
+ # #393 — whether the ordinary `call.*` type checks are reported INSIDE a compiled template. The
97
+ # `flow.*` family is no longer gated on this knob: #1047 removed the binding gap that made it
98
+ # unsound in a view, and it reports by default like every other family.
99
+ # Off by default: the synthesised `self` is an open receiver and the seeds are deliberately
100
+ # narrow, so the typing half of a view unit is not yet precise enough to be worth a diagnostic
101
+ # per template line, while the EFFECTS half needs no receiver precision at all and is what the
102
+ # view layer is analysed for first. A project that wants `@user.nmae` in `show.html.erb` can
103
+ # turn it on; see `docs/manual/plugins/rigor-actionpack.md`.
104
+ "view_type_checks" => { kind: :boolean, default: false }
76
105
  },
77
106
  consumes: [
78
107
  { plugin_id: "rails-routes", name: :helper_table, optional: true },
79
108
  { plugin_id: "activerecord", name: :model_index, optional: true }
80
109
  ],
110
+ # ADR-25 (#534 item 7) — the framework-namespace signatures. See `sig/action_controller.rbs` for
111
+ # the admission rule and, more importantly, for what it refuses to declare, and
112
+ # `sig/action_view.rbs` for the one class a template unit's `self` is typed as.
113
+ signature_paths: ["sig"],
114
+ # #393 — the ERB templates this plugin compiles into {Plugin::TemplateUnit}s. A manifest row is
115
+ # read without running plugin code, so it cannot consult `view_search_paths:`; a project that
116
+ # moves its views away from `app/views` gets no units, which is the quiet answer rather than a
117
+ # wrong one. Both `show.html.erb` and the bare `show.erb` shape are claimed.
118
+ # #1065 — the non-ERB handlers are claimed and DECLINED. Nothing here is compiled, and
119
+ # {#template_units_for_file} returns `[]` for every one of them; what the claim buys is the
120
+ # engine knowing a template exists at that logical name, which is what stops a `.js` render's
121
+ # format fallback from resolving past `_row.js.haml` onto `_row.html.erb`. Action View runs the
122
+ # Haml file. The list is the handlers a Rails app actually keeps under `app/views` — the common
123
+ # third-party template gems, plus Action View's own `builder` and `ruby`. It is NOT exhaustive:
124
+ # `raw` and `html` are Action View handlers too and are deliberately left out, because a claim on
125
+ # `*.html` would key a file whose name carries no format at all. A handler nobody claims is
126
+ # invisible, and its fallback behaves as it did before this claim existed.
127
+ template_globs: ["app/views/**/*.erb",
128
+ "app/views/**/*.{haml,slim,jbuilder,builder,rabl,ruby}"],
129
+ # ADR-26 — every class the bundled signature names is declared so the CONSTANT resolves; none of
130
+ # them enumerates a method surface, so `ParameterMissing#param` and its siblings must stay
131
+ # lenient rather than becoming `call.undefined-method` on a rescue body.
132
+ open_receivers: [
133
+ "ActionController::ActionControllerError",
134
+ "ActionController::BadRequest",
135
+ "ActionController::RoutingError",
136
+ "ActionController::UrlGenerationError",
137
+ "ActionController::MethodNotAllowed",
138
+ "ActionController::NotImplemented",
139
+ "ActionController::UnknownFormat",
140
+ "ActionController::InvalidAuthenticityToken",
141
+ "ActionController::InvalidCrossOriginRequest",
142
+ "ActionController::MissingExactTemplate",
143
+ "ActionController::ParameterMissing",
144
+ "ActionController::UnpermittedParameters",
145
+ # #393 — the view context every template unit's `self` is typed as. Declared in
146
+ # `sig/action_view.rbs` so the name resolves, open so the helper surface stays lenient.
147
+ "ActionView::Base"
148
+ ],
81
149
  # ADR-103 WD10 / WD14 (#387) — see {Effects} for what each row is and why.
82
150
  effect_root: "rails",
83
151
  effect_labels: %w[
@@ -98,10 +166,202 @@ module Rigor
98
166
  ).discover
99
167
  end
100
168
 
169
+ # #393 / #1047 — the diagnostic family a compiled template does not report while `view_type_checks:`
170
+ # is off (its default). Measured rather than assumed, on the private corpus copies recorded in
171
+ # `docs/notes/20260917-erb-template-units.md` and `docs/notes/20260917-render-locals-and-layouts.md`.
172
+ #
173
+ # `call.` is the receiver-precision family. `self` is an open `ActionView::Base` and the seeds are
174
+ # deliberately narrow, so what this family would report about a view is mostly a statement about what
175
+ # the synthesis does not know yet.
176
+ #
177
+ # **`flow.` was here and is not any more (#1047).** It was suppressed for one measured reason: with
178
+ # no render-site `locals:`, redmine's standard optional-local preamble
179
+ # (`<% path = nil unless defined? path %>` in `app/views/common/_other.html.erb`) really did assign
180
+ # nil in the compiled Ruby, and the flow rules folded three live branches on a partial Rails renders
181
+ # correctly. {RenderLocals} binds those names from the three sites that render that partial, the
182
+ # preamble stops being a fresh assignment, and the corpus re-measured with `flow.` UNSUPPRESSED is
183
+ # byte-identical to the corpus with it suppressed — on redmine (506 templates, layouts included) and
184
+ # on mastodon (46). The suppression was a stand-in for a missing binding, so it goes when the binding
185
+ # arrives rather than staying as a habit.
186
+ SUPPRESSED_VIEW_RULES = ["call."].freeze
187
+
188
+ # #1047 — where the project's view helpers live, read only for the names they `def`: a template's
189
+ # `defined?(current_user)` tests a HELPER, not an optional local, and must not seed one
190
+ # ({ViewUnits.self_declared_locals}). Rails' own convention and not a config knob, for the reason
191
+ # `template_globs:` is not one either.
192
+ HELPER_SEARCH_PATHS = ["app/helpers"].freeze
193
+
101
194
  def init(_services)
102
195
  @controller_search_paths = Array(config.fetch("controller_search_paths")).map(&:to_s)
103
196
  @view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
197
+ @view_type_checks = config.fetch("view_type_checks") ? true : false
198
+ end
199
+
200
+ # #393 — one ERB template compiled into one {Plugin::TemplateUnit}. Called once per matched file, on
201
+ # the parent, before any analysis (ADR-16 Tier D as revived by #392).
202
+ #
203
+ # The three synthesised bindings and where each comes from:
204
+ #
205
+ # - `self_type:` — {ViewUnits::SELF_TYPE}, a declared-but-open `ActionView::Base`, so every helper
206
+ # call resolves lenient rather than drawing a finding per line.
207
+ # - `locals:` — the names every RENDER SITE passes ({RenderLocals}, #1047), overlaid with the Rails
208
+ # 7.1 strict-locals comment where a template carries one. The seed is what makes Prism read `user`
209
+ # as a local rather than as a method call, which is what a `defined? user` preamble needs.
210
+ # - `ivar_seeds:` — the assigns of the controller actions that render this template, through
211
+ # {ViewAssigns}. A partial has none: its bindings belong to the render site.
212
+ #
213
+ # A compiler whose line numbering could not be measured declines the file rather than reporting at
214
+ # lines it cannot vouch for — {ErbCompiler::Unmappable} propagates, and the seam turns it into one
215
+ # `:plugin_loader` `runtime-error` row naming the template.
216
+ #
217
+ # ## Compiled Ruby that does not parse declines the file, silently
218
+ #
219
+ # A template is not Ruby, and the compiled form of one is not always Ruby either: a `case` split
220
+ # across tags with markup between the `case` and its first `when` is invalid anywhere. Handing such a
221
+ # body to the engine would put TWO parse diagnostics on a template Rails renders perfectly — a false
222
+ # positive per file, which is the one outcome this feature may not have (AGENTS.md). So the compiled
223
+ # Ruby is parsed and a body that does not parse declines the file through the seam's own `[]` door:
224
+ # no unit, no diagnostic, no effects.
225
+ #
226
+ # A LAYOUT used to be the bulk of that residue — `<%= yield %>` is legal ERB and illegal Ruby outside
227
+ # a method — and since #1047 it is not: {ErbCompiler::YIELD_METHOD} rewrites the keyword into a call
228
+ # on the view context, so all four of redmine's layouts and both of mastodon's compile. The residue
229
+ # is one `case` under stdlib ERB and none under Erubi.
230
+ #
231
+ # The cost is one Prism parse per template on the parent, and since #1047 both that parse and the
232
+ # compile are the ones {RenderLocals} already performed while building its index.
233
+ def template_units_for_file(path:, source:)
234
+ # #1065 — a claimed non-ERB handler is read for its NAME and nothing else: this plugin compiles
235
+ # ERB, and a `.haml` body handed to an ERB compiler would be garbage rather than a decline.
236
+ return [] unless path.end_with?(ERB_SUFFIX)
237
+
238
+ name = ViewUnits.logical_name(path, @view_search_paths)
239
+ # Scrubbed ONCE, here, and handed to both readers. Scrubbing inside the compiler alone left
240
+ # `ViewUnits.strict_locals` matching a Regexp against the raw bytes, and a single invalid byte
241
+ # anywhere in a template carrying a strict-locals comment raised out of the hook — which the seam
242
+ # correctly turns into an `error`-severity `:plugin_loader` row, so one mis-encoded view failed
243
+ # the whole run.
244
+ text = ErbCompiler.scrub(source)
245
+ revalidate_indexes
246
+ compiled, line_map, transform, parsed = render_locals.compiled_for(path, text) || compile_now(text)
247
+ return [] unless parsed
248
+
249
+ [
250
+ Rigor::Plugin::TemplateUnit.new(
251
+ logical_name: name, path: path, ruby_source: compiled, line_map: line_map,
252
+ self_type: ViewUnits::SELF_TYPE,
253
+ locals: template_locals(name, text),
254
+ ivar_seeds: view_assigns.seeds_for(name), transform_id: transform,
255
+ suppressed_rules: @view_type_checks ? [] : SUPPRESSED_VIEW_RULES
256
+ )
257
+ ]
258
+ end
259
+
260
+ # The compile the {RenderLocals} cache could not answer — an editor's in-flight bytes, or a template
261
+ # the index never globbed. Same four values the cache carries.
262
+ # The one handler this plugin compiles; every other claimed template is declined for its name.
263
+ ERB_SUFFIX = ".erb"
264
+
265
+ def compile_now(text)
266
+ compiled, line_map, transform = ErbCompiler.compile(text)
267
+ [compiled, line_map, transform, Prism.parse(compiled).errors.empty?]
268
+ end
269
+ private :compile_now
270
+
271
+ # A template's locals, from three sources that each say a name is bound, weakest first so the
272
+ # strongest wins a type: every name the template itself tests for with `defined?` or `local_assigns`
273
+ # (its author's own optional-local declaration, which needs no render site — #1047's review found
274
+ # the render-site index alone misses a `locals: opts` site, a helper-side `render` and a local no
275
+ # site passes at all), the render sites {RenderLocals} could read, and the strict-locals comment.
276
+ def template_locals(name, text)
277
+ ViewUnits.self_declared_locals(text, helpers: render_locals.helper_methods)
278
+ .merge(render_locals.seeds_for(name))
279
+ .merge(ViewUnits.strict_locals(text))
280
+ end
281
+ private :template_locals
282
+
283
+ # #1047 — the two parent-side indexes are memoised on this instance, and a long-lived
284
+ # `LanguageServer::ProjectContext` keeps the instance across publishes. A memo that is never
285
+ # revalidated keeps a render site's locals after the site changed on disk, which is a `flow.` row
286
+ # on a correct partial until the process restarts.
287
+ #
288
+ # So the indexes are revalidated once per COLLECTION PASS, against a fingerprint of every controller,
289
+ # helper and template they read ({RenderLocals::Builder.fingerprint}: one glob and one `stat` per
290
+ # file). The collector announces a pass through {#template_units_pass_started}; the check itself is
291
+ # deferred to the pass's first `#template_units_for_file`, so a warm pass that carries every unit and
292
+ # compiles nothing pays nothing. Inferring the pass from the order of paths was tried and is not
293
+ # enough: a warm pass offers only the editor's buffer, so switching buffers after a render site
294
+ # changed on disk was served from the stale index whichever way the two paths sorted.
295
+ #
296
+ # Revalidating at the FIRST call of the pass rather than when an edited template's own bytes arrive is
297
+ # load-bearing too: `_card.html.erb` globs before `show.html.erb`, so a reset triggered by `show` would
298
+ # come after `_card`'s unit had already been built from the stale index. The collector's whole-claim
299
+ # carry decision ({Analysis::TemplateUnitCollector}) is the other half: it is what re-offers `_card`.
300
+ def template_units_pass_started
301
+ @index_pass_pending = true
302
+ end
303
+
304
+ def revalidate_indexes
305
+ return unless @index_pass_pending
306
+
307
+ @index_pass_pending = false
308
+ drop_indexes unless indexes_fresh?
309
+ end
310
+ private :revalidate_indexes
311
+
312
+ def indexes_fresh?
313
+ return true if @render_locals.nil?
314
+
315
+ @render_locals.fingerprint == index_fingerprint
316
+ end
317
+ private :indexes_fresh?
318
+
319
+ def drop_indexes
320
+ @render_locals = nil
321
+ @view_assigns = nil
322
+ end
323
+ private :drop_indexes
324
+
325
+ def index_fingerprint
326
+ RenderLocals::Builder.fingerprint(
327
+ io_boundary: io_boundary, controller_search_paths: @controller_search_paths,
328
+ view_search_paths: @view_search_paths, helper_search_paths: HELPER_SEARCH_PATHS
329
+ )
330
+ rescue StandardError
331
+ nil
332
+ end
333
+ private :index_fingerprint
334
+
335
+ # The controller-assigns index, built once per run on the parent. Deliberately NOT a `producer` —
336
+ # a producer's value is cached against the run key, and this is consulted from the template-unit
337
+ # hook, which runs before the analysis a producer belongs to.
338
+ def view_assigns
339
+ @view_assigns ||= begin
340
+ ViewAssigns::Builder.new(io_boundary: io_boundary, search_paths: @controller_search_paths).build
341
+ rescue StandardError
342
+ ViewAssigns.empty
343
+ end
344
+ end
345
+ private :view_assigns
346
+
347
+ # #1047 — the render-site `locals:` index, and the compiled source every template was read through
348
+ # while it was built. Lazy and parent-only for the same reason {#view_assigns} is: it answers a
349
+ # question the analysis has not begun to ask yet. See {RenderLocals} for what a site contributes, and
350
+ # {#revalidate_indexes} for when a memoised index is dropped. A builder that raised is memoised as
351
+ # the empty index too — otherwise every hook call re-globbed and recompiled the project to fail
352
+ # again — and its nil fingerprint never equals a real one, so the next pass retries it.
353
+ def render_locals
354
+ @render_locals ||= begin
355
+ RenderLocals::Builder.new(
356
+ io_boundary: io_boundary, controller_search_paths: @controller_search_paths,
357
+ view_search_paths: @view_search_paths, helper_search_paths: HELPER_SEARCH_PATHS,
358
+ view_assigns: view_assigns
359
+ ).build
360
+ rescue StandardError
361
+ RenderLocals.empty
362
+ end
104
363
  end
364
+ private :render_locals
105
365
 
106
366
  # ADR-37 — the four Action Pack phases run per-call over the engine-owned walk. Each rule gates on
107
367
  # `controller_file?(path)` (the plugin only validates files under `controller_search_paths`,
@@ -144,7 +404,8 @@ module Rigor
144
404
  diagnostics_for(
145
405
  Analyzer.render_violations_for(
146
406
  call_node: node, ancestors: context.ancestors, path: path,
147
- view_search_roots: @view_search_paths, controller_index: producer_value(:controller_index)
407
+ view_search_roots: @view_search_paths, io_boundary: io_boundary,
408
+ controller_index: producer_value(:controller_index)
148
409
  ),
149
410
  path: path, node: node
150
411
  )
@@ -0,0 +1,71 @@
1
+ # Action Pack's framework namespaces — issue #534 item 7.
2
+ #
3
+ # The 2026-09-01 corpus sweep counted `ActionController` 37 times on mastodon as an unresolved
4
+ # constant. This file names the namespace and the errors controllers rescue, and nothing else.
5
+ #
6
+ # ## What must NOT be declared here, and why the list is so short
7
+ #
8
+ # `ActionController::Parameters` is absent on purpose, and it is the most important absence in this
9
+ # plugin. `Rigor::Plugin::Actionpack::REQUEST_CONTEXT_READER_TYPES` types `params` as a *lenient*
10
+ # nominal — a real class name Rigor ships no RBS for — precisely so that `params.require(...)`,
11
+ # `params.to_unsafe_h` and every other member of a surface no signature can complete resolve
12
+ # lenient-to-`Dynamic` instead of drawing `call.undefined-method`. Declaring the class here, even
13
+ # empty, would make it RBS-known and undo that. The same argument covers `ActionDispatch::Request`,
14
+ # `Request::Session`, `Flash::FlashHash`, `Flash::FlashNow` and `Cookies::CookieJar`.
15
+ #
16
+ # `ActionController::Base` and `ActionController::API` are absent for the stronger reason: every
17
+ # controller in a Rails app inherits from one of them, so declaring either empty would close the
18
+ # whole project's controller hierarchy and turn `render`, `before_action`, `head` and the rest into
19
+ # `call.undefined-method` on working code.
20
+ #
21
+ # What is left is exactly the shape that is safe: bare namespace modules nobody dispatches on, and
22
+ # exception classes whose inherited surface is a core Ruby one Rigor already carries. Each error
23
+ # class is listed in the manifest's `open_receivers:` (ADR-26), so the framework-specific readers
24
+ # this file does not enumerate (`ParameterMissing#param`, `UrlGenerationError#routes`, …) stay
25
+ # lenient too.
26
+ #
27
+ # Superclasses transcribed from Rails v8.0.2 (`actionpack/lib/action_controller/metal/exceptions.rb`
28
+ # and `strong_parameters.rb`); `ParameterMissing < KeyError` and `UnpermittedParameters < IndexError`
29
+ # are the two that are NOT under `ActionControllerError`, and getting that wrong would be a wrong
30
+ # `rescue` reachability answer, so both are stated from the source rather than from the pattern.
31
+
32
+ module ActionController
33
+ class ActionControllerError < StandardError
34
+ end
35
+
36
+ class BadRequest < ActionControllerError
37
+ end
38
+
39
+ class RoutingError < ActionControllerError
40
+ end
41
+
42
+ class UrlGenerationError < ActionControllerError
43
+ end
44
+
45
+ class MethodNotAllowed < ActionControllerError
46
+ end
47
+
48
+ class NotImplemented < MethodNotAllowed
49
+ end
50
+
51
+ class UnknownFormat < ActionControllerError
52
+ end
53
+
54
+ class InvalidAuthenticityToken < ActionControllerError
55
+ end
56
+
57
+ class InvalidCrossOriginRequest < ActionControllerError
58
+ end
59
+
60
+ class MissingExactTemplate < UnknownFormat
61
+ end
62
+
63
+ # `::KeyError` is generic in core RBS (`KeyError[K, R]`, the receiver and key of the failed lookup),
64
+ # so the supertype has to be applied. `untyped` on both: Rails raises it from a Parameters lookup and
65
+ # this signature has no business claiming what `#receiver` / `#key` answer.
66
+ class ParameterMissing < ::KeyError[untyped, untyped]
67
+ end
68
+
69
+ class UnpermittedParameters < ::IndexError
70
+ end
71
+ end
@@ -0,0 +1,43 @@
1
+ # Action View's namespace — issue #393.
2
+ #
3
+ # `ActionView::Base` is the `self_type:` of every ERB template unit the plugin compiles. It is declared
4
+ # here for one reason: so that the NAME resolves. `Analysis::TemplateUnits#seed` binds an unresolvable
5
+ # `self_type:` to `Dynamic[top]`, which is silent but also tells the engine nothing, and a declared
6
+ # receiver is what lets a later slice put a real helper surface behind it.
7
+ #
8
+ # ## Why it enumerates nothing
9
+ #
10
+ # The same argument `action_controller.rbs` makes about `ActionController::Parameters`, one step stronger.
11
+ # A template's implicit-self calls are `link_to`, `form_with`, `t`, `l`, `cache`, `content_for`,
12
+ # `image_tag`, `turbo_frame_tag`, every `*_path` / `*_url` route helper, every method in the project's own
13
+ # `ApplicationHelper` and `<Controller>Helper`, every `helper_method` a controller exported, and whatever
14
+ # the project's gems mixed in. No signature completes that surface, and a declared class DROPS every
15
+ # member its signature omits — so enumerating the ten helpers the design note names would turn the other
16
+ # thousand into `call.undefined-method` on working templates.
17
+ #
18
+ # So the class is named and left empty, and `open_receivers:` in the manifest (ADR-26) keeps its whole
19
+ # method surface lenient. That is the "lenient / open receivers, never closed" posture stated as code.
20
+ #
21
+ # `ActionView::UnknownLocal` is deliberately ABSENT — see `ViewUnits::UNKNOWN_LOCAL`. It is the name a
22
+ # strict-locals parameter is seeded with precisely because nothing resolves it, so the engine binds
23
+ # `Dynamic[top]` while the local's NAME still reaches the parse.
24
+
25
+ # ## The one declared member, and why it is not a helper
26
+ #
27
+ # `__rigor_yield` is issue #1047's reading of a LAYOUT's `yield`. `<%= yield %>` is legal ERB and illegal
28
+ # Ruby outside a method body, so `ErbCompiler` rewrites the keyword into a call on this receiver before
29
+ # either compiler runs — the template-unit seam does not wrap a unit's body in a synthesised method, so the
30
+ # body has to parse as written. The name is not Rails', is not reachable from any template a user writes,
31
+ # and exists only so the rewritten body resolves.
32
+ #
33
+ # It returns `String` and nothing narrower. Rails' `yield` hands back whatever the inner template's output
34
+ # buffer holds and `yield :sidebar` hands back a `content_for` buffer (an empty `SafeBuffer`, never nil,
35
+ # when nothing was provided); a lenient `String` is the
36
+ # widest honest reading of that, and it is what keeps a layout from seeding a type the analyzer could not
37
+ # justify. `*untyped` covers `yield`, `yield :sidebar` and `yield(:sidebar)` alike.
38
+
39
+ module ActionView
40
+ class Base
41
+ def __rigor_yield: (*untyped) -> String
42
+ end
43
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/source/node_children"
4
+
5
+ require "prism"
6
+
7
+ require_relative "serializer_index"
8
+
9
+ module Rigor
10
+ module Plugin
11
+ class ActiveModelSerializers < Rigor::Plugin::Base
12
+ # Walks the configured serializer-search paths through the plugin's `IoBoundary`, parses each `.rb`
13
+ # file with Prism, records every `class X < Y` it finds with the facts the derivation needs, and
14
+ # keeps the classes whose ancestry closes back to a configured root:
15
+ #
16
+ # class ApplicationSerializer < ActiveModel::Serializer # root child
17
+ # class REST::AccountSerializer < ApplicationSerializer # reached through it
18
+ #
19
+ # The closure is what a direct-superclass match (the shape `rigor-pundit`'s policy discoverer uses)
20
+ # would miss, and a project base serializer is the common case rather than the exotic one. It is
21
+ # also the ONLY gate: a class the closure does not reach is not a serializer here, whatever it is
22
+ # called, because `object` is an ordinary name that any class may define.
23
+ #
24
+ # Every class in the scanned tree is recorded first and filtered afterwards, because the file that
25
+ # defines a base serializer may be parsed after the files that inherit from it.
26
+ class SerializerDiscoverer
27
+ # A `class << self` body is class-level for the same reason a `def self.x` body is, and it is a
28
+ # separate node type: without this arm a receiver-less `def object` inside one would register as
29
+ # an instance method, and an `object.<name>` there as a read of the resource.
30
+ SKIPPED_SCOPES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
31
+
32
+ # AMS's resource-backed declaration macros. Each names something the serializer renders by
33
+ # calling it on the resource, unless the serializer defines a method of that name itself.
34
+ DECLARATION_MACROS = %i[attributes attribute has_many has_one belongs_to].freeze
35
+
36
+ # Macros that DEFINE a method on the serializer rather than declaring one to read off the
37
+ # resource. `delegate :object, to: :wrapper` and `attr_reader :object` are both real ways to
38
+ # override AMS's reader, and neither writes a `def` for an ancestor walk to find; a name defined
39
+ # this way is also not a name the resource has to answer.
40
+ DEFINITION_MACROS = %i[attr_reader attr_accessor attr_writer delegate alias_method].freeze
41
+
42
+ # `object.tap`, `object.nil?`, `object.is_a?` and their siblings say nothing about which class the
43
+ # resource is, so they are not evidence.
44
+ #
45
+ # Listed rather than read from `Object.instance_methods`, which was the first shape and is
46
+ # process-dependent: `to_json`, `to_yaml` and `pretty_print` are on Object only because some
47
+ # library in the ANALYSER's process required them, so whether a serializer derived could change
48
+ # with Rigor's own load order. These are the public instance methods every Ruby object has from
49
+ # `Object` / `Kernel` alone.
50
+ UNIVERSAL_METHODS = %w[
51
+ ! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup
52
+ enum_for eql? equal? extend freeze frozen? hash inspect instance_of? instance_variable_defined?
53
+ instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method
54
+ methods nil? object_id private_methods public_method public_methods public_send
55
+ remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods
56
+ taint tainted? tap then to_enum to_s trust untaint untrust untrusted? yield_self
57
+ ].to_set.freeze
58
+
59
+ def initialize(io_boundary:, search_paths:, base_classes:)
60
+ @io_boundary = io_boundary
61
+ @search_paths = search_paths
62
+ @base_classes = base_classes.map { |name| name.to_s.delete_prefix("::") }
63
+ end
64
+
65
+ def discover
66
+ candidates = []
67
+ ruby_files_under(@search_paths).each do |path|
68
+ contents = read_safely(path)
69
+ next if contents.nil?
70
+
71
+ walk(Prism.parse(contents).value, []) do |entry_fields|
72
+ candidates << SerializerIndex::Entry.new(file_path: path, **entry_fields)
73
+ end
74
+ end
75
+ SerializerIndex.new(close_over_roots(candidates))
76
+ end
77
+
78
+ private
79
+
80
+ # Keeps only the classes whose declared-superclass chain reaches one of the roots. Iterating to a
81
+ # fixed point rather than recursing keeps a cyclic `class A < B; class B < A` source — which Ruby
82
+ # rejects but a static parse can still be handed — from recursing forever.
83
+ def close_over_roots(candidates)
84
+ by_name = candidates.to_h { |entry| [entry.class_name, entry] }
85
+ reached = @base_classes.to_set
86
+ loop do
87
+ grown = candidates.select do |entry|
88
+ !reached.include?(entry.class_name) && reached.include?(entry.superclass_name)
89
+ end
90
+ break if grown.empty?
91
+
92
+ grown.each { |entry| reached << entry.class_name }
93
+ end
94
+ reached.filter_map { |name| by_name[name] }
95
+ end
96
+
97
+ def read_safely(path)
98
+ @io_boundary.read_file(path)
99
+ rescue Plugin::AccessDeniedError, Errno::ENOENT
100
+ nil
101
+ end
102
+
103
+ def ruby_files_under(roots)
104
+ roots.flat_map do |root|
105
+ absolute = File.expand_path(root)
106
+ # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
107
+ next [] unless @io_boundary.directory?(absolute)
108
+
109
+ Dir.glob(File.join(absolute, "**", "*.rb"))
110
+ end
111
+ end
112
+
113
+ def walk(node, lexical_path, &)
114
+ return if node.nil?
115
+
116
+ case node
117
+ when Prism::ClassNode then visit_class(node, lexical_path, &)
118
+ when Prism::ModuleNode then visit_module(node, lexical_path, &)
119
+ else
120
+ node.rigor_each_child { |child| walk(child, lexical_path, &) }
121
+ end
122
+ end
123
+
124
+ def visit_class(node, lexical_path, &)
125
+ local_name = constant_path_name(node.constant_path)
126
+ return if local_name.nil?
127
+
128
+ superclass = node.superclass ? constant_path_name(node.superclass) : nil
129
+ unless superclass.nil?
130
+ yield({ class_name: qualify(lexical_path, local_name),
131
+ superclass_name: superclass.delete_prefix("::"),
132
+ **collect_facts(node.body) })
133
+ end
134
+
135
+ walk(node.body, lexical_path + [local_name], &) if node.body
136
+ end
137
+
138
+ def visit_module(node, lexical_path, &)
139
+ local_name = constant_path_name(node.constant_path)
140
+ return if local_name.nil?
141
+
142
+ walk(node.body, lexical_path + [local_name], &) if node.body
143
+ end
144
+
145
+ # The three name sets, gathered in ONE pass over the class body. A nested class or module is not
146
+ # descended into — its `def`s and its `object` reads belong to it, not to this class.
147
+ def collect_facts(body)
148
+ facts = { declared_names: Set.new, object_reads: Set.new, own_method_names: Set.new }
149
+ collect_from(body, facts)
150
+ facts.transform_values { |set| set.to_a.sort.freeze }
151
+ end
152
+
153
+ def collect_from(node, facts)
154
+ return if node.nil? || SKIPPED_SCOPES.any? { |kind| node.is_a?(kind) }
155
+
156
+ case node
157
+ when Prism::DefNode
158
+ # A `def self.x` body is class-level: its `object` would be a NoMethodError at run time, so
159
+ # neither its name nor its reads are evidence about the resource.
160
+ return unless node.receiver.nil?
161
+
162
+ facts[:own_method_names] << node.name.to_s
163
+ when Prism::CallNode then record_call(node, facts)
164
+ end
165
+ node.rigor_each_child { |child| collect_from(child, facts) }
166
+ end
167
+
168
+ def record_call(node, facts)
169
+ if node.receiver.nil? && DECLARATION_MACROS.include?(node.name)
170
+ symbol_arguments(node).each { |name| facts[:declared_names] << name }
171
+ elsif node.receiver.nil? && DEFINITION_MACROS.include?(node.name)
172
+ symbol_arguments(node).each { |name| facts[:own_method_names] << name }
173
+ elsif object_reader?(node.receiver) && !UNIVERSAL_METHODS.include?(node.name.to_s)
174
+ facts[:object_reads] << node.name.to_s
175
+ end
176
+ end
177
+
178
+ def object_reader?(receiver)
179
+ receiver.is_a?(Prism::CallNode) && receiver.name == :object && receiver.receiver.nil? &&
180
+ receiver.arguments.nil?
181
+ end
182
+
183
+ def symbol_arguments(node)
184
+ (node.arguments&.arguments || []).filter_map do |argument|
185
+ argument.unescaped if argument.is_a?(Prism::SymbolNode)
186
+ end
187
+ end
188
+
189
+ # `class REST::AccountSerializer` inside no module is already fully qualified; a class written as
190
+ # `class AccountSerializer` inside `module REST` is not. A name the source rooted explicitly
191
+ # (`class ::AccountSerializer`) keeps neither the root marker nor the lexical prefix.
192
+ def qualify(lexical_path, local_name)
193
+ return local_name.delete_prefix("::") if local_name.start_with?("::")
194
+
195
+ (lexical_path + [local_name]).join("::")
196
+ end
197
+
198
+ def constant_path_name(node)
199
+ case node
200
+ when Prism::ConstantReadNode then node.name.to_s
201
+ when Prism::ConstantPathNode then constant_path_parts(node)
202
+ end
203
+ end
204
+
205
+ def constant_path_parts(node)
206
+ parts = []
207
+ current = node
208
+ while current.is_a?(Prism::ConstantPathNode)
209
+ parts.unshift(current.name.to_s)
210
+ current = current.parent
211
+ end
212
+ case current
213
+ when nil then "::#{parts.join('::')}"
214
+ when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end