rigortype 0.2.7 → 0.2.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 (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -12,17 +12,13 @@ require_relative "../source/node_walker"
12
12
 
13
13
  module Rigor
14
14
  module Plugin
15
- # Base class every Rigor plugin subclasses. The plugin gem
16
- # subclasses {Base}, declares its identity through {.manifest},
17
- # registers the subclass with {Rigor::Plugin.register}, and
18
- # overrides {#init} to wire up any state it needs from the
19
- # injected service container.
15
+ # Base class every Rigor plugin subclasses. The plugin gem subclasses {Base}, declares its identity
16
+ # through {.manifest}, registers the subclass with {Rigor::Plugin.register}, and overrides {#init} to wire
17
+ # up any state it needs from the injected service container.
20
18
  #
21
- # This class implements all plugin protocol hooks: per-call
22
- # return-type contributions (`dynamic_return`), narrowing-fact
23
- # contributions (`narrowing_facts`), AST node rules (`node_rule`),
24
- # and producer/cache hooks. Cumulative implementation per the
25
- # ADR-37 / ADR-52 slice chain.
19
+ # This class implements all plugin protocol hooks: per-call return-type contributions (`dynamic_return`),
20
+ # narrowing-fact contributions (`narrowing_facts`), AST node rules (`node_rule`), and producer/cache
21
+ # hooks. Cumulative implementation per the ADR-37 / ADR-52 slice chain.
26
22
  #
27
23
  # Example plugin:
28
24
  #
@@ -42,10 +38,8 @@ module Rigor
42
38
  # Rigor::Plugin.register(MyRailsPlugin)
43
39
  class Base # rubocop:disable Metrics/ClassLength
44
40
  class << self
45
- # Declares the plugin's manifest. Called once at class
46
- # definition time the resulting {Manifest} is cached on
47
- # the class so {Rigor::Plugin::Loader} reads it without
48
- # constructing the plugin.
41
+ # Declares the plugin's manifest. Called once at class definition time — the resulting {Manifest} is
42
+ # cached on the class so {Rigor::Plugin::Loader} reads it without constructing the plugin.
49
43
  def manifest(**fields)
50
44
  if fields.empty?
51
45
  raise ArgumentError, "plugin #{self} did not declare a manifest" unless defined?(@manifest) && @manifest
@@ -56,8 +50,7 @@ module Rigor
56
50
  @manifest = Manifest.new(**fields)
57
51
  end
58
52
 
59
- # ADR-7 § "Slice 6-A" — DSL declaration of a cached
60
- # producer. Plugin authors write
53
+ # ADR-7 § "Slice 6-A" — DSL declaration of a cached producer. Plugin authors write
61
54
  #
62
55
  # class MyPlugin < Rigor::Plugin::Base
63
56
  # manifest(id: "rails", version: "0.1.0")
@@ -68,44 +61,31 @@ module Rigor
68
61
  # end
69
62
  # end
70
63
  #
71
- # The block runs through `instance_exec` so `self` inside
72
- # the body is the plugin instance `io_boundary`,
73
- # `services`, `manifest`, `config` are all in scope. The
74
- # block receives the call-site `params` Hash as its sole
75
- # argument; the same params Hash mixes into the cache
76
- # key per `Cache::Descriptor#cache_key_for`.
64
+ # The block runs through `instance_exec` so `self` inside the body is the plugin instance —
65
+ # `io_boundary`, `services`, `manifest`, `config` are all in scope. The block receives the call-site
66
+ # `params` Hash as its sole argument; the same params Hash mixes into the cache key per
67
+ # `Cache::Descriptor#cache_key_for`.
77
68
  #
78
- # `serialize:` / `deserialize:` apply to the producer's
79
- # return VALUE (the cache layer wraps them around the
80
- # record-and-validate entry pair itself). Default
81
- # round-trip is `Marshal.dump` / `Marshal.load` per the
82
- # v0.0.9 callable surface; producers whose return values
83
- # are not Marshal-clean must supply their own pair.
69
+ # `serialize:` / `deserialize:` apply to the producer's return VALUE (the cache layer wraps them
70
+ # around the record-and-validate entry pair itself). Default round-trip is `Marshal.dump` /
71
+ # `Marshal.load` per the v0.0.9 callable surface; producers whose return values are not Marshal-clean
72
+ # must supply their own pair.
84
73
  #
85
- # `watch:` (ADR-60 WD3) declares the glob coverage of a
86
- # discovery-style producer the files whose addition /
87
- # removal / edit must invalidate the cached value even
88
- # when the producer block never read them individually
89
- # (e.g. it globbed a directory itself). It is either
74
+ # `watch:` (ADR-60 WD3) declares the glob coverage of a discovery-style producer — the files whose
75
+ # addition / removal / edit must invalidate the cached value even when the producer block never read
76
+ # them individually (e.g. it globbed a directory itself). It is either
90
77
  #
91
- # - a static Array of `[roots, pattern, ...]` tuples
92
- # (`roots` a String or Array of Strings; one or more
93
- # glob-pattern suffixes per tuple the same shape
94
- # {#glob_descriptor} takes), or
95
- # - a Proc, run through `instance_exec` on the plugin
96
- # instance at `cache_for` invocation time (NEVER at
97
- # class-definition time — search roots are typically
98
- # computed in `#init` from config), returning the same
99
- # tuple Array.
78
+ # - a static Array of `[roots, pattern, ...]` tuples (`roots` a String or Array of Strings; one or
79
+ # more glob-pattern suffixes per tuple the same shape {#glob_descriptor} takes), or
80
+ # - a Proc, run through `instance_exec` on the plugin instance at `cache_for` invocation time (NEVER
81
+ # at class-definition time — search roots are typically computed in `#init` from config), returning
82
+ # the same tuple Array.
100
83
  #
101
- # The evaluated tuples become {Cache::Descriptor::GlobEntry}
102
- # rows in the dependency descriptor recorded after the
103
- # block runs; `Descriptor#fresh?` re-globs + re-digests on
104
- # the next run.
84
+ # The evaluated tuples become {Cache::Descriptor::GlobEntry} rows in the dependency descriptor
85
+ # recorded after the block runs; `Descriptor#fresh?` re-globs + re-digests on the next run.
105
86
  #
106
- # Producer ids are auto-prefixed `plugin.<manifest.id>.`
107
- # at the cache layer (slice 6-C) so plugin-side ids cannot
108
- # collide with built-in producers.
87
+ # Producer ids are auto-prefixed `plugin.<manifest.id>.` at the cache layer (slice 6-C) so plugin-side
88
+ # ids cannot collide with built-in producers.
109
89
  def producer(id, watch: nil, serialize: nil, deserialize: nil, &block)
110
90
  raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
111
91
 
@@ -117,8 +97,8 @@ module Rigor
117
97
  id.to_sym
118
98
  end
119
99
 
120
- # ADR-60 WD3 — `watch:` is nil (no glob coverage), a static
121
- # tuple Array, or a Proc evaluated per `cache_for` call.
100
+ # ADR-60 WD3 — `watch:` is nil (no glob coverage), a static tuple Array, or a Proc evaluated per
101
+ # `cache_for` call.
122
102
  def validate_producer_watch!(watch)
123
103
  return if watch.nil? || watch.is_a?(Array) || watch.respond_to?(:call)
124
104
 
@@ -128,20 +108,16 @@ module Rigor
128
108
  end
129
109
  private :validate_producer_watch!
130
110
 
131
- # Frozen snapshot of the producer table. Inherited
132
- # producers from a superclass are intentionally NOT
133
- # surfaced — Plugin::Base subclasses do not chain
134
- # producers, and the loader instantiates one
135
- # subclass per registration.
111
+ # Frozen snapshot of the producer table. Inherited producers from a superclass are intentionally NOT
112
+ # surfaced — Plugin::Base subclasses do not chain producers, and the loader instantiates one subclass
113
+ # per registration.
136
114
  def producers
137
115
  (@producers || {}).dup.freeze
138
116
  end
139
117
 
140
- # ADR-37 slice 1 — declares a node-scoped diagnostic rule.
141
- # The engine owns a single AST walk per file (see
142
- # {#node_rule_diagnostics}) and dispatches each node to the
143
- # rules registered for its type, so a plugin author writes the
144
- # check, never the traversal:
118
+ # ADR-37 slice 1 — declares a node-scoped diagnostic rule. The engine owns a single AST walk per file
119
+ # (see {#node_rule_diagnostics}) and dispatches each node to the rules registered for its type, so a
120
+ # plugin author writes the check, never the traversal:
145
121
  #
146
122
  # class MyPlugin < Rigor::Plugin::Base
147
123
  # manifest(id: "demo", version: "0.1.0")
@@ -152,25 +128,19 @@ module Rigor
152
128
  # end
153
129
  # end
154
130
  #
155
- # `node_type` is a `Prism::Node` subclass; the rule fires for
156
- # every node where `node.is_a?(node_type)`. The block runs
157
- # through `instance_exec` so `self` is the plugin instance
158
- # `config`, `services`, `io_boundary`, `diagnostic`, and the
159
- # cross-plugin `services.fact_store` are all in scope. It
160
- # receives `(node, scope, path, file_context, context)` the
161
- # fourth argument is the value built by {.node_file_context} for
162
- # a two-pass plugin (`nil` otherwise); the fifth is a
163
- # {Rigor::Plugin::NodeContext} carrying the node's lexical
164
- # ancestors (enclosing class / method / block DSL). Trailing
165
- # arguments may be omitted from the block's parameter list. The
166
- # block MUST return an Array of `Rigor::Analysis::Diagnostic`
167
- # (an empty array to fire nothing); the runner stamps
131
+ # `node_type` is a `Prism::Node` subclass; the rule fires for every node where
132
+ # `node.is_a?(node_type)`. The block runs through `instance_exec` so `self` is the plugin instance —
133
+ # `config`, `services`, `io_boundary`, `diagnostic`, and the cross-plugin `services.fact_store` are
134
+ # all in scope. It receives `(node, scope, path, file_context, context)` the fourth argument is the
135
+ # value built by {.node_file_context} for a two-pass plugin (`nil` otherwise); the fifth is a
136
+ # {Rigor::Plugin::NodeContext} carrying the node's lexical ancestors (enclosing class / method / block
137
+ # DSL). Trailing arguments may be omitted from the block's parameter list. The block MUST return an
138
+ # Array of `Rigor::Analysis::Diagnostic` (an empty array to fire nothing); the runner stamps
168
139
  # `plugin.<id>` provenance.
169
140
  #
170
- # Multiple rules for the same `node_type` are allowed and run
171
- # in declaration order. This is the {.producer}-style class DSL
172
- # rather than a manifest field because a rule carries logic that
173
- # needs the plugin instance, not pure data.
141
+ # Multiple rules for the same `node_type` are allowed and run in declaration order. This is the
142
+ # {.producer}-style class DSL rather than a manifest field because a rule carries logic that needs the
143
+ # plugin instance, not pure data.
174
144
  def node_rule(node_type, &block)
175
145
  raise ArgumentError, "Plugin::Base.node_rule requires a block body" if block.nil?
176
146
  unless node_type.is_a?(Class) && node_type <= Prism::Node
@@ -183,19 +153,16 @@ module Rigor
183
153
  node_type
184
154
  end
185
155
 
186
- # Frozen snapshot of the declared node rules, in declaration
187
- # order. Not inherited from a superclass like {.producers},
188
- # the loader instantiates one subclass per registration.
156
+ # Frozen snapshot of the declared node rules, in declaration order. Not inherited from a superclass —
157
+ # like {.producers}, the loader instantiates one subclass per registration.
189
158
  def node_rules
190
159
  (@node_rules || []).dup.freeze
191
160
  end
192
161
 
193
- # ADR-37 slice 1c — declares a per-file context builder for a
194
- # two-pass (collect-then-validate) plugin. The block runs once
195
- # per analysed file (via `instance_exec`, so the plugin instance
196
- # is `self`) BEFORE any node rule fires, receives `(root, scope)`,
197
- # and returns an arbitrary file-local value that is threaded to
198
- # every {.node_rule} block as its fourth argument:
162
+ # ADR-37 slice 1c — declares a per-file context builder for a two-pass (collect-then-validate)
163
+ # plugin. The block runs once per analysed file (via `instance_exec`, so the plugin instance is
164
+ # `self`) BEFORE any node rule fires, receives `(root, scope)`, and returns an arbitrary file-local
165
+ # value that is threaded to every {.node_rule} block as its fourth argument:
199
166
  #
200
167
  # node_file_context do |root, _scope|
201
168
  # collect_declared_states(root) # the "collect" pass
@@ -206,16 +173,14 @@ module Rigor
206
173
  # validate(node, path, states) # the "validate" pass
207
174
  # end
208
175
  #
209
- # This is what lets a same-file two-pass plugin drop its
210
- # hand-rolled validate walk: the collect pass computes the closed
211
- # namespace once (it MUST complete before validation because a
212
- # reference may precede its declaration), and the engine owns the
213
- # validate walk. A cross-file collect belongs in `#prepare` +
214
- # `services.fact_store` instead — a node rule reads the fact
215
- # directly and needs no per-file context.
176
+ # This is what lets a same-file two-pass plugin drop its hand-rolled validate walk: the collect pass
177
+ # computes the closed namespace once (it MUST complete before validation because a reference may
178
+ # precede its declaration), and the engine owns the validate walk. A cross-file collect belongs in
179
+ # `#prepare` + `services.fact_store` instead a node rule reads the fact directly and needs no
180
+ # per-file context.
216
181
  #
217
- # Only one builder per plugin; a second declaration replaces the
218
- # first. The block result is `nil` when none is declared.
182
+ # Only one builder per plugin; a second declaration replaces the first. The block result is `nil`
183
+ # when none is declared.
219
184
  def node_file_context(&block)
220
185
  raise ArgumentError, "Plugin::Base.node_file_context requires a block body" if block.nil?
221
186
 
@@ -227,10 +192,9 @@ module Rigor
227
192
  defined?(@node_file_context_block) ? @node_file_context_block : nil
228
193
  end
229
194
 
230
- # ADR-37 slice 2 / ADR-52 WD2 — declares a per-call-site
231
- # return-type contribution, gated by receiver class, method name,
232
- # or both. The narrow successor to the `return_type` slot of the
233
- # deleted `flow_contribution_for` hook (ADR-52 WD3):
195
+ # ADR-37 slice 2 / ADR-52 WD2 — declares a per-call-site return-type contribution, gated by receiver
196
+ # class, method name, or both. The narrow successor to the `return_type` slot of the deleted
197
+ # `flow_contribution_for` hook (ADR-52 WD3):
234
198
  #
235
199
  # # receiver-gated only:
236
200
  # dynamic_return receivers: ["ActiveRecord::Base"] do |call_node, scope|
@@ -250,78 +214,59 @@ module Rigor
250
214
  # # the block reads the receiver's shape itself
251
215
  # end
252
216
  #
253
- # `receivers:` is a non-empty Array of class names; the engine
254
- # calls the block only when the call's receiver type's class
255
- # equals or inherits from one of them (via
256
- # `Environment#class_ordering`). It MAY be omitted — then the rule
257
- # is receiver-independent and fires on `methods:` alone.
217
+ # `receivers:` is a non-empty Array of class names; the engine calls the block only when the call's
218
+ # receiver type's class equals or inherits from one of them (via `Environment#class_ordering`). It
219
+ # MAY be omitted then the rule is receiver-independent and fires on `methods:` alone.
258
220
  #
259
- # `methods:` is an Array of Symbol method names. When provided, the
260
- # block is skipped unless `call_node.name` is in the list —
261
- # declarative and cheaper than an in-block guard (the engine
262
- # compiles it into the registry's contribution table, ADR-52 WD1).
263
- # It is REQUIRED when `receivers:` is omitted: a rule gated on
264
- # neither would fire on every dispatch, which is exactly the
265
- # ungated cost the `flow_contribution_for` escape valve carries —
266
- # `dynamic_return` declines to reintroduce it.
221
+ # `methods:` is an Array of Symbol method names. When provided, the block is skipped unless
222
+ # `call_node.name` is in the list — declarative and cheaper than an in-block guard (the engine
223
+ # compiles it into the registry's contribution table, ADR-52 WD1). It is REQUIRED when `receivers:`
224
+ # is omitted: a rule gated on neither would fire on every dispatch, which is exactly the ungated cost
225
+ # the `flow_contribution_for` escape valve carries `dynamic_return` declines to reintroduce it.
267
226
  #
268
- # Method-name and type-shape refinement can still be done inside
269
- # the block. The block runs through `instance_exec`, so `config`
270
- # / `services` are in scope.
271
- # ADR-52 slice 3`receivers:` may also be a **callable**
272
- # (a `-> { ... }` resolved once per run, lazily, the first time
273
- # the rule is consulted — always after `#prepare`) for a receiver
274
- # set the plugin only knows at run time:
227
+ # Method-name and type-shape refinement can still be done inside the block. The block runs through
228
+ # `instance_exec`, so `config` / `services` are in scope.
229
+ # ADR-52 slice 3 — `receivers:` may also be a **callable** (a `-> { ... }` resolved once per run,
230
+ # lazily, the first time the rule is consulted always after `#prepare`) for a receiver set the
231
+ # plugin only knows at run time:
275
232
  #
276
233
  # dynamic_return receivers: -> { attachment_index.model_names } do |call_node, scope|
277
234
  # # fires when the receiver class is one a `prepare`-time scan
278
235
  # # found; the block does the precise per-call lookup
279
236
  # end
280
237
  #
281
- # The callable runs through `instance_exec`, so it reads the
282
- # plugin's own `#prepare`-built indexes. It MUST be idempotent and
283
- # post-`#prepare`-safe reference a lazily-built / memoised index
284
- # (as activestorage's `attachment_index` and activerecord's
285
- # `model_index` are), never a value captured at class-definition
286
- # time. The resolved set is a safe over-approximation of the
287
- # block's own filter (it admits subclasses too), so the block
288
- # stays the precise gate and diagnostics are unchanged.
238
+ # The callable runs through `instance_exec`, so it reads the plugin's own `#prepare`-built indexes.
239
+ # It MUST be idempotent and post-`#prepare`-safe reference a lazily-built / memoised index (as
240
+ # activestorage's `attachment_index` and activerecord's `model_index` are), never a value captured at
241
+ # class-definition time. The resolved set is a safe over-approximation of the block's own filter (it
242
+ # admits subclasses too), so the block stays the precise gate and diagnostics are unchanged.
289
243
  #
290
- # ADR-52 slice 4 — `methods:` may ALSO be a callable, for a
291
- # method-name set the plugin only knows at run time (a Sorbet
292
- # catalog's keys, a config-derived DSL method name):
244
+ # ADR-52 slice 4 — `methods:` may ALSO be a callable, for a method-name set the plugin only knows at
245
+ # run time (a Sorbet catalog's keys, a config-derived DSL method name):
293
246
  #
294
247
  # dynamic_return methods: -> { catalog.method_names } do |call_node, scope|
295
248
  # ...
296
249
  # end
297
250
  #
298
- # Same contract as a callable `receivers:` — `instance_exec`'d,
299
- # resolved lazily after `#prepare`, memoised, idempotent. A
300
- # callable method set cannot be compiled into the registry's
301
- # name gate (it is unknown at registry-build time), so the
302
- # plugin is consulted on every dispatch and the name filter runs
303
- # in this instance path instead the block still only fires for
304
- # a listed name, so diagnostics are unchanged.
305
- # ADR-52 slice 5a `file_methods:` is the per-file
306
- # specialisation of the run-time `methods:` callable, for a name
307
- # set that varies per analysed file (rigor-rspec's `let` names —
308
- # the names depend on each file's `describe`/`let` structure, so
309
- # one run-wide set cannot exist). The callable receives the file
310
- # path, runs through `instance_exec`, and is memoised per
311
- # `(rule, path)`:
251
+ # Same contract as a callable `receivers:` — `instance_exec`'d, resolved lazily after `#prepare`,
252
+ # memoised, idempotent. A callable method set cannot be compiled into the registry's name gate (it is
253
+ # unknown at registry-build time), so the plugin is consulted on every dispatch and the name filter
254
+ # runs in this instance path instead the block still only fires for a listed name, so diagnostics
255
+ # are unchanged.
256
+ # ADR-52 slice 5a `file_methods:` is the per-file specialisation of the run-time `methods:`
257
+ # callable, for a name set that varies per analysed file (rigor-rspec's `let` names — the names
258
+ # depend on each file's `describe`/`let` structure, so one run-wide set cannot exist). The callable
259
+ # receives the file path, runs through `instance_exec`, and is memoised per `(rule, path)`:
312
260
  #
313
261
  # dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
314
262
  # ...
315
263
  # end
316
264
  #
317
- # Same idempotence contract as the other callables, plus: it MUST
318
- # tolerate any path the engine analyses (return `[]` / nil for a
319
- # file it has no names for never raise). Like a callable
320
- # `methods:`, it cannot compile into the registry name gate, so
321
- # the plugin is consulted on every dispatch and filtered here.
322
- # `file_methods:` replaces `methods:` (declaring both is
323
- # rejected — they are the same gate at two scopes); it MAY
324
- # combine with `receivers:`.
265
+ # Same idempotence contract as the other callables, plus: it MUST tolerate any path the engine
266
+ # analyses (return `[]` / nil for a file it has no names for — never raise). Like a callable
267
+ # `methods:`, it cannot compile into the registry name gate, so the plugin is consulted on every
268
+ # dispatch and filtered here. `file_methods:` replaces `methods:` (declaring both is rejected they
269
+ # are the same gate at two scopes); it MAY combine with `receivers:`.
325
270
  def dynamic_return(receivers: nil, methods: nil, file_methods: nil, &block)
326
271
  raise ArgumentError, "Plugin::Base.dynamic_return requires a block body" if block.nil?
327
272
 
@@ -340,8 +285,8 @@ module Rigor
340
285
  nil
341
286
  end
342
287
 
343
- # A class-name Array is frozen element-wise; a run-time callable
344
- # (ADR-52 slice 3) is stored verbatim and resolved per instance.
288
+ # A class-name Array is frozen element-wise; a run-time callable (ADR-52 slice 3) is stored verbatim
289
+ # and resolved per instance.
345
290
  def normalize_dynamic_return_receivers(receivers)
346
291
  return nil if receivers.nil?
347
292
  return receivers if receivers.respond_to?(:call)
@@ -350,9 +295,8 @@ module Rigor
350
295
  end
351
296
  private :normalize_dynamic_return_receivers
352
297
 
353
- # A method-name Array is symbol-normalised + frozen; a run-time
354
- # callable (ADR-52 slice 4) is stored verbatim and resolved per
355
- # instance.
298
+ # A method-name Array is symbol-normalised + frozen; a run-time callable (ADR-52 slice 4) is stored
299
+ # verbatim and resolved per instance.
356
300
  def normalize_dynamic_return_methods(methods)
357
301
  return nil if methods.nil?
358
302
  return methods if methods.respond_to?(:call)
@@ -361,26 +305,21 @@ module Rigor
361
305
  end
362
306
  private :normalize_dynamic_return_methods
363
307
 
364
- # Frozen snapshot of the declared dynamic-return rules. Memoised:
365
- # `@dynamic_returns` is built once at class-definition time (via
366
- # `dynamic_return`) and never mutated during analysis, and every
367
- # element is already frozen, so a fresh `dup.freeze` per call was
368
- # pure waste the engine calls this for every plugin on every
369
- # dispatch (`collect_plugin_contributions`), making it a top
370
- # allocation site on plugin-heavy projects. The cached frozen
371
- # array is immutable, so sharing one instance across callers is
372
- # safe.
373
- # rubocop:disable Naming/MemoizedInstanceVariableName -- the
374
- # natural name `@dynamic_returns` is the canonical (mutable-at-
375
- # definition) store this snapshots; the memo must be distinct.
308
+ # Frozen snapshot of the declared dynamic-return rules. Memoised: `@dynamic_returns` is built once at
309
+ # class-definition time (via `dynamic_return`) and never mutated during analysis, and every element
310
+ # is already frozen, so a fresh `dup.freeze` per call was pure waste the engine calls this for
311
+ # every plugin on every dispatch (`collect_plugin_contributions`), making it a top allocation site on
312
+ # plugin-heavy projects. The cached frozen array is immutable, so sharing one instance across callers
313
+ # is safe.
314
+ # rubocop:disable Naming/MemoizedInstanceVariableName -- the natural name `@dynamic_returns` is the
315
+ # canonical (mutable-at-definition) store this snapshots; the memo must be distinct.
376
316
  def dynamic_returns
377
317
  @dynamic_returns_snapshot ||= (@dynamic_returns || []).dup.freeze
378
318
  end
379
319
  # rubocop:enable Naming/MemoizedInstanceVariableName
380
320
 
381
- # ADR-52 WD2 — a rule must gate on something. `receivers:` alone,
382
- # `methods:` alone, or both are valid; neither is not (it would
383
- # fire on every dispatch).
321
+ # ADR-52 WD2 — a rule must gate on something. `receivers:` alone, `methods:` alone, or both are
322
+ # valid; neither is not (it would fire on every dispatch).
384
323
  def validate_dynamic_return_gate!(receivers, methods, file_methods)
385
324
  return unless receivers.nil? && file_methods.nil?
386
325
  return if (methods.is_a?(Array) && !methods.empty?) || methods.respond_to?(:call)
@@ -390,9 +329,8 @@ module Rigor
390
329
  "gated on none would fire on every dispatch (that is what flow_contribution_for is for)"
391
330
  end
392
331
 
393
- # ADR-52 slice 5a — `file_methods:` must be a callable, and is
394
- # mutually exclusive with `methods:` (one name gate, two scopes —
395
- # declaring both is a contradiction, not a composition).
332
+ # ADR-52 slice 5a — `file_methods:` must be a callable, and is mutually exclusive with `methods:`
333
+ # (one name gate, two scopes — declaring both is a contradiction, not a composition).
396
334
  def validate_dynamic_return_file_methods!(file_methods, methods)
397
335
  return if file_methods.nil?
398
336
 
@@ -409,8 +347,8 @@ module Rigor
409
347
  end
410
348
 
411
349
  def validate_dynamic_return_receivers!(receivers)
412
- # ADR-52 slice 3 — a run-time callable is resolved per instance
413
- # after `#prepare`; its shape is checked at resolution time.
350
+ # ADR-52 slice 3 — a run-time callable is resolved per instance after `#prepare`; its shape is
351
+ # checked at resolution time.
414
352
  return if receivers.respond_to?(:call)
415
353
  return if receivers.is_a?(Array) && !receivers.empty? && receivers.all? { |r| r.is_a?(String) && !r.empty? }
416
354
 
@@ -421,8 +359,8 @@ module Rigor
421
359
 
422
360
  def validate_dynamic_return_methods!(methods)
423
361
  return if methods.nil?
424
- # ADR-52 slice 4 — a run-time callable resolves to the name set
425
- # per instance after `#prepare`; its shape is checked then.
362
+ # ADR-52 slice 4 — a run-time callable resolves to the name set per instance after `#prepare`; its
363
+ # shape is checked then.
426
364
  return if methods.respond_to?(:call)
427
365
  return if methods.is_a?(Array) && !methods.empty? &&
428
366
  methods.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
@@ -435,30 +373,24 @@ module Rigor
435
373
  private :validate_dynamic_return_gate!, :validate_dynamic_return_receivers!,
436
374
  :validate_dynamic_return_methods!, :validate_dynamic_return_file_methods!
437
375
 
438
- # ADR-37 slice 2 — declares a predicate/assertion narrowing
439
- # contribution, method-gated. The narrow successor to the
440
- # `post_return_facts` slot of the deleted `flow_contribution_for`
441
- # hook (ADR-52 WD3):
376
+ # ADR-37 slice 2 — declares a predicate/assertion narrowing contribution, method-gated. The narrow
377
+ # successor to the `post_return_facts` slot of the deleted `flow_contribution_for` hook (ADR-52 WD3):
442
378
  #
443
379
  # narrowing_facts methods: [:assert_kind_of] do |call_node, scope|
444
380
  # # return an Array of post-return facts, or nil
445
381
  # end
446
382
  #
447
- # `methods:` is a non-empty Array of method names; the engine
448
- # calls the block only when `call_node.name` is one of them. The
449
- # block returns the same `post_return_facts` the merger applies.
383
+ # `methods:` is a non-empty Array of method names; the engine calls the block only when
384
+ # `call_node.name` is one of them. The block returns the same `post_return_facts` the merger applies.
450
385
  #
451
- # This hook supplies post-return *narrowing facts* — the
452
- # predicate/assertion edges a call establishes about its
453
- # arguments or receiver, e.g. `assert_kind_of(String, x)` `x`
454
- # is narrowed to `String` on the continuation. It does NOT give a
455
- # call a *type*; for that use {dynamic_return} (per-call-site) or
456
- # contribute RBS via the manifest `signature_paths:`.
457
- # `dynamic_return` is the type slot, this is the fact slot.
386
+ # This hook supplies post-return *narrowing facts* — the predicate/assertion edges a call establishes
387
+ # about its arguments or receiver, e.g. `assert_kind_of(String, x)` ⇒ `x` is narrowed to `String` on
388
+ # the continuation. It does NOT give a call a *type*; for that use {dynamic_return} (per-call-site)
389
+ # or contribute RBS via the manifest `signature_paths:`. `dynamic_return` is the type slot, this is
390
+ # the fact slot.
458
391
  #
459
- # Renamed from `type_specifier` (ADR-80): the old name read as a
460
- # parallel to `dynamic_return` (a type) when it actually returns
461
- # facts. {.type_specifier} survives as a deprecating alias through
392
+ # Renamed from `type_specifier` (ADR-80): the old name read as a parallel to `dynamic_return` (a
393
+ # type) when it actually returns facts. {.type_specifier} survives as a deprecating alias through
462
394
  # 0.2.x and is removed in 0.3.0.
463
395
  def narrowing_facts(methods:, &block)
464
396
  raise ArgumentError, "Plugin::Base.narrowing_facts requires a block body" if block.nil?
@@ -474,11 +406,10 @@ module Rigor
474
406
  nil
475
407
  end
476
408
 
477
- # DEPRECATED (ADR-80) — renamed to {.narrowing_facts}. This hook
478
- # supplies post-return narrowing *facts*, not a type; the old
479
- # name misleads by parallel with {.dynamic_return}. Retained as a
480
- # warning-emitting alias through 0.2.x; REMOVED in 0.3.0. Migrate
481
- # `type_specifier methods: …` → `narrowing_facts methods: …`.
409
+ # DEPRECATED (ADR-80) — renamed to {.narrowing_facts}. This hook supplies post-return narrowing
410
+ # *facts*, not a type; the old name misleads by parallel with {.dynamic_return}. Retained as a
411
+ # warning-emitting alias through 0.2.x; REMOVED in 0.3.0. Migrate `type_specifier methods: …` →
412
+ # `narrowing_facts methods: …`.
482
413
  def type_specifier(methods:, &)
483
414
  unless @type_specifier_deprecation_warned
484
415
  @type_specifier_deprecation_warned = true
@@ -488,9 +419,9 @@ module Rigor
488
419
  narrowing_facts(methods:, &)
489
420
  end
490
421
 
491
- # Frozen snapshot of the declared type-specifier rules. Memoised
492
- # for the same reason as {dynamic_returns} — consulted per plugin
493
- # per dispatch, over an array fixed at class-definition time.
422
+ # Frozen snapshot of the declared type-specifier rules. Memoised for the same reason as
423
+ # {dynamic_returns} — consulted per plugin per dispatch, over an array fixed at class-definition
424
+ # time.
494
425
  # rubocop:disable Naming/MemoizedInstanceVariableName -- see dynamic_returns
495
426
  def type_specifiers
496
427
  @type_specifiers_snapshot ||= (@type_specifiers || []).dup.freeze
@@ -503,43 +434,35 @@ module Rigor
503
434
  def initialize(services:, config: {})
504
435
  @services = services
505
436
  @config = merge_config_defaults(config).freeze
506
- # ADR-52 slice 3 — per-rule cache of resolved run-time
507
- # `dynamic_return receivers:` callables. Created here (before any
508
- # subclass `initialize` freezes the instance) so the lazy
509
- # memo-on-first-dispatch is a Hash-content mutation, sound even on
510
- # a self-freezing plugin.
437
+ # ADR-52 slice 3 — per-rule cache of resolved run-time `dynamic_return receivers:` callables. Created
438
+ # here (before any subclass `initialize` freezes the instance) so the lazy memo-on-first-dispatch is
439
+ # a Hash-content mutation, sound even on a self-freezing plugin.
511
440
  @dynamic_return_runtime_cache = {}
512
- # ADR-60 WD4 — nil-inclusive memo tables for the authoring
513
- # helpers ({#read_fact} / {#producer_value} / {#producer_error}).
514
- # Allocated here, before any subclass `initialize` self-freeze,
515
- # for the same reason: a populate is a Hash-content mutation.
441
+ # ADR-60 WD4 — nil-inclusive memo tables for the authoring helpers ({#read_fact} / {#producer_value}
442
+ # / {#producer_error}). Allocated here, before any subclass `initialize` self-freeze, for the same
443
+ # reason: a populate is a Hash-content mutation.
516
444
  @fact_cache = {}
517
445
  @producer_value_cache = {}
518
446
  @producer_errors = {}
519
447
  end
520
448
 
521
- # Override in subclasses to wire any state the plugin needs
522
- # from the injected service container. Default is a no-op so
523
- # plugins that only contribute through later-slice protocol
524
- # hooks do not have to define an explicit body.
449
+ # Override in subclasses to wire any state the plugin needs from the injected service container.
450
+ # Default is a no-op so plugins that only contribute through later-slice protocol hooks do not have to
451
+ # define an explicit body.
525
452
  def init(services) # rubocop:disable Lint/UnusedMethodArgument
526
453
  nil
527
454
  end
528
455
 
529
- # NOTE: (ADR-52 WD3): the legacy ungated per-call hook
530
- # `flow_contribution_for` was DELETED here pre-1.0 after its five
531
- # production users migrated. Per-call return types are declared via
532
- # the gated {.dynamic_return} DSL (static / run-time / per-file
533
- # name sets, static / run-time receiver sets); post-return
534
- # narrowing facts via {.narrowing_facts}. See the CHANGELOG
535
- # migration note for the idiom-by-idiom mapping.
536
-
537
- # ADR-9 slice 3 per-run preparation hook. The runner
538
- # invokes `#prepare(services)` on every loaded plugin once
539
- # per `Analysis::Runner.run`, after `#init` has run on every
540
- # plugin and before any `#diagnostics_for_file` call.
541
- # Plugins use this hook to compute and publish facts other
542
- # plugins consume:
456
+ # NOTE: (ADR-52 WD3): the legacy ungated per-call hook `flow_contribution_for` was DELETED here pre-1.0
457
+ # after its five production users migrated. Per-call return types are declared via the gated
458
+ # {.dynamic_return} DSL (static / run-time / per-file name sets, static / run-time receiver sets);
459
+ # post-return narrowing facts via {.narrowing_facts}. See the CHANGELOG migration note for the
460
+ # idiom-by-idiom mapping.
461
+
462
+ # ADR-9 slice 3 per-run preparation hook. The runner invokes `#prepare(services)` on every loaded
463
+ # plugin once per `Analysis::Runner.run`, after `#init` has run on every plugin and before any
464
+ # `#diagnostics_for_file` call. Plugins use this hook to compute and publish facts other plugins
465
+ # consume:
543
466
  #
544
467
  # def prepare(services)
545
468
  # services.fact_store.publish(
@@ -547,72 +470,56 @@ module Rigor
547
470
  # )
548
471
  # end
549
472
  #
550
- # Default no-op so plugins without facts to publish leave
551
- # `#prepare` unimplemented. Failures isolate as
552
- # `:plugin_loader runtime-error` diagnostics; a plugin that
553
- # raises in `#prepare` has its facts considered un-published
554
- # and downstream consumers see `nil` from `fact_store.read`.
473
+ # Default no-op so plugins without facts to publish leave `#prepare` unimplemented. Failures isolate as
474
+ # `:plugin_loader runtime-error` diagnostics; a plugin that raises in `#prepare` has its facts
475
+ # considered un-published and downstream consumers see `nil` from `fact_store.read`.
555
476
  #
556
- # Slice 3 calls plugins in registration order. ADR-9 slice 5
557
- # introduces topological ordering by `consumes:` so producers
558
- # always run before consumers.
477
+ # Slice 3 calls plugins in registration order. ADR-9 slice 5 introduces topological ordering by
478
+ # `consumes:` so producers always run before consumers.
559
479
  def prepare(services) # rubocop:disable Lint/UnusedMethodArgument
560
480
  nil
561
481
  end
562
482
 
563
- # ADR-7 § "Slice 5-A" — per-file diagnostic emission hook.
564
- # Override in plugin subclasses to return an array of
565
- # `Rigor::Analysis::Diagnostic` rows for the analysed file.
566
- # The runner stamps each returned diagnostic with
567
- # `source_family: "plugin.<manifest.id>"` automatically per
568
- # ADR-7 § "Slice 5-B"; plugin authors should construct
569
- # diagnostics without setting `source_family` (any value
570
- # they pass is overwritten).
483
+ # ADR-7 § "Slice 5-A" — per-file diagnostic emission hook. Override in plugin subclasses to return an
484
+ # array of `Rigor::Analysis::Diagnostic` rows for the analysed file. The runner stamps each returned
485
+ # diagnostic with `source_family: "plugin.<manifest.id>"` automatically per ADR-7 § "Slice 5-B"; plugin
486
+ # authors should construct diagnostics without setting `source_family` (any value they pass is
487
+ # overwritten).
571
488
  #
572
- # `path` is the analysed file path; `scope` is the entry
573
- # `Rigor::Scope` after `ScopeIndexer` ran; `root` is the
574
- # parsed `Prism::Node` root. Plugin authors can traverse
575
- # `root` themselves or declare rules via the `.node_rule` DSL
576
- # (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base
577
- # class mentioned in ADR-2 was superseded by the block-based
578
- # `.node_rule` DSL.
489
+ # `path` is the analysed file path; `scope` is the entry `Rigor::Scope` after `ScopeIndexer` ran; `root`
490
+ # is the parsed `Prism::Node` root. Plugin authors can traverse `root` themselves or declare rules via
491
+ # the `.node_rule` DSL (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base class mentioned in ADR-2
492
+ # was superseded by the block-based `.node_rule` DSL.
579
493
  #
580
- # Default returns `[]` so plugins that contribute through
581
- # other channels (e.g. slice-4 narrowing contributions,
582
- # slice-6 cache producers) do not have to override.
494
+ # Default returns `[]` so plugins that contribute through other channels (e.g. slice-4 narrowing
495
+ # contributions, slice-6 cache producers) do not have to override.
583
496
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
584
497
  []
585
498
  end
586
499
 
587
- # ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over
588
- # one file and returns their diagnostics. The engine owns the
589
- # single AST walk here so plugin authors never hand-roll a
590
- # traversal: every node reachable from `root` is offered to each
591
- # rule whose `node_type` it satisfies (`node.is_a?`), the rule's
592
- # block is `instance_exec`'d on this plugin instance with
593
- # `(node, scope, path)`, and the returned diagnostics are
594
- # concatenated in (node, declaration) order.
500
+ # ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over one file and returns their
501
+ # diagnostics. The engine owns the single AST walk here so plugin authors never hand-roll a traversal:
502
+ # every node reachable from `root` is offered to each rule whose `node_type` it satisfies
503
+ # (`node.is_a?`), the rule's block is `instance_exec`'d on this plugin instance with
504
+ # `(node, scope, path)`, and the returned diagnostics are concatenated in (node, declaration) order.
595
505
  #
596
- # Returns `[]` immediately when the plugin declares no node rules,
597
- # so it is a zero-cost no-op for every plugin that does not use
598
- # the DSL. The runner calls it alongside `#diagnostics_for_file`
599
- # and stamps `plugin.<id>` provenance on the result; a raise
600
- # propagates to the runner's per-plugin isolation boundary.
506
+ # Returns `[]` immediately when the plugin declares no node rules, so it is a zero-cost no-op for every
507
+ # plugin that does not use the DSL. The runner calls it alongside `#diagnostics_for_file` and stamps
508
+ # `plugin.<id>` provenance on the result; a raise propagates to the runner's per-plugin isolation
509
+ # boundary.
601
510
  def node_rule_diagnostics(path:, scope:, root:)
602
511
  rules = self.class.node_rules
603
512
  return [] if rules.empty? || root.nil?
604
513
 
605
- # ADR-37 slice 1c — build the per-file context once (the
606
- # "collect" pass) before the engine-owned validate walk, so a
607
- # two-pass plugin sees the closed namespace at every node.
514
+ # ADR-37 slice 1c — build the per-file context once (the "collect" pass) before the engine-owned
515
+ # validate walk, so a two-pass plugin sees the closed namespace at every node.
608
516
  context_block = self.class.node_file_context_block
609
517
  file_context = context_block ? instance_exec(root, scope, &context_block) : nil
610
518
 
611
519
  diagnostics = []
612
520
  Source::NodeWalker.each_with_ancestors(root) do |node, ancestors|
613
- # One frozen NodeContext per node, shared across the rules
614
- # that match it (ADR-52 WD1) built lazily so non-matching
615
- # nodes (the vast majority) allocate nothing.
521
+ # One frozen NodeContext per node, shared across the rules that match it (ADR-52 WD1) — built
522
+ # lazily so non-matching nodes (the vast majority) allocate nothing.
616
523
  context = nil
617
524
  rules.each do |rule|
618
525
  next unless node.is_a?(rule[:node_type])
@@ -624,20 +531,17 @@ module Rigor
624
531
  diagnostics
625
532
  end
626
533
 
627
- # ADR-37 slice 2 — the return type contributed by this plugin's
628
- # {.dynamic_return} rules for a call, or nil. The engine calls this
629
- # from `MethodDispatcher`; a rule fires only when `receiver_type`'s
630
- # class equals or inherits from one of its declared `receivers:`.
631
- # First non-nil wins (declaration order). Failures isolate to nil.
534
+ # ADR-37 slice 2 — the return type contributed by this plugin's {.dynamic_return} rules for a call, or
535
+ # nil. The engine calls this from `MethodDispatcher`; a rule fires only when `receiver_type`'s class
536
+ # equals or inherits from one of its declared `receivers:`. First non-nil wins (declaration order).
537
+ # Failures isolate to nil.
632
538
  def dynamic_return_type(call_node:, scope:, receiver_type:)
633
539
  rules = self.class.dynamic_returns
634
540
  return nil if rules.empty? || receiver_type.nil?
635
541
 
636
- # `class_name` is nil for a receiver carrier with no nominal
637
- # class (a refinement dimension, an inferred shape) fine for a
638
- # receiver-less (methods-only) rule (ADR-52 WD2), which gates on
639
- # the method name alone and reads the receiver shape inside its
640
- # own block.
542
+ # `class_name` is nil for a receiver carrier with no nominal class (a refinement dimension, an
543
+ # inferred shape) — fine for a receiver-less (methods-only) rule (ADR-52 WD2), which gates on the
544
+ # method name alone and reads the receiver shape inside its own block.
641
545
  class_name = dynamic_return_receiver_class_name(receiver_type)
642
546
  environment = scope&.environment
643
547
  rules.each do |rule|
@@ -651,11 +555,9 @@ module Rigor
651
555
  nil
652
556
  end
653
557
 
654
- # ADR-37 slice 2 — the post-return narrowing facts contributed by
655
- # this plugin's {.narrowing_facts} rules for a call. The engine
656
- # calls this from `StatementEvaluator`; a rule fires only when
657
- # `call_node.name`
658
- # is one of its declared `methods:`. Failures isolate to [].
558
+ # ADR-37 slice 2 — the post-return narrowing facts contributed by this plugin's {.narrowing_facts}
559
+ # rules for a call. The engine calls this from `StatementEvaluator`; a rule fires only when
560
+ # `call_node.name` is one of its declared `methods:`. Failures isolate to [].
659
561
  def type_specifier_facts(call_node:, scope:)
660
562
  rules = self.class.type_specifiers
661
563
  return [] if rules.empty? || !call_node.respond_to?(:name)
@@ -673,23 +575,17 @@ module Rigor
673
575
  []
674
576
  end
675
577
 
676
- # Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism
677
- # `node` for return from `#diagnostics_for_file`. Internalises the
678
- # 1-based `line` / `start_column + 1` convention every plugin
679
- # otherwise re-derives by hand, so authors pass the node and the
680
- # message/severity/rule rather than unpacking `node.location`.
578
+ # Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism `node` for return from
579
+ # `#diagnostics_for_file`. Internalises the 1-based `line` / `start_column + 1` convention every plugin
580
+ # otherwise re-derives by hand, so authors pass the node and the message/severity/rule rather than
581
+ # unpacking `node.location`.
681
582
  #
682
- # `source_family` is intentionally NOT accepted — the runner
683
- # stamps `plugin.<manifest.id>` on every returned diagnostic
684
- # (ADR-7 § "Slice 5-B"), so any value set here would be
685
- # overwritten.
686
- # Pass `location:` (a Prism location) to point the diagnostic at a
687
- # sub-location of `node` rather than `node.location` — typically
688
- # `node.message_loc` so a matcher / method-name diagnostic points
689
- # at the name, not the receiver-spanning whole call. A `nil`
690
- # `location:` falls back to `node.location`, so
691
- # `location: node.message_loc` reproduces the common
692
- # `message_loc || location` idiom.
583
+ # `source_family` is intentionally NOT accepted — the runner stamps `plugin.<manifest.id>` on every
584
+ # returned diagnostic (ADR-7 § "Slice 5-B"), so any value set here would be overwritten.
585
+ # Pass `location:` (a Prism location) to point the diagnostic at a sub-location of `node` rather than
586
+ # `node.location` — typically `node.message_loc` so a matcher / method-name diagnostic points at the
587
+ # name, not the receiver-spanning whole call. A `nil` `location:` falls back to `node.location`, so
588
+ # `location: node.message_loc` reproduces the common `message_loc || location` idiom.
693
589
  def diagnostic(node, path:, message:, severity: :error, rule: nil, location: nil)
694
590
  Analysis::Diagnostic.from_location(
695
591
  location || node.location,
@@ -697,16 +593,13 @@ module Rigor
697
593
  )
698
594
  end
699
595
 
700
- # ADR-60 WD4 — maps a plugin's own violation objects to
701
- # `Rigor::Analysis::Diagnostic`s through {#diagnostic}, absorbing
702
- # the `violations.map { |v| diagnostic(node, …) }` block the
703
- # node-rule plugins otherwise repeat. Each violation duck-types:
704
- # `#message` (required); optional `#node` (the Prism node to
705
- # position at falls back to the `node:` argument, the common
706
- # "all violations point at the same call" case), `#location` (a
707
- # sub-location such as `node.message_loc`), `#severity` (defaults
708
- # `:error`), and `#rule`. Returns an Array suitable for direct
709
- # return from `#diagnostics_for_file` / a `node_rule` block.
596
+ # ADR-60 WD4 — maps a plugin's own violation objects to `Rigor::Analysis::Diagnostic`s through
597
+ # {#diagnostic}, absorbing the `violations.map { |v| diagnostic(node, …) }` block the node-rule plugins
598
+ # otherwise repeat. Each violation duck-types: `#message` (required); optional `#node` (the Prism node
599
+ # to position at — falls back to the `node:` argument, the common "all violations point at the same
600
+ # call" case), `#location` (a sub-location such as `node.message_loc`), `#severity` (defaults
601
+ # `:error`), and `#rule`. Returns an Array suitable for direct return from `#diagnostics_for_file` / a
602
+ # `node_rule` block.
710
603
  def diagnostics_for(violations, path:, node: nil)
711
604
  Array(violations).map do |violation|
712
605
  target = (violation.node if violation.respond_to?(:node)) || node
@@ -721,13 +614,11 @@ module Rigor
721
614
  end
722
615
  end
723
616
 
724
- # ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by
725
- # another plugin's `#prepare` hook, memoised per `(plugin_id,
726
- # name)` on this instance INCLUDING a nil result. The nil-inclusive
727
- # memo retires the hand-rolled `@x_resolved` flag the discovery
728
- # plugins carried to distinguish "fact not published" from "not yet
729
- # read". `services.fact_store` is the only sanctioned cross-plugin
730
- # channel; a fact no loaded producer published reads as nil.
617
+ # ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by another plugin's `#prepare` hook,
618
+ # memoised per `(plugin_id, name)` on this instance INCLUDING a nil result. The nil-inclusive memo
619
+ # retires the hand-rolled `@x_resolved` flag the discovery plugins carried to distinguish "fact not
620
+ # published" from "not yet read". `services.fact_store` is the only sanctioned cross-plugin channel; a
621
+ # fact no loaded producer published reads as nil.
731
622
  def read_fact(plugin_id:, name:)
732
623
  key = [plugin_id.to_s, name.to_sym].freeze
733
624
  return @fact_cache[key] if @fact_cache.key?(key)
@@ -735,13 +626,11 @@ module Rigor
735
626
  @fact_cache[key] = services.fact_store.read(plugin_id: plugin_id.to_s, name: name.to_sym)
736
627
  end
737
628
 
738
- # ADR-60 WD4 — runs a declared {.producer} through {#cache_for}
739
- # and returns its value, memoised per `(id, params)` INCLUDING nil.
740
- # A `StandardError` the producer raises (a malformed project file,
741
- # an I/O failure) is rescued, recorded for {#producer_error}, and
742
- # yields nil so one bad project file degrades a plugin to silence
743
- # rather than aborting the whole run. This is the `*_index_or_nil`
744
- # shape the discovery plugins hand-rolled, named once.
629
+ # ADR-60 WD4 — runs a declared {.producer} through {#cache_for} and returns its value, memoised per
630
+ # `(id, params)` INCLUDING nil. A `StandardError` the producer raises (a malformed project file, an I/O
631
+ # failure) is rescued, recorded for {#producer_error}, and yields nil — so one bad project file
632
+ # degrades a plugin to silence rather than aborting the whole run. This is the `*_index_or_nil` shape
633
+ # the discovery plugins hand-rolled, named once.
745
634
  def producer_value(id, params: {})
746
635
  key = [id.to_sym, params].freeze
747
636
  return @producer_value_cache[key] if @producer_value_cache.key?(key)
@@ -752,23 +641,19 @@ module Rigor
752
641
  @producer_value_cache[key] = nil
753
642
  end
754
643
 
755
- # ADR-60 WD4 — the `StandardError` a prior {#producer_value} call
756
- # rescued for `id`, or nil when it succeeded or was never called.
757
- # Plugins surface it as a load-error diagnostic from
644
+ # ADR-60 WD4 — the `StandardError` a prior {#producer_value} call rescued for `id`, or nil when it
645
+ # succeeded or was never called. Plugins surface it as a load-error diagnostic from
758
646
  # `#diagnostics_for_file`.
759
647
  def producer_error(id)
760
648
  @producer_errors[id.to_sym]
761
649
  end
762
650
 
763
- # Boilerplate-reduction helper (review §1.3): the "did you mean …?"
764
- # suggestion every diagnostic-emitting plugin otherwise hand-rolls.
765
- # Returns the closest of `candidates` to `name` via
766
- # `DidYouMean::SpellChecker` (the same engine Ruby's own
767
- # `NoMethodError` hints use), or `nil` when there is no good match /
768
- # no candidates replacing the per-plugin Levenshtein copies. A
769
- # **class** method so it is callable both from a plugin instance
770
- # (`Rigor::Plugin::Base.suggest(...)`) and from an `Analyzer` module
771
- # function that has no instance.
651
+ # Boilerplate-reduction helper (review §1.3): the "did you mean …?" suggestion every
652
+ # diagnostic-emitting plugin otherwise hand-rolls. Returns the closest of `candidates` to `name` via
653
+ # `DidYouMean::SpellChecker` (the same engine Ruby's own `NoMethodError` hints use), or `nil` when
654
+ # there is no good match / no candidates — replacing the per-plugin Levenshtein copies. A **class**
655
+ # method so it is callable both from a plugin instance (`Rigor::Plugin::Base.suggest(...)`) and from an
656
+ # `Analyzer` module function that has no instance.
772
657
  def self.suggest(name, candidates)
773
658
  dictionary = Array(candidates).map(&:to_s)
774
659
  return nil if dictionary.empty?
@@ -776,23 +661,18 @@ module Rigor
776
661
  DidYouMean::SpellChecker.new(dictionary: dictionary).correct(name.to_s).first
777
662
  end
778
663
 
779
- # Convenience accessor — `manifest` on the instance returns
780
- # the class-level manifest declaration.
664
+ # Convenience accessor — `manifest` on the instance returns the class-level manifest declaration.
781
665
  def manifest
782
666
  self.class.manifest
783
667
  end
784
668
 
785
- # ADR-25 — absolute RBS signature directories this plugin
786
- # contributes. Resolves each `manifest.signature_paths` entry
787
- # (declared relative to the plugin gem root) against that
788
- # root. The gem root is the directory above `lib/` in the
789
- # file that defined the plugin class (falling back to that
790
- # file's directory for a non-conventional layout). Returns
791
- # `[]` when the manifest declares no `signature_paths:` or
792
- # the class is anonymous (an anonymous class cannot ship a
793
- # gem). `Plugin::Loader` validates the resolved dirs exist at
794
- # load time; `Environment.for_project` merges them into the
795
- # signature-path set fed to `RbsLoader`.
669
+ # ADR-25 — absolute RBS signature directories this plugin contributes. Resolves each
670
+ # `manifest.signature_paths` entry (declared relative to the plugin gem root) against that root. The
671
+ # gem root is the directory above `lib/` in the file that defined the plugin class (falling back to
672
+ # that file's directory for a non-conventional layout). Returns `[]` when the manifest declares no
673
+ # `signature_paths:` or the class is anonymous (an anonymous class cannot ship a gem).
674
+ # `Plugin::Loader` validates the resolved dirs exist at load time; `Environment.for_project` merges
675
+ # them into the signature-path set fed to `RbsLoader`.
796
676
  def signature_paths
797
677
  relative = manifest.signature_paths
798
678
  return [] if relative.empty?
@@ -808,60 +688,42 @@ module Rigor
808
688
  relative.map { |rel| File.expand_path(rel, root) }
809
689
  end
810
690
 
811
- # ADR-28 — the path-scoped method-protocol contracts this
812
- # plugin contributes. Defaults to the manifest-declared
813
- # `protocol_contracts:`; the same indirection
814
- # `#signature_paths` uses, so a plugin MAY override this to
815
- # fold per-project config into the contract set (e.g.
816
- # substituting the convention `path_glob` with a user-supplied
817
- # one) without the manifest having to be config-aware.
818
- # `Plugin::Registry#protocol_contracts` aggregates the result
819
- # across loaded plugins.
691
+ # ADR-28 — the path-scoped method-protocol contracts this plugin contributes. Defaults to the
692
+ # manifest-declared `protocol_contracts:`; the same indirection `#signature_paths` uses, so a plugin
693
+ # MAY override this to fold per-project config into the contract set (e.g. substituting the convention
694
+ # `path_glob` with a user-supplied one) without the manifest having to be config-aware.
695
+ # `Plugin::Registry#protocol_contracts` aggregates the result across loaded plugins.
820
696
  def protocol_contracts
821
697
  manifest.protocol_contracts
822
698
  end
823
699
 
824
- # ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}.
825
- # Memoised so the boundary's accumulated `FileEntry`
826
- # rows persist across producer invocations within the
827
- # same plugin instance and feed cache invalidation
700
+ # ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}. Memoised so the boundary's accumulated `FileEntry`
701
+ # rows persist across producer invocations within the same plugin instance and feed cache invalidation
828
702
  # via `cache_for`.
829
703
  def io_boundary
830
704
  @io_boundary ||= services.io_boundary_for(manifest.id)
831
705
  end
832
706
 
833
- # ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that
834
- # performs a `Cache::Store#fetch_or_validate` round-trip for
835
- # the named producer (the ADR-45 record-and-validate path).
836
- # The entry is KEYED on the stable identity inputs the
837
- # plugin's `PluginEntry` template (id, version, config_hash)
838
- # composed with the optional `descriptor:` extras and
839
- # stores, beside the value, a DEPENDENCY descriptor recorded
840
- # AFTER the producer block ran: the {IoBoundary}'s
841
- # post-compute read history plus the evaluated `watch:`
842
- # {Cache::Descriptor::GlobEntry} rows. In-block reads are
843
- # therefore always captured (the structural stale-cache
844
- # hazard `fetch_or_compute`'s call-time snapshot carried);
845
- # the next run re-validates the recorded dependencies by
846
- # re-digest (`Descriptor#fresh?`) and recomputes when any
847
- # changed. The producer id is auto-prefixed
848
- # `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
707
+ # ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that performs a `Cache::Store#fetch_or_validate`
708
+ # round-trip for the named producer (the ADR-45 record-and-validate path). The entry is KEYED on the
709
+ # stable identity inputs — the plugin's `PluginEntry` template (id, version, config_hash) composed with
710
+ # the optional `descriptor:` extras and stores, beside the value, a DEPENDENCY descriptor recorded
711
+ # AFTER the producer block ran: the {IoBoundary}'s post-compute read history plus the evaluated
712
+ # `watch:` {Cache::Descriptor::GlobEntry} rows. In-block reads are therefore always captured (the
713
+ # structural stale-cache hazard `fetch_or_compute`'s call-time snapshot carried); the next run
714
+ # re-validates the recorded dependencies by re-digest (`Descriptor#fresh?`) and recomputes when any
715
+ # changed. The producer id is auto-prefixed `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
849
716
  # caches stay sandboxed from built-in producers.
850
717
  #
851
- # When `services.cache_store` is `nil` (e.g. CLI
852
- # `--no-cache`), the callable bypasses the cache and
853
- # runs the producer block every time — same semantics
854
- # as the v0.0.9 cache surface for built-in producers.
718
+ # When `services.cache_store` is `nil` (e.g. CLI `--no-cache`), the callable bypasses the cache and
719
+ # runs the producer block every time — same semantics as the v0.0.9 cache surface for built-in
720
+ # producers.
855
721
  #
856
- # `descriptor:` (optional) supplies extra `Cache::Descriptor`
857
- # rows for IDENTITY inputsgem-version `GemEntry` pins,
858
- # `ConfigEntry` rows for external state that compose into
859
- # the cache KEY via `Cache::Descriptor.compose`; per-slot
860
- # conflicts raise `Cache::Descriptor::Conflict` to make
861
- # divergent inputs visible rather than silently shadowing.
862
- # A key change is a miss, so the invalidation effect of the
863
- # legacy `glob_descriptor`-as-`descriptor:` idiom is
864
- # preserved unchanged.
722
+ # `descriptor:` (optional) supplies extra `Cache::Descriptor` rows for IDENTITY inputs — gem-version
723
+ # `GemEntry` pins, `ConfigEntry` rows for external statethat compose into the cache KEY via
724
+ # `Cache::Descriptor.compose`; per-slot conflicts raise `Cache::Descriptor::Conflict` to make divergent
725
+ # inputs visible rather than silently shadowing. A key change is a miss, so the invalidation effect of
726
+ # the legacy `glob_descriptor`-as-`descriptor:` idiom is preserved unchanged.
865
727
  def cache_for(producer_id, params: {}, descriptor: nil)
866
728
  producer = self.class.producers[producer_id.to_sym]
867
729
  unless producer
@@ -889,28 +751,20 @@ module Rigor
889
751
  end
890
752
  end
891
753
 
892
- # Builds a `Cache::Descriptor` covering every file matched by
893
- # `pattern` (a glob, e.g. `"**/*.rb"`) under any of `roots`.
894
- # Each matching file contributes a `:digest`-comparator
895
- # `FileEntry` so the cache invalidates on any content change,
896
- # any addition (a newly-glob-matched file appears in the
897
- # descriptor), or any removal (the previously-matched file
898
- # drops out).
754
+ # Builds a `Cache::Descriptor` covering every file matched by `pattern` (a glob, e.g. `"**/*.rb"`)
755
+ # under any of `roots`. Each matching file contributes a `:digest`-comparator `FileEntry` so the cache
756
+ # invalidates on any content change, any addition (a newly-glob-matched file appears in the
757
+ # descriptor), or any removal (the previously-matched file drops out).
899
758
  #
900
- # ADR-60 WD3 made this **private**: the declared way for a
901
- # discovery-style producer to cover its glob is `producer
902
- # watch:` (one {Cache::Descriptor::GlobEntry} per glob in the
903
- # record-and-validate dependency descriptor), not a hand-built
904
- # descriptor composed into the cache *key*. The method survives
905
- # only as the building block for the rare producer that needs
906
- # `FileEntry` rows directly; plugin code calls `watch:`.
759
+ # ADR-60 WD3 made this **private**: the declared way for a discovery-style producer to cover its glob
760
+ # is `producer watch:` (one {Cache::Descriptor::GlobEntry} per glob in the record-and-validate
761
+ # dependency descriptor), not a hand-built descriptor composed into the cache *key*. The method
762
+ # survives only as the building block for the rare producer that needs `FileEntry` rows directly;
763
+ # plugin code calls `watch:`.
907
764
  #
908
- # @param roots [Array<String>] search roots (relative to
909
- # the project root, or absolute paths)
910
- # @param patterns [Array<String>] glob suffixes joined under
911
- # each root via `File.join(root, pattern)`. Multiple
912
- # patterns union into one descriptor (`"**/*.erb",
913
- # "**/*.html"` etc.).
765
+ # @param roots [Array<String>] search roots (relative to the project root, or absolute paths)
766
+ # @param patterns [Array<String>] glob suffixes joined under each root via `File.join(root, pattern)`.
767
+ # Multiple patterns union into one descriptor (`"**/*.erb", "**/*.html"` etc.).
914
768
  # @return [Rigor::Cache::Descriptor]
915
769
  def glob_descriptor(roots, *patterns)
916
770
  files = collect_glob_files(Array(roots), patterns)
@@ -927,11 +781,10 @@ module Rigor
927
781
 
928
782
  private
929
783
 
930
- # ADR-40 — merge the manifest's declared `config_schema`
931
- # `default:` values *under* the user-supplied config (user wins),
932
- # so a plugin reads `config.fetch("key")` and gets the declared
933
- # default with no `DEFAULT_*` constant. A class declared without a
934
- # manifest (test doubles) keeps the raw config unchanged.
784
+ # ADR-40 — merge the manifest's declared `config_schema` `default:` values *under* the user-supplied
785
+ # config (user wins), so a plugin reads `config.fetch("key")` and gets the declared default with no
786
+ # `DEFAULT_*` constant. A class declared without a manifest (test doubles) keeps the raw config
787
+ # unchanged.
935
788
  def merge_config_defaults(config)
936
789
  unless self.class.instance_variable_defined?(:@manifest) && self.class.instance_variable_get(:@manifest)
937
790
  return config
@@ -940,28 +793,24 @@ module Rigor
940
793
  self.class.manifest.config_defaults.merge(config)
941
794
  end
942
795
 
943
- # ADR-37 slice 2 — the class name to match a `dynamic_return`
944
- # `receivers:` entry against, from a receiver `Type`. Covers the
945
- # instance (`Nominal[X]`) and class (`Singleton[X]`) shapes; other
946
- # carriers decline (nil → no match).
796
+ # ADR-37 slice 2 — the class name to match a `dynamic_return` `receivers:` entry against, from a
797
+ # receiver `Type`. Covers the instance (`Nominal[X]`) and class (`Singleton[X]`) shapes; other carriers
798
+ # decline (nil no match).
947
799
  def dynamic_return_receiver_class_name(receiver_type)
948
800
  case receiver_type
949
801
  when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
950
802
  end
951
803
  end
952
804
 
953
- # The gate for one `dynamic_return` rule. Method-name gate first —
954
- # a Symbol-array probe vs the receiver ancestry resolution below
955
- # (ADR-52 WD1); both are pure predicates, so order only affects
956
- # cost. A receiver-less rule (ADR-52 WD2) skips the ancestry check
957
- # entirely and fires on the method name alone.
805
+ # The gate for one `dynamic_return` rule. Method-name gate first — a Symbol-array probe vs the receiver
806
+ # ancestry resolution below (ADR-52 WD1); both are pure predicates, so order only affects cost. A
807
+ # receiver-less rule (ADR-52 WD2) skips the ancestry check entirely and fires on the method name alone.
958
808
  def dynamic_return_rule_applies?(rule, call_node, class_name, environment, scope)
959
809
  return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(call_node.name)
960
810
 
961
811
  if rule[:file_methods]
962
- # The path is read here, not in `dynamic_return_type`, so a
963
- # spec-double scope without `source_path` only affects
964
- # `file_methods:` rules (other gate forms never touch it).
812
+ # The path is read here, not in `dynamic_return_type`, so a spec-double scope without
813
+ # `source_path` only affects `file_methods:` rules (other gate forms never touch it).
965
814
  path = scope.respond_to?(:source_path) ? scope.source_path : nil
966
815
  return false unless resolved_dynamic_return_file_methods(rule, path).include?(call_node.name)
967
816
  end
@@ -973,12 +822,10 @@ module Rigor
973
822
  receivers.any? { |c| class_matches_receiver?(class_name, c, environment) }
974
823
  end
975
824
 
976
- # ADR-52 slice 4 — the rule's method-name set. A static Array is
977
- # returned as-is (`#include?` over Symbols); a run-time callable is
978
- # `instance_exec`'d against this plugin and memoised as a Symbol Set,
979
- # same lazy/idempotent contract as a callable `receivers:`. The
980
- # cache key is namespaced so a rule that makes both `methods:` and
981
- # `receivers:` callable keeps two distinct memo slots.
825
+ # ADR-52 slice 4 — the rule's method-name set. A static Array is returned as-is (`#include?` over
826
+ # Symbols); a run-time callable is `instance_exec`'d against this plugin and memoised as a Symbol Set,
827
+ # same lazy/idempotent contract as a callable `receivers:`. The cache key is namespaced so a rule that
828
+ # makes both `methods:` and `receivers:` callable keeps two distinct memo slots.
982
829
  def resolved_dynamic_return_methods(rule)
983
830
  methods = rule[:methods]
984
831
  return methods unless methods.respond_to?(:call)
@@ -987,15 +834,12 @@ module Rigor
987
834
  Array(instance_exec(&methods)).to_set(&:to_sym).freeze
988
835
  end
989
836
 
990
- # ADR-52 slice 5a — the rule's per-file method-name set. The
991
- # `file_methods:` callable is `instance_exec`'d with the file path
992
- # and memoised per `(rule, path)` one resolution per analysed
993
- # file, the per-file analogue of the run-wide `methods:` memo. A
994
- # nil path (synthetic call sites with no file context) resolves to
995
- # the empty set: the gate has nothing to key on, so the rule
996
- # declines — fail-closed, consistent with the gate's purpose. A
997
- # raising callable degrades to "declines this dispatch" via
998
- # `dynamic_return_type`'s surrounding rescue.
837
+ # ADR-52 slice 5a — the rule's per-file method-name set. The `file_methods:` callable is
838
+ # `instance_exec`'d with the file path and memoised per `(rule, path)` — one resolution per analysed
839
+ # file, the per-file analogue of the run-wide `methods:` memo. A nil path (synthetic call sites with no
840
+ # file context) resolves to the empty set: the gate has nothing to key on, so the rule declines
841
+ # fail-closed, consistent with the gate's purpose. A raising callable degrades to "declines this
842
+ # dispatch" via `dynamic_return_type`'s surrounding rescue.
999
843
  EMPTY_NAME_SET = Set.new.freeze
1000
844
  private_constant :EMPTY_NAME_SET
1001
845
 
@@ -1006,30 +850,26 @@ module Rigor
1006
850
  Array(instance_exec(path, &rule[:file_methods])).to_set(&:to_sym).freeze
1007
851
  end
1008
852
 
1009
- # ADR-52 slice 3 — the rule's receiver class-name Array. A static
1010
- # Array is returned as-is; a run-time callable is `instance_exec`'d
1011
- # against this plugin (so it reads the `#prepare`-built indexes) and
1012
- # memoised per rule for the run. Resolution is lazy first reached
1013
- # during file analysis, always after `#prepare` and the callable
1014
- # is required to be idempotent, so the memoised set is stable. A
1015
- # callable that raises degrades to "no receivers match" (the rule
1016
- # declines), never a crash, consistent with the surrounding rescue.
853
+ # ADR-52 slice 3 — the rule's receiver class-name Array. A static Array is returned as-is; a run-time
854
+ # callable is `instance_exec`'d against this plugin (so it reads the `#prepare`-built indexes) and
855
+ # memoised per rule for the run. Resolution is lazy — first reached during file analysis, always after
856
+ # `#prepare` and the callable is required to be idempotent, so the memoised set is stable. A callable
857
+ # that raises degrades to "no receivers match" (the rule declines), never a crash, consistent with the
858
+ # surrounding rescue.
1017
859
  def resolved_dynamic_return_receivers(rule)
1018
860
  receivers = rule[:receivers]
1019
861
  return receivers unless receivers.respond_to?(:call)
1020
862
 
1021
- # `||= {}` keeps the path correct even when a caller bypassed
1022
- # `initialize` (`allocate` in unit specs that inject a fake
1023
- # index); a self-freezing plugin already has the Hash from
1024
- # `initialize`, so the `||=` is a no-op there (never a FrozenError).
863
+ # `||= {}` keeps the path correct even when a caller bypassed `initialize` (`allocate` in unit specs
864
+ # that inject a fake index); a self-freezing plugin already has the Hash from `initialize`, so the
865
+ # `||=` is a no-op there (never a FrozenError).
1025
866
  (@dynamic_return_runtime_cache ||= {})[rule] ||=
1026
867
  Array(instance_exec(&receivers)).map { |c| c.to_s.dup.freeze }.freeze
1027
868
  end
1028
869
 
1029
- # True when `class_name` equals or inherits from `constraint`,
1030
- # matched through `Environment#class_ordering` (the mechanism
1031
- # `MacroBlockSelfType` / `additional_initializers` use). Degrades to
1032
- # "no match" on any resolution failure (false-positive-safe).
870
+ # True when `class_name` equals or inherits from `constraint`, matched through
871
+ # `Environment#class_ordering` (the mechanism `MacroBlockSelfType` / `additional_initializers` use).
872
+ # Degrades to "no match" on any resolution failure (false-positive-safe).
1033
873
  def class_matches_receiver?(class_name, constraint, environment)
1034
874
  return true if class_name == constraint
1035
875
  return false if environment.nil?
@@ -1051,23 +891,16 @@ module Rigor
1051
891
 
1052
892
  public
1053
893
 
1054
- # ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
1055
- # template carrying this plugin's id, version, and a
1056
- # SHA-256 digest of its (canonicalised) config hash.
1057
- # Callers outside the plugin (e.g. `Environment.for_project`
1058
- # caching per-file synthesizer output) compose this entry
1059
- # into their own cache descriptor so a config change to
1060
- # the plugin (e.g. flipping `require_magic_comment:`)
894
+ # ADR-32 WD5 — the `Cache::Descriptor::PluginEntry` template carrying this plugin's id, version, and a
895
+ # SHA-256 digest of its (canonicalised) config hash. Callers outside the plugin (e.g.
896
+ # `Environment.for_project` caching per-file synthesizer output) compose this entry into their own
897
+ # cache descriptor so a config change to the plugin (e.g. flipping `require_magic_comment:`)
1061
898
  # invalidates the dependent cache.
1062
899
  def plugin_entry
1063
- # Built fresh on each call rather than memoised so a
1064
- # plugin subclass that freezes itself in `initialize`
1065
- # (e.g. `Rigor::Plugin::RbsInline` per ADR-32) doesn't
1066
- # trip a FrozenError on first read. The construction
1067
- # cost is a single `Data.define`-backed value-object
1068
- # build; the cache key derivation downstream is the
1069
- # expensive step, and it's already memoised inside
1070
- # `Cache::Store`.
900
+ # Built fresh on each call rather than memoised so a plugin subclass that freezes itself in
901
+ # `initialize` (e.g. `Rigor::Plugin::RbsInline` per ADR-32) doesn't trip a FrozenError on first read.
902
+ # The construction cost is a single `Data.define`-backed value-object build; the cache key derivation
903
+ # downstream is the expensive step, and it's already memoised inside `Cache::Store`.
1071
904
  Cache::Descriptor::PluginEntry.new(
1072
905
  id: manifest.id,
1073
906
  version: manifest.version,
@@ -1077,14 +910,10 @@ module Rigor
1077
910
 
1078
911
  private
1079
912
 
1080
- # ADR-60 WD3 — the cache KEY descriptor: the plugin's
1081
- # PluginEntry template composed with an optional
1082
- # plugin-author-supplied extension carrying IDENTITY inputs
1083
- # (gem-version pins, `ConfigEntry` rows, configuration-file
1084
- # digests). The IoBoundary read history deliberately does NOT
1085
- # enter the key — it is recorded post-compute into the
1086
- # dependency descriptor instead (see
1087
- # {#producer_dependency_descriptor}).
913
+ # ADR-60 WD3 — the cache KEY descriptor: the plugin's PluginEntry template composed with an optional
914
+ # plugin-author-supplied extension carrying IDENTITY inputs (gem-version pins, `ConfigEntry` rows,
915
+ # configuration-file digests). The IoBoundary read history deliberately does NOT enter the key — it is
916
+ # recorded post-compute into the dependency descriptor instead (see {#producer_dependency_descriptor}).
1088
917
  def compose_key_descriptor(extra)
1089
918
  auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
1090
919
  return auto_built if extra.nil?
@@ -1092,18 +921,13 @@ module Rigor
1092
921
  Cache::Descriptor.compose(auto_built, extra)
1093
922
  end
1094
923
 
1095
- # ADR-60 WD3 — the dependency descriptor stored beside the
1096
- # producer's value, built AFTER the block ran so every
1097
- # in-block `io_boundary` read is captured, plus the evaluated
1098
- # `watch:` glob rows.
924
+ # ADR-60 WD3 — the dependency descriptor stored beside the producer's value, built AFTER the block ran
925
+ # so every in-block `io_boundary` read is captured, plus the evaluated `watch:` glob rows.
1099
926
  #
1100
- # The boundary snapshot may carry `ConfigEntry` rows (URL
1101
- # fetches, see {IoBoundary#open_url}). `Descriptor#fresh?`
1102
- # refuses any non-file/glob slot, so including them makes the
1103
- # entry permanently stale the producer recomputes EVERY run.
1104
- # That is deliberate: it is sound (never stale) and
1105
- # URL-reading producers are rare; a remote document has no
1106
- # cheap local re-validation anyway.
927
+ # The boundary snapshot may carry `ConfigEntry` rows (URL fetches, see {IoBoundary#open_url}).
928
+ # `Descriptor#fresh?` refuses any non-file/glob slot, so including them makes the entry permanently
929
+ # stale the producer recomputes EVERY run. That is deliberate: it is sound (never stale) and
930
+ # URL-reading producers are rare; a remote document has no cheap local re-validation anyway.
1107
931
  def producer_dependency_descriptor(producer)
1108
932
  boundary = io_boundary.cache_descriptor
1109
933
  Cache::Descriptor.new(
@@ -1113,15 +937,11 @@ module Rigor
1113
937
  )
1114
938
  end
1115
939
 
1116
- # ADR-60 WD3 — evaluates a producer's `watch:` declaration
1117
- # into {Cache::Descriptor::GlobEntry} rows. A Proc is
1118
- # `instance_exec`'d on this plugin instance (so `#init`-built
1119
- # search roots are in scope); the result like the static
1120
- # form is an Array of `[roots, pattern, ...]` tuples, one
1121
- # GlobEntry per (root, pattern) pair. Roots are expanded to
1122
- # absolute paths (matching {#glob_descriptor}) so freshness
1123
- # re-validation does not depend on the validating process's
1124
- # working directory.
940
+ # ADR-60 WD3 — evaluates a producer's `watch:` declaration into {Cache::Descriptor::GlobEntry} rows. A
941
+ # Proc is `instance_exec`'d on this plugin instance (so `#init`-built search roots are in scope); the
942
+ # result — like the static form — is an Array of `[roots, pattern, ...]` tuples, one GlobEntry per
943
+ # (root, pattern) pair. Roots are expanded to absolute paths (matching {#glob_descriptor}) so freshness
944
+ # re-validation does not depend on the validating process's working directory.
1125
945
  def watch_glob_entries(watch)
1126
946
  return [] if watch.nil?
1127
947
 
@@ -1135,14 +955,11 @@ module Rigor
1135
955
  end.uniq
1136
956
  end
1137
957
 
1138
- # ADR-60 WD3 — `fetch_or_validate` stores a
1139
- # `[value, dependency_descriptor]` pair, but the producer's
1140
- # declared `serialize:`/`deserialize:` contract covers the
1141
- # VALUE alone. These wrappers apply the custom callable to the
1142
- # value half and Marshal the descriptor half, so a producer
1143
- # with a non-Marshal-clean value keeps working unchanged. A
1144
- # nil callable returns nil — the store's default whole-pair
1145
- # Marshal round-trip applies.
958
+ # ADR-60 WD3 — `fetch_or_validate` stores a `[value, dependency_descriptor]` pair, but the producer's
959
+ # declared `serialize:`/`deserialize:` contract covers the VALUE alone. These wrappers apply the custom
960
+ # callable to the value half and Marshal the descriptor half, so a producer with a non-Marshal-clean
961
+ # value keeps working unchanged. A nil callable returns nil — the store's default whole-pair Marshal
962
+ # round-trip applies.
1146
963
  def pair_serializer(serialize)
1147
964
  return nil if serialize.nil?
1148
965