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
@@ -48,50 +48,32 @@ module Rigor
48
48
  :analyzed_files, :unresolved_self_calls
49
49
 
50
50
  # @param configuration [Rigor::Configuration]
51
- # @param explain [Boolean] surface fail-soft fallback events
52
- # as `:info` diagnostics.
53
- # @param cache_store [Rigor::Cache::Store, nil] the persistent
54
- # cache the runner exposes to producers (`RbsConstantTable`
55
- # and successors). Pass `nil` to disable caching for this
56
- # run; the CLI's `--no-cache` flag wires `nil` through.
57
- # v0.0.9 group A slice 1 introduces the surface; later
51
+ # @param explain [Boolean] surface fail-soft fallback events as `:info` diagnostics.
52
+ # @param cache_store [Rigor::Cache::Store, nil] the persistent cache the runner exposes to producers
53
+ # (`RbsConstantTable` and successors). Pass `nil` to disable caching for this run; the CLI's
54
+ # `--no-cache` flag wires `nil` through. v0.0.9 group A slice 1 introduces the surface; later
58
55
  # slices route real producers through it.
59
- # @param workers [Integer] ADR-15 Phase 4b — when greater
60
- # than zero, per-file analysis dispatches across a pool of
61
- # N workers. Default `0` keeps the sequential code path
62
- # bit-for-bit unchanged. Controlled via the
63
- # `RIGOR_RACTOR_WORKERS` env var or `.rigor.yml`
64
- # `parallel.workers:` (Phase 4c, fully wired).
65
- # @param collect_stats [Boolean] when true (default), `#run`
66
- # builds a {RunStats} summary exposed via `result.stats`
67
- # this forces the RBS env build at end-of-run so the
68
- # `class_decl_paths` snapshot has real source attribution.
69
- # Set to false to skip the stats summary entirely; the
70
- # CLI's `--no-stats` threads `false` through to keep
71
- # trivial-fixture runs from warming `.rigor/cache`.
72
- # @param prebuilt [Rigor::Analysis::ProjectScan, nil] when
73
- # supplied, the runner adopts the pre-built plugin
74
- # registry / dependency-source index / scanner outputs
75
- # from the snapshot and skips the per-call pre-passes
76
- # that produce them. Used by long-lived integrations
77
- # (`Rigor::LanguageServer::ProjectContext`) to keep
78
- # per-buffer requests fast scanners walk the project
79
- # once per generation rather than once per request, and
80
- # plugin `#prepare` runs once per generation rather than
81
- # once per request. Watched-file invalidation is the
82
- # owner's responsibility; the runner trusts the snapshot
83
- # it was given.
84
- # @param environment [Rigor::Environment, nil] opt-in
85
- # Environment override. When supplied, sequential mode uses
86
- # the provided env instance in `#analyze_files` instead of
87
- # building a fresh one via `Environment.for_project`, and
88
- # attaches the runner's per-run reporter pair onto the
89
- # env's mutable `Reporters` slot via
90
- # `Environment#attach_reporters!`. Long-lived consumers
91
- # (LSP `ProjectContext`) pass a shared env so per-publish
92
- # work doesn't repeat the `Environment.for_project` build
93
- # (bundler / lockfile / collection discovery, RbsLoader
94
- # construction). Pool mode ignores the override — each
56
+ # @param workers [Integer] ADR-15 Phase 4b — when greater than zero, per-file analysis dispatches
57
+ # across a pool of N workers. Default `0` keeps the sequential code path bit-for-bit unchanged.
58
+ # Controlled via the `RIGOR_RACTOR_WORKERS` env var or `.rigor.yml` `parallel.workers:` (Phase 4c,
59
+ # fully wired).
60
+ # @param collect_stats [Boolean] when true (default), `#run` builds a {RunStats} summary exposed via
61
+ # `result.stats` this forces the RBS env build at end-of-run so the `class_decl_paths` snapshot
62
+ # has real source attribution. Set to false to skip the stats summary entirely; the CLI's
63
+ # `--no-stats` threads `false` through to keep trivial-fixture runs from warming `.rigor/cache`.
64
+ # @param prebuilt [Rigor::Analysis::ProjectScan, nil] when supplied, the runner adopts the pre-built
65
+ # plugin registry / dependency-source index / scanner outputs from the snapshot and skips the
66
+ # per-call pre-passes that produce them. Used by long-lived integrations
67
+ # (`Rigor::LanguageServer::ProjectContext`) to keep per-buffer requests fast scanners walk the
68
+ # project once per generation rather than once per request, and plugin `#prepare` runs once per
69
+ # generation rather than once per request. Watched-file invalidation is the owner's responsibility;
70
+ # the runner trusts the snapshot it was given.
71
+ # @param environment [Rigor::Environment, nil] opt-in Environment override. When supplied, sequential
72
+ # mode uses the provided env instance in `#analyze_files` instead of building a fresh one via
73
+ # `Environment.for_project`, and attaches the runner's per-run reporter pair onto the env's mutable
74
+ # `Reporters` slot via `Environment#attach_reporters!`. Long-lived consumers (LSP `ProjectContext`)
75
+ # pass a shared env so per-publish work doesn't repeat the `Environment.for_project` build (bundler
76
+ # / lockfile / collection discovery, RbsLoader construction). Pool mode ignores the override — each
95
77
  # worker continues to build its own Environment.
96
78
  def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength
97
79
  cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
@@ -107,33 +89,28 @@ module Rigor
107
89
  @buffer = buffer
108
90
  @prebuilt = prebuilt
109
91
  @environment_override = environment
110
- # ADR-46 slice 1 — opt-in cross-file dependency recording. Off by
111
- # default; when true, `analyze_file` records each file's
112
- # cross-file reads into `file_dependencies` (the incremental
92
+ # ADR-46 slice 1 — opt-in cross-file dependency recording. Off by default; when true,
93
+ # `analyze_file` records each file's cross-file reads into `file_dependencies` (the incremental
113
94
  # cache, a later slice, consumes them).
114
95
  @record_dependencies = record_dependencies
115
- # ADR-24 slice 4a — opt-in unresolved-implicit-self-call recording.
116
- # Off by default; when true, `analyze_file` activates the engine
117
- # choke-point recorder and collects each file's misses into
118
- # `unresolved_self_calls` (a later closed-class-gated rule consumes
119
- # them). Purely observational — diagnostics are byte-identical.
96
+ # ADR-24 slice 4a — opt-in unresolved-implicit-self-call recording. Off by default; when true,
97
+ # `analyze_file` activates the engine choke-point recorder and collects each file's misses into
98
+ # `unresolved_self_calls` (a later closed-class-gated rule consumes them). Purely observational
99
+ # diagnostics are byte-identical.
120
100
  @record_self_calls = record_self_calls
121
101
  @unresolved_self_calls = {}
122
- # Memoised activation decision for the `call.self-undefined-method`
123
- # rule (nil = not yet computed). See `self_undefined_rule_active?`.
102
+ # Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
103
+ # See `self_undefined_rule_active?`.
124
104
  @self_undefined_rule_active = nil
125
105
  @analyzed_files = [].freeze
126
- # In-memory source map for `#run_source` — `{ logical_path => source
127
- # String }`. When set, `parse_source` reads bytes from here instead
128
- # of disk and `expand_paths` accepts the (possibly non-existent)
129
- # logical path. nil on a normal disk-backed run.
106
+ # In-memory source map for `#run_source` — `{ logical_path => source String }`. When set,
107
+ # `parse_source` reads bytes from here instead of disk and `expand_paths` accepts the (possibly
108
+ # non-existent) logical path. nil on a normal disk-backed run.
130
109
  @in_memory_sources = nil
131
- # ADR-46 slice 2 — the subset-analysis hook. When set (a collection
132
- # of paths), the whole-project pre-pass still runs over every file
133
- # (so the cross-file index is complete), but only files in this set
134
- # are analyzed for diagnostics the body tier re-analyses the
135
- # affected closure and serves the rest from the per-file cache.
136
- # `nil` (the default) analyzes everything.
110
+ # ADR-46 slice 2 — the subset-analysis hook. When set (a collection of paths), the whole-project
111
+ # pre-pass still runs over every file (so the cross-file index is complete), but only files in this
112
+ # set are analyzed for diagnostics — the body tier re-analyses the affected closure and serves the
113
+ # rest from the per-file cache. `nil` (the default) analyzes everything.
137
114
  @analyze_only = analyze_only && Set.new(analyze_only)
138
115
  @file_dependencies = {}
139
116
  @plugin_registry = Plugin::Registry::EMPTY
@@ -141,14 +118,11 @@ module Rigor
141
118
  @rbs_extended_reporter = RbsExtended::Reporter.new
142
119
  @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
143
120
  @source_rbs_synthesis_reporter = Plugin::SourceRbsSynthesisReporter.new
144
- # `#run` resets these for each invocation; pre-seed them to
145
- # empty containers so `build_run_stats` / `pre_file_diagnostics`
146
- # (private, called only from `#run`) can read them without
147
- # nil-guards. The four end-of-pass snapshots (RBS class /
148
- # signature-path tables, synthesized-namespace names,
149
- # `rigor:v1:conforms-to` results) live in one shared mutable
150
- # {RunSnapshots} sink so the analysis path that writes them and
151
- # the run / aggregator code that reads them stay in separate
121
+ # `#run` resets these for each invocation; pre-seed them to empty containers so `build_run_stats` /
122
+ # `pre_file_diagnostics` (private, called only from `#run`) can read them without nil-guards. The
123
+ # four end-of-pass snapshots (RBS class / signature-path tables, synthesized-namespace names,
124
+ # `rigor:v1:conforms-to` results) live in one shared mutable {RunSnapshots} sink so the analysis
125
+ # path that writes them and the run / aggregator code that reads them stay in separate
152
126
  # collaborators without a back-reference cycle.
153
127
  @snapshots = RunSnapshots.new
154
128
  @cached_plugin_prepare_diagnostics = [].freeze
@@ -166,20 +140,15 @@ module Rigor
166
140
  build_collaborators
167
141
  end
168
142
 
169
- # ADR-pending editor mode — present when the runner is wired
170
- # for the `--tmp-file` / `--instead-of` buffer-binding shape
171
- # (`docs/design/20260516-editor-mode.md`). Nil for normal
172
- # project runs.
143
+ # ADR-pending editor mode — present when the runner is wired for the `--tmp-file` / `--instead-of`
144
+ # buffer-binding shape (`docs/design/20260516-editor-mode.md`). Nil for normal project runs.
173
145
  attr_reader :buffer
174
146
 
175
- # Walks every Ruby file under `paths`, parses it, builds a
176
- # per-node scope index through
177
- # `Rigor::Inference::ScopeIndexer`, and runs the
178
- # `Rigor::Analysis::CheckRules` catalogue over it. Returns
179
- # a `Rigor::Analysis::Result` aggregating every produced
180
- # diagnostic plus any Prism parse errors. The Environment
181
- # is built once at run start through `Environment.for_project`
182
- # so all files share the same RBS load.
147
+ # Walks every Ruby file under `paths`, parses it, builds a per-node scope index through
148
+ # `Rigor::Inference::ScopeIndexer`, and runs the `Rigor::Analysis::CheckRules` catalogue over it.
149
+ # Returns a `Rigor::Analysis::Result` aggregating every produced diagnostic plus any Prism parse
150
+ # errors. The Environment is built once at run start through `Environment.for_project` so all files
151
+ # share the same RBS load.
183
152
  def run(paths = @configuration.paths)
184
153
  Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
185
154
  @configuration.fold_platform_specific_paths
@@ -206,15 +175,12 @@ module Rigor
206
175
  )
207
176
  end
208
177
 
209
- # Analyze a single source String in memory, without writing it to
210
- # disk — a clean entry point for embedders (LSP / editor mode) and a
211
- # faster spec path than the per-call tmpdir + chdir. The source is
212
- # bound to `path` (purely a logical identity carried in diagnostic
213
- # locations; it need not exist on disk). The full run machinery still
214
- # runs environment build, plugin `prepare`, severity profile so
215
- # the result matches a one-file disk run; only the cross-file project
216
- # pre-pass is empty (there is one file, and the per-file indexer
217
- # self-discovers its own classes / defs).
178
+ # Analyze a single source String in memory, without writing it to disk — a clean entry point for
179
+ # embedders (LSP / editor mode) and a faster spec path than the per-call tmpdir + chdir. The source
180
+ # is bound to `path` (purely a logical identity carried in diagnostic locations; it need not exist on
181
+ # disk). The full run machinery still runs environment build, plugin `prepare`, severity profile —
182
+ # so the result matches a one-file disk run; only the cross-file project pre-pass is empty (there is
183
+ # one file, and the per-file indexer self-discovers its own classes / defs).
218
184
  #
219
185
  # @param source [String] Ruby source to analyze.
220
186
  # @param path [String] logical path for diagnostic locations.
@@ -226,37 +192,31 @@ module Rigor
226
192
  @in_memory_sources = nil
227
193
  end
228
194
 
229
- # ADR-46 — the project file set that a run over `paths` would
230
- # analyze, computed by globbing only (no RBS environment build), so
231
- # the incremental fingerprint can be derived cheaply on the warm path
195
+ # ADR-46 — the project file set that a run over `paths` would analyze, computed by globbing only (no
196
+ # RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path
232
197
  # before deciding whether to build the env at all.
233
198
  def analysis_file_set(paths = @configuration.paths)
234
199
  expand_paths(paths).fetch(:files)
235
200
  end
236
201
 
237
- # ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the
238
- # incremental step walks: `dependents[X] = { A : A read a
239
- # declaration / body from X }`. On an edit to X, the body tier
240
- # (slice 2) re-analyses `{X} dependents[X]` and serves every other
241
- # file from the per-file cache. Built on demand from the recorded
242
- # `sources` sets (so it reflects whatever `analyze_file` captured —
243
- # empty unless the runner was constructed with
244
- # `record_dependencies: true`). The negative (`missing`) edges are
245
- # NOT inverted here: they feed the structural tier (slice 3), which
246
- # re-checks a consumer when a name it looked up and did not resolve
247
- # later appears.
202
+ # ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the incremental step walks:
203
+ # `dependents[X] = { A : A read a declaration / body from X }`. On an edit to X, the body tier
204
+ # (slice 2) re-analyses `{X} ∪ dependents[X]` and serves every other file from the per-file cache.
205
+ # Built on demand from the recorded `sources` sets (so it reflects whatever `analyze_file` captured —
206
+ # empty unless the runner was constructed with `record_dependencies: true`). The negative (`missing`)
207
+ # edges are NOT inverted here: they feed the structural tier (slice 3), which re-checks a consumer
208
+ # when a name it looked up and did not resolve later appears.
248
209
  def file_dependents
249
210
  Incremental.invert(@file_dependencies.transform_values(&:sources))
250
211
  end
251
212
 
252
- # ADR-46 slice 4 — per-symbol body fingerprints, computed from the
253
- # project pre-pass def index. Returns a frozen hash of the form:
213
+ # ADR-46 slice 4 — per-symbol body fingerprints, computed from the project pre-pass def index. Returns
214
+ # a frozen hash of the form:
254
215
  # { "path/to/file.rb" => { "ClassName#method" => sha256_hex, … }, … }
255
- # Used by {Analysis::IncrementalSession} to detect which symbols in a
256
- # changed file actually changed bodies, so only callers of those
257
- # specific symbols are re-checked. Only meaningful after a run that
258
- # populated `@project_discovered_def_nodes` (i.e. any full or subset
259
- # analysis); returns an empty frozen hash before the first run.
216
+ # Used by {Analysis::IncrementalSession} to detect which symbols in a changed file actually changed
217
+ # bodies, so only callers of those specific symbols are re-checked. Only meaningful after a run that
218
+ # populated `@project_discovered_def_nodes` (i.e. any full or subset analysis); returns an empty
219
+ # frozen hash before the first run.
260
220
  def symbol_fingerprints
261
221
  result = Hash.new { |h, k| h[k] = {} }
262
222
  @project_discovered_def_sources.each do |class_name, methods|
@@ -272,11 +232,10 @@ module Rigor
272
232
  result.transform_values(&:freeze).freeze
273
233
  end
274
234
 
275
- # ADR-46 slice 3 — per-file set of the qualified class/module names
276
- # declared in that file. Used to detect a class that *appeared* in an
277
- # edit so a subclass whose ancestor was previously undefined (and so
278
- # recorded a negative class edge) is re-checked. Inverts the project
279
- # class-source attribution (class → declaring files).
235
+ # ADR-46 slice 3 — per-file set of the qualified class/module names declared in that file. Used to
236
+ # detect a class that *appeared* in an edit so a subclass whose ancestor was previously undefined
237
+ # (and so recorded a negative class edge) is re-checked. Inverts the project class-source attribution
238
+ # (class declaring files).
280
239
  def class_declarations
281
240
  result = Hash.new { |hash, key| hash[key] = Set.new }
282
241
  @project_discovered_class_sources.each do |class_name, files|
@@ -285,14 +244,12 @@ module Rigor
285
244
  result.transform_values(&:freeze).freeze
286
245
  end
287
246
 
288
- # ADR-45 — unchanged-project fast path. Serves the whole run's
289
- # (pre-severity-profile) diagnostics from one record-and-validate
290
- # cache entry when every file the previous run read is unchanged,
291
- # skipping the dominant per-file inference. The dependency set is
292
- # collected AFTER the run (so it captures files the plugins read
293
- # mid-analysis, e.g. a Pundit policy) and re-validated on the next
294
- # run; the entry is keyed on the inputs known up front (config, gem
295
- # / engine versions, analyzed-path set).
247
+ # ADR-45 — unchanged-project fast path. Serves the whole run's (pre-severity-profile) diagnostics
248
+ # from one record-and-validate cache entry when every file the previous run read is unchanged,
249
+ # skipping the dominant per-file inference. The dependency set is collected AFTER the run (so it
250
+ # captures files the plugins read mid-analysis, e.g. a Pundit policy) and re-validated on the next
251
+ # run; the entry is keyed on the inputs known up front (config, gem / engine versions, analyzed-path
252
+ # set).
296
253
  def compute_run_diagnostics(expansion)
297
254
  @run_served_from_cache = false
298
255
  return assemble_run_diagnostics(expansion) unless run_result_cacheable?
@@ -313,19 +270,17 @@ module Rigor
313
270
  @run_served_from_cache = !computed
314
271
  diagnostics
315
272
  rescue StandardError
316
- # The result cache must never break a run. If anything in the
317
- # cache path fails, fall back to a direct, uncached analysis.
273
+ # The result cache must never break a run. If anything in the cache path fails, fall back to a
274
+ # direct, uncached analysis.
318
275
  @run_served_from_cache = false
319
276
  assemble_run_diagnostics(expansion)
320
277
  end
321
278
 
322
279
  def assemble_run_diagnostics(expansion, environment: nil)
323
280
  diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
324
- # ADR-46 — record which project files this run actually analyzed
325
- # (the `analyze_only` subset, or all of them). The incremental
326
- # orchestrator serves every analyzed-but-not-affected file from the
327
- # per-file cache, so it needs the full analyzed set to subtract the
328
- # affected closure from.
281
+ # ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
282
+ # all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
283
+ # per-file cache, so it needs the full analyzed set to subtract the affected closure from.
329
284
  targets = target_files(expansion)
330
285
  @analyzed_files = targets
331
286
  diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
@@ -336,9 +291,8 @@ module Rigor
336
291
  diagnostics + @diagnostic_aggregator.source_rbs_synthesis_diagnostics
337
292
  end
338
293
 
339
- # A cache hit skipped the analysis, so the per-run stats (wall
340
- # split, RBS-class counts, …) were never gathered report none
341
- # rather than the stale snapshot defaults.
294
+ # A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never
295
+ # gathered report none rather than the stale snapshot defaults.
342
296
  def stats_for_run(wall_started_at:, expansion:)
343
297
  return nil unless @collect_stats
344
298
  return nil if @run_served_from_cache
@@ -346,20 +300,18 @@ module Rigor
346
300
  build_run_stats(wall_started_at: wall_started_at, expansion: expansion)
347
301
  end
348
302
 
349
- # Cacheable only for a full sequential project run with a writable
350
- # cache and no per-buffer / prebuilt override — every other mode has
351
- # a different result identity (pool workers read in separate
352
- # processes; editor mode is per-buffer; prebuilt is the LSP path).
303
+ # Cacheable only for a full sequential project run with a writable cache and no per-buffer /
304
+ # prebuilt override — every other mode has a different result identity (pool workers read in
305
+ # separate processes; editor mode is per-buffer; prebuilt is the LSP path).
353
306
  def run_result_cacheable?
354
307
  !@cache_store.nil? && !@cache_store.read_only? &&
355
308
  @buffer.nil? && @prebuilt.nil? && !pool_mode?
356
309
  end
357
310
 
358
- # Stable cache key inputs — known before the run: a digest of the
359
- # resolved configuration, the engine + rbs versions + `--explain`,
360
- # and the analyzed-path SET (adding/removing a file changes the
361
- # key; editing one is caught by dependency validation). nil disables
362
- # the cache for this run rather than risking a malformed key.
311
+ # Stable cache key inputs — known before the run: a digest of the resolved configuration, the engine
312
+ # + rbs versions + `--explain`, and the analyzed-path SET (adding/removing a file changes the key;
313
+ # editing one is caught by dependency validation). nil disables the cache for this run rather than
314
+ # risking a malformed key.
363
315
  def run_key_descriptor(expansion, rbs_descriptor)
364
316
  Cache::Descriptor.new(
365
317
  gems: rbs_descriptor.gems,
@@ -373,18 +325,15 @@ module Rigor
373
325
  nil
374
326
  end
375
327
 
376
- # Files the run actually depended on, collected AFTER it ran:
377
- # every analyzed file, every RBS `sig` file (`rbs_descriptor.files`),
378
- # and every file each plugin read (complete post-run, so reads made
379
- # mid-analysis are included). Re-digested on the next run by
380
- # {Descriptor#fresh?}.
328
+ # Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS `sig`
329
+ # file (`rbs_descriptor.files`), and every file each plugin read (complete post-run, so reads made
330
+ # mid-analysis are included). Re-digested on the next run by {Descriptor#fresh?}.
381
331
  def run_dependency_descriptor(expansion, rbs_descriptor)
382
332
  entries = analyzed_file_entries(expansion) + rbs_descriptor.files
383
333
  @plugin_registry.plugins.each do |plugin|
384
- # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=`
385
- # initializer: plugin instances are frozen after the run, and a
386
- # plugin that never built a boundary read no files through it, so
387
- # it contributes no dependencies.
334
+ # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
335
+ # are frozen after the run, and a plugin that never built a boundary read no files through it,
336
+ # so it contributes no dependencies.
388
337
  boundary = plugin.instance_variable_get(:@io_boundary)
389
338
  entries.concat(boundary.cache_descriptor.files) if boundary
390
339
  end
@@ -450,14 +399,11 @@ module Rigor
450
399
  apply_pre_passes_result(@pre_passes.adopt_prebuilt(scan))
451
400
  end
452
401
 
453
- # Internal: copies a {ProjectPrePasses::Result} bundle onto the
454
- # runner's ivars in the assignment order the original inline
455
- # pre-pass body used, so every downstream reader (per-file
456
- # analysis seed, pool environment build, diagnostic aggregator)
457
- # sees the same ivar surface. The prebuilt path leaves the
458
- # discovery tables at their frozen-empty constructor defaults
459
- # (the bundle carries `nil` for them, matching the original
460
- # adopt path that never touched them).
402
+ # Internal: copies a {ProjectPrePasses::Result} bundle onto the runner's ivars in the assignment
403
+ # order the original inline pre-pass body used, so every downstream reader (per-file analysis seed,
404
+ # pool environment build, diagnostic aggregator) sees the same ivar surface. The prebuilt path leaves
405
+ # the discovery tables at their frozen-empty constructor defaults (the bundle carries `nil` for them,
406
+ # matching the original adopt path that never touched them).
461
407
  def apply_pre_passes_result(result) # rubocop:disable Metrics/AbcSize
462
408
  @plugin_registry = result.plugin_registry
463
409
  @dependency_source_index = result.dependency_source_index
@@ -483,20 +429,17 @@ module Rigor
483
429
  end
484
430
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result
485
431
 
486
- # Ruby versions probed (ascending) to discover the lowest one this
487
- # Prism build accepts for `version:`. Prism exposes no version
488
- # list, so the floor is found empirically only when a
489
- # misconfigured `target_ruby` is rejected so the diagnostic can
490
- # name it instead of leaving the user to guess (the dogfood field
491
- # trial, 20260620-skill-driven-onboarding-dogfood.md, burned cycles
492
- # on exactly this).
432
+ # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
433
+ # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
434
+ # misconfigured `target_ruby` is rejected — so the diagnostic can name it instead of leaving the user
435
+ # to guess (the dogfood field trial, 20260620-skill-driven-onboarding-dogfood.md, burned cycles on
436
+ # exactly this).
493
437
  PRISM_VERSION_LADDER = %w[
494
438
  3.0.0 3.1.0 3.2.0 3.3.0 3.4.0 3.5.0 4.0.0 4.1.0 4.2.0
495
439
  ].freeze
496
440
 
497
- # @return [String, nil] the lowest `target_ruby` this Prism build
498
- # accepts, or nil if none of the ladder parses. Memoised per
499
- # process (the value is constant for a given Prism).
441
+ # @return [String, nil] the lowest `target_ruby` this Prism build accepts, or nil if none of the
442
+ # ladder parses. Memoised per process (the value is constant for a given Prism).
500
443
  def self.prism_supported_floor
501
444
  @prism_supported_floor ||= PRISM_VERSION_LADDER.find do |candidate|
502
445
  Prism.parse("nil", version: candidate)
@@ -506,14 +449,11 @@ module Rigor
506
449
  end
507
450
  end
508
451
 
509
- # `target_ruby` flows through to Prism's `version:` option.
510
- # Prism enforces the supported range and raises `ArgumentError`
511
- # for versions it does not recognise. Run a one-time smoke parse
512
- # here so a misconfigured target_ruby surfaces as a single
513
- # project-level diagnostic instead of crashing the whole run on
514
- # the first file — and name the supported floor + where to read
515
- # the right value, so the fix is obvious without a guess-and-retry
516
- # loop.
452
+ # `target_ruby` flows through to Prism's `version:` option. Prism enforces the supported range and
453
+ # raises `ArgumentError` for versions it does not recognise. Run a one-time smoke parse here so a
454
+ # misconfigured target_ruby surfaces as a single project-level diagnostic instead of crashing the
455
+ # whole run on the first file and name the supported floor + where to read the right value, so the
456
+ # fix is obvious without a guess-and-retry loop.
517
457
  def validate_target_ruby
518
458
  Prism.parse("nil", version: @configuration.target_ruby)
519
459
  nil
@@ -533,25 +473,19 @@ module Rigor
533
473
 
534
474
  private
535
475
 
536
- # Editor mode § "Scope choice — option A". Under
537
- # `buffer:` non-nil the per-file analysis emits diagnostics
538
- # ONLY for the buffer's logical path; the rest of `paths:`
539
- # is consumed by the project-wide pre-passes (synthetic
540
- # methods, project-patched methods, plugin facts) but
541
- # contributes no per-file diagnostics. This is the v1 cut
542
- # before a per-file diagnostic cache exists; option B (full
543
- # project + incremental cache) is queued.
476
+ # Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
477
+ # diagnostics ONLY for the buffer's logical path; the rest of `paths:` is consumed by the
478
+ # project-wide pre-passes (synthetic methods, project-patched methods, plugin facts) but contributes
479
+ # no per-file diagnostics. This is the v1 cut before a per-file diagnostic cache exists; option B
480
+ # (full project + incremental cache) is queued.
544
481
  #
545
- # The buffer's logical path is added to the file list even
546
- # if it's not under `paths:` per design § "Failure
547
- # envelope": "--instead-of=Y with Y not under any paths:
548
- # directory → treated as a valid logical identity for the
549
- # buffer".
482
+ # The buffer's logical path is added to the file list even if it's not under `paths:` — per design §
483
+ # "Failure envelope": "--instead-of=Y with Y not under any paths: directory treated as a valid
484
+ # logical identity for the buffer".
550
485
  def target_files(expansion)
551
486
  files = expansion.fetch(:files)
552
- # ADR-46 slice 2 — restrict the analyzed set to the affected
553
- # closure while the pre-pass (run separately over `expansion`'s
554
- # full file list) keeps the cross-file index complete. Buffer mode
487
+ # ADR-46 slice 2 — restrict the analyzed set to the affected closure while the pre-pass (run
488
+ # separately over `expansion`'s full file list) keeps the cross-file index complete. Buffer mode
555
489
  # takes precedence — its single logical path is the analyzed set.
556
490
  files = files.select { |path| @analyze_only.include?(path) } if @analyze_only
557
491
  return files if @buffer.nil?
@@ -559,11 +493,9 @@ module Rigor
559
493
  [@buffer.logical_path]
560
494
  end
561
495
 
562
- # Editor mode (`buffer:` non-nil) auto-flips the cache store
563
- # to `read_only: true` so multiple debounced editor invocations
564
- # against the same project don't churn the on-disk cache or
565
- # race on schema-version writes. Cache reads continue
566
- # unchanged; misses still run the producer block but the
496
+ # Editor mode (`buffer:` non-nil) auto-flips the cache store to `read_only: true` so multiple
497
+ # debounced editor invocations against the same project don't churn the on-disk cache or race on
498
+ # schema-version writes. Cache reads continue unchanged; misses still run the producer block but the
567
499
  # result is not persisted. Per design doc § "Cache behaviour".
568
500
  def enforce_read_only_cache(cache_store, buffer)
569
501
  return cache_store if buffer.nil?
@@ -573,14 +505,11 @@ module Rigor
573
505
  Cache::Store.new(root: cache_store.root, read_only: true)
574
506
  end
575
507
 
576
- # Wires the three responsibility collaborators. Called at the end
577
- # of construction (after every state ivar is seeded). The per-run
578
- # varying state (the plugin registry, dependency-source / scanner
579
- # indexes, prepare-diagnostic snapshot, and the four end-of-pass
580
- # snapshots) is reached through reader procs so each collaborator
581
- # observes the live ivar value at call time without a
582
- # back-reference cycle. The reporter accumulators and the
583
- # {RunSnapshots} sink are shared mutable instances.
508
+ # Wires the three responsibility collaborators. Called at the end of construction (after every state
509
+ # ivar is seeded). The per-run varying state (the plugin registry, dependency-source / scanner
510
+ # indexes, prepare-diagnostic snapshot, and the four end-of-pass snapshots) is reached through reader
511
+ # procs so each collaborator observes the live ivar value at call time without a back-reference
512
+ # cycle. The reporter accumulators and the {RunSnapshots} sink are shared mutable instances.
584
513
  def build_collaborators # rubocop:disable Metrics/MethodLength
585
514
  @pre_passes = ProjectPrePasses.new(
586
515
  configuration: @configuration, cache_store: @cache_store, buffer: @buffer,
@@ -616,26 +545,20 @@ module Rigor
616
545
  )
617
546
  end
618
547
 
619
- # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`.
620
- # Editor mode (`buffer:` non-nil) silently overrides pool
621
- # mode to sequential. The real decision lives on
622
- # {PoolCoordinator}; the predicate stays on the runner because
623
- # `run_result_cacheable?` consults it (and a spec exercises it
548
+ # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
549
+ # silently overrides pool mode to sequential. The real decision lives on {PoolCoordinator}; the
550
+ # predicate stays on the runner because `run_result_cacheable?` consults it (and a spec exercises it
624
551
  # via `send`).
625
552
  def pool_mode?
626
553
  @pool_coordinator.pool_mode?
627
554
  end
628
555
 
629
- # End-of-run telemetry. Walks the cached
630
- # `class_decl_paths` snapshot (sequential mode: from
631
- # the coordinator's environment; pool mode: from the
632
- # first worker's `:prepare` payload) and partitions the
633
- # RBS class universe into "project sig/" (paths under
634
- # `signature_paths`) vs "bundled" (everything else).
635
- # Gem source-walk counts come from `dependency_source_index`
636
- # which is already constructed regardless of pool mode.
637
- # Wall + RSS are single syscalls; total cost is bounded
638
- # by the snapshot size (~1000-2000 entries).
556
+ # End-of-run telemetry. Walks the cached `class_decl_paths` snapshot (sequential mode: from the
557
+ # coordinator's environment; pool mode: from the first worker's `:prepare` payload) and partitions
558
+ # the RBS class universe into "project sig/" (paths under `signature_paths`) vs "bundled" (everything
559
+ # else). Gem source-walk counts come from `dependency_source_index` which is already constructed
560
+ # regardless of pool mode. Wall + RSS are single syscalls; total cost is bounded by the snapshot size
561
+ # (~1000-2000 entries).
639
562
  def build_run_stats(wall_started_at:, expansion:)
640
563
  snapshot = @snapshots.class_decl_paths
641
564
  project_sig, bundled = RunStats.partition_classes(
@@ -655,21 +578,15 @@ module Rigor
655
578
  )
656
579
  end
657
580
 
658
- # ADR-7 § "Slice 5-A/5-B" — invokes every loaded plugin's
659
- # per-file diagnostic emission hook
660
- # (`Plugin::Base#diagnostics_for_file`) and re-stamps the
661
- # returned diagnostics with
662
- # `source_family: "plugin.<manifest.id>"` so plugin
663
- # authors cannot accidentally publish under another
664
- # plugin's identifier or under `:builtin`. Plugin
665
- # exceptions are isolated per ADR-2 § "Plugin Trust and
666
- # I/O Policy" — a raise from one plugin becomes a
667
- # `:plugin_loader` `runtime-error` diagnostic without
668
- # affecting other plugins or the rest of the run.
669
- # ADR-52 WD1 — only the plugins that overrode
670
- # `#diagnostics_for_file` or declared a `node_rule` are visited
671
- # (`contribution_index.for_file_diagnostics`); a skipped plugin's
672
- # two hooks could only have returned `[]`.
581
+ # ADR-7 § "Slice 5-A/5-B" — invokes every loaded plugin's per-file diagnostic emission hook
582
+ # (`Plugin::Base#diagnostics_for_file`) and re-stamps the returned diagnostics with
583
+ # `source_family: "plugin.<manifest.id>"` so plugin authors cannot accidentally publish under
584
+ # another plugin's identifier or under `:builtin`. Plugin exceptions are isolated per ADR-2 § "Plugin
585
+ # Trust and I/O Policy" — a raise from one plugin becomes a `:plugin_loader` `runtime-error`
586
+ # diagnostic without affecting other plugins or the rest of the run.
587
+ # ADR-52 WD1 — only the plugins that overrode `#diagnostics_for_file` or declared a `node_rule` are
588
+ # visited (`contribution_index.for_file_diagnostics`); a skipped plugin's two hooks could only have
589
+ # returned `[]`.
673
590
  def plugin_emitted_diagnostics(path, root, scope, node_results)
674
591
  return [] if @plugin_registry.empty?
675
592
 
@@ -678,18 +595,15 @@ module Rigor
678
595
  end
679
596
  end
680
597
 
681
- # ADR-52 WD4 + ADR-53 B4 — one engine-owned AST walk per file
682
- # dispatches each node to every matching (plugin, rule) AND drives
683
- # the built-in node collectors (`node_collectors`), so the file is
684
- # walked once for both. The per-plugin results are bucketed in
685
- # registry order so plugin emission stays plugin-major
686
- # (byte-identical with the old per-plugin walk); the collectors are
687
- # populated in place for `diagnose` to consume.
598
+ # ADR-52 WD4 + ADR-53 B4 — one engine-owned AST walk per file dispatches each node to every matching
599
+ # (plugin, rule) AND drives the built-in node collectors (`node_collectors`), so the file is walked
600
+ # once for both. The per-plugin results are bucketed in registry order so plugin emission stays
601
+ # plugin-major (byte-identical with the old per-plugin walk); the collectors are populated in place
602
+ # for `diagnose` to consume.
688
603
  #
689
- # When no plugin declares a node rule, the walk still runs to drive
690
- # the collectors (the converged path replaces the standalone
691
- # `RuleWalk.run`); `node_collectors` nil means a caller that does
692
- # not need built-in collection from this walk.
604
+ # When no plugin declares a node rule, the walk still runs to drive the collectors (the converged
605
+ # path replaces the standalone `RuleWalk.run`); `node_collectors` nil means a caller that does not
606
+ # need built-in collection from this walk.
693
607
  def node_rule_results_by_plugin(path, root, scope, node_collectors, scope_index)
694
608
  walk = @plugin_registry.node_rule_walk
695
609
  driver = node_collectors && CheckRules.node_collector_driver(node_collectors)
@@ -710,9 +624,8 @@ module Rigor
710
624
 
711
625
  def collect_plugin_diagnostics(plugin, path, root, scope, node_result)
712
626
  raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
713
- # A node-rule context/rule raise isolates the whole plugin's
714
- # node-rule contribution, matching the old combined per-plugin
715
- # rescue (which discarded `diagnostics_for_file` output too).
627
+ # A node-rule context/rule raise isolates the whole plugin's node-rule contribution, matching the
628
+ # old combined per-plugin rescue (which discarded `diagnostics_for_file` output too).
716
629
  raise node_result.error if node_result&.error
717
630
 
718
631
  raw += node_result.diagnostics if node_result
@@ -755,22 +668,20 @@ module Rigor
755
668
 
756
669
  # Resolves the user-supplied path list into:
757
670
  # - `:files` — the concrete `.rb` files to analyze.
758
- # - `:errors` — `Diagnostic` entries for each path that
759
- # does not exist or is not a recognisable Ruby source.
671
+ # - `:errors` — `Diagnostic` entries for each path that does not exist or is not a recognisable Ruby
672
+ # source.
760
673
  #
761
- # Surfacing path errors is a first-preview must-have:
762
- # `rigor check ./does_not_exist.rb` previously exited
763
- # cleanly with no output, which silently masked typos.
674
+ # Surfacing path errors is a first-preview must-have: `rigor check ./does_not_exist.rb` previously
675
+ # exited cleanly with no output, which silently masked typos.
764
676
  def expand_paths(paths)
765
677
  files = []
766
678
  bad = []
767
679
  Array(paths).each do |path|
768
680
  if File.directory?(path)
769
681
  files.concat(reject_excluded(Dir.glob(File.join(path, RUBY_GLOB))))
770
- # Editor-mode bypass: the buffer's logical path is treated
771
- # as a real `.rb` file regardless of on-disk presence
772
- # `parse_source` reads bytes from the buffer's physical
773
- # path. Common case: LSP client editing a brand-new file.
682
+ # Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
683
+ # on-disk presence `parse_source` reads bytes from the buffer's physical path. Common case: LSP
684
+ # client editing a brand-new file.
774
685
  elsif accept_as_ruby_file?(path)
775
686
  files << path
776
687
  elsif File.exist?(path)
@@ -782,13 +693,11 @@ module Rigor
782
693
  { files: files, errors: path_expansion_errors(bad, any_files: files.any?) }
783
694
  end
784
695
 
785
- # A bad path *among valid ones* is a warn-and-skip — the run still
786
- # does useful work, so `rigor check app lib` with no `lib/` (the
787
- # 20260620 field trial's strap case) analyses `app` instead of
788
- # aborting on exit 1. A bad path that leaves NOTHING to analyse
789
- # stays an error so a lone typo (`rigor check typo.rb`) is not
790
- # silently masked (the regression the path-error check was added
791
- # to catch in the first place).
696
+ # A bad path *among valid ones* is a warn-and-skip — the run still does useful work, so `rigor check
697
+ # app lib` with no `lib/` (the 20260620 field trial's strap case) analyses `app` instead of aborting
698
+ # on exit 1. A bad path that leaves NOTHING to analyse stays an error so a lone typo (`rigor check
699
+ # typo.rb`) is not silently masked (the regression the path-error check was added to catch in the
700
+ # first place).
792
701
  def path_expansion_errors(bad, any_files:)
793
702
  severity = any_files ? :warning : :error
794
703
  suffix = any_files ? " (skipped)" : ""
@@ -801,15 +710,12 @@ module Rigor
801
710
  @in_memory_sources&.key?(path)
802
711
  end
803
712
 
804
- # `Configuration#exclude_patterns` is a list of glob patterns
805
- # checked against each globbed path via `File.fnmatch?` (without
806
- # `FNM_PATHNAME`, so `**` and `*` both span path separators
807
- # the patterns behave like substring globs). Built-in defaults
808
- # exclude `vendor/bundle`, `.bundle`, `node_modules`, and `tmp`
809
- # so the analyser never walks into vendored deps or build
810
- # artefacts. User-supplied entries (`.rigor.yml` `exclude:`)
811
- # layer on top. Explicit file arguments to the CLI bypass this
812
- # filter — only the directory-glob expansion is filtered.
713
+ # `Configuration#exclude_patterns` is a list of glob patterns checked against each globbed path via
714
+ # `File.fnmatch?` (without `FNM_PATHNAME`, so `**` and `*` both span path separators the patterns
715
+ # behave like substring globs). Built-in defaults exclude `vendor/bundle`, `.bundle`, `node_modules`,
716
+ # and `tmp` so the analyser never walks into vendored deps or build artefacts. User-supplied entries
717
+ # (`.rigor.yml` `exclude:`) layer on top. Explicit file arguments to the CLI bypass this filter — only
718
+ # the directory-glob expansion is filtered.
813
719
  def reject_excluded(file_list)
814
720
  return file_list if @configuration.exclude_patterns.empty?
815
721
 
@@ -830,11 +736,9 @@ module Rigor
830
736
  )
831
737
  end
832
738
 
833
- # Reads + parses the source at `path`. Under editor mode
834
- # (`@buffer` set) reads bytes from `@buffer.physical_path`
835
- # when `path` matches the logical binding, then parses with
836
- # `filepath: path` so Prism's location data carries the
837
- # LOGICAL path. Non-binding paths go through the cheaper
739
+ # Reads + parses the source at `path`. Under editor mode (`@buffer` set) reads bytes from
740
+ # `@buffer.physical_path` when `path` matches the logical binding, then parses with `filepath: path`
741
+ # so Prism's location data carries the LOGICAL path. Non-binding paths go through the cheaper
838
742
  # `Prism.parse_file` codepath unchanged.
839
743
  def parse_source(path)
840
744
  if @in_memory_sources&.key?(path)
@@ -847,12 +751,9 @@ module Rigor
847
751
  Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
848
752
  end
849
753
 
850
- # Seeds the cross-file project pre-pass indexes onto a
851
- # fresh per-file scope: discovered classes, and the ADR-24
852
- # def-node / superclass / included-module maps. Each is
853
- # applied only when non-empty so a runner constructed
854
- # without the project pre-pass (e.g. a single-file probe)
855
- # keeps an empty seed.
754
+ # Seeds the cross-file project pre-pass indexes onto a fresh per-file scope: discovered classes, and
755
+ # the ADR-24 def-node / superclass / included-module maps. Each is applied only when non-empty so a
756
+ # runner constructed without the project pre-pass (e.g. a single-file probe) keeps an empty seed.
856
757
  def seed_project_scope(scope)
857
758
  tables = project_scope_seed_tables
858
759
  return scope if tables.empty?
@@ -860,9 +761,8 @@ module Rigor
860
761
  scope.with_discovery(scope.discovery.with(**tables))
861
762
  end
862
763
 
863
- # The cross-file pre-pass tables {#seed_project_scope} applies, as a
864
- # plain Hash so the fork-pool path can hand the same seed to its
865
- # {WorkerSession} (whose per-file scopes would otherwise miss every
764
+ # The cross-file pre-pass tables {#seed_project_scope} applies, as a plain Hash so the fork-pool path
765
+ # can hand the same seed to its {WorkerSession} (whose per-file scopes would otherwise miss every
866
766
  # cross-file def — ADR-15 sequential-equivalence contract).
867
767
  def project_scope_seed_tables
868
768
  tables = {}
@@ -881,18 +781,16 @@ module Rigor
881
781
  end
882
782
  tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
883
783
  seed_member_layout_tables(tables)
884
- # ADR-46 slice 1 — the class-declaration source map is read only by
885
- # the ancestry accessors during dependency recording, so seed it
886
- # only when recording is on; a normal run never carries it.
784
+ # ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
785
+ # dependency recording, so seed it only when recording is on; a normal run never carries it.
887
786
  if @record_dependencies && !@project_discovered_class_sources.empty?
888
787
  tables[:discovered_class_sources] = @project_discovered_class_sources
889
788
  end
890
789
  tables
891
790
  end
892
791
 
893
- # ADR-48 — seed the Data + Struct member-layout tables (each only when
894
- # non-empty). Extracted to keep {#project_scope_seed_tables} under the
895
- # complexity budget.
792
+ # ADR-48 — seed the Data + Struct member-layout tables (each only when non-empty). Extracted to keep
793
+ # {#project_scope_seed_tables} under the complexity budget.
896
794
  def seed_member_layout_tables(tables)
897
795
  tables[:data_member_layouts] = @project_data_member_layouts unless @project_data_member_layouts.empty?
898
796
  return if @project_struct_member_layouts.empty?
@@ -900,12 +798,11 @@ module Rigor
900
798
  tables[:struct_member_layouts] = @project_struct_member_layouts
901
799
  end
902
800
 
903
- # ADR-46 slice 1 — when dependency recording is enabled, wrap the
904
- # per-file analysis so the cross-file reads its inference makes are
905
- # captured into `file_dependencies[path]`. Off by default: a normal
906
- # run calls the body directly and the instrumented `Scope` accessors
907
- # short-circuit on `DependencyRecorder.active? == false`. Recording
908
- # is observational, so diagnostics are byte-identical either way.
801
+ # ADR-46 slice 1 — when dependency recording is enabled, wrap the per-file analysis so the cross-file
802
+ # reads its inference makes are captured into `file_dependencies[path]`. Off by default: a normal run
803
+ # calls the body directly and the instrumented `Scope` accessors short-circuit on
804
+ # `DependencyRecorder.active? == false`. Recording is observational, so diagnostics are byte-identical
805
+ # either way.
909
806
  def analyze_file(path, environment)
910
807
  return analyze_file_body(path, environment) unless @record_dependencies
911
808
 
@@ -926,10 +823,9 @@ module Rigor
926
823
  end
927
824
 
928
825
  scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
929
- # ADR-24 slice 4a/4 — record unresolved implicit-self calls during the
930
- # typing pass ONLY (not CheckRules, whose own `type_of` queries would
931
- # otherwise re-trigger the choke-point). `self_call_misses` feeds the
932
- # `call.self-undefined-method` collector; the recorder is inert unless
826
+ # ADR-24 slice 4a/4 — record unresolved implicit-self calls during the typing pass ONLY (not
827
+ # CheckRules, whose own `type_of` queries would otherwise re-trigger the choke-point).
828
+ # `self_call_misses` feeds the `call.self-undefined-method` collector; the recorder is inert unless
933
829
  # the rule is active or `record_self_calls:` opted in.
934
830
  index = nil
935
831
  self_call_record = with_self_call_recording(path) do
@@ -960,12 +856,10 @@ module Rigor
960
856
  ]
961
857
  end
962
858
 
963
- # ADR-53 B4 — the built-in node collectors and the plugin node rules
964
- # share ONE traversal of the file. The collectors are built here (they
965
- # need the completed `index`) and populated by the converged plugin
966
- # walk; `node_results` carries the per-plugin node-rule output. Both
967
- # the built-in `diagnose` output and the plugin diagnostics are then
968
- # built from that single walk's results.
859
+ # ADR-53 B4 — the built-in node collectors and the plugin node rules share ONE traversal of the file.
860
+ # The collectors are built here (they need the completed `index`) and populated by the converged
861
+ # plugin walk; `node_results` carries the per-plugin node-rule output. Both the built-in `diagnose`
862
+ # output and the plugin diagnostics are then built from that single walk's results.
969
863
  def rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
970
864
  root = parse_result.value
971
865
  node_collectors = CheckRules.build_node_collectors(path, index)
@@ -982,11 +876,10 @@ module Rigor
982
876
  diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
983
877
  end
984
878
 
985
- # ADR-24 slice 4a — runs `block` (the typing pass) with the self-call
986
- # recorder active when either the test-only `record_self_calls:` flag is
987
- # set or the `call.self-undefined-method` rule resolves to a firing
988
- # severity. Returns the frozen {SelfCallResolutionRecorder::Record}, or
989
- # nil when recording is inactive (the common path — one integer read).
879
+ # ADR-24 slice 4a — runs `block` (the typing pass) with the self-call recorder active when either
880
+ # the test-only `record_self_calls:` flag is set or the `call.self-undefined-method` rule resolves to
881
+ # a firing severity. Returns the frozen {SelfCallResolutionRecorder::Record}, or nil when recording is
882
+ # inactive (the common path — one integer read).
990
883
  def with_self_call_recording(path, &)
991
884
  unless self_call_recording_active?
992
885
  yield
@@ -1002,11 +895,9 @@ module Rigor
1002
895
  @record_self_calls || self_undefined_rule_active?
1003
896
  end
1004
897
 
1005
- # Memoised: the rule fires only when its resolved severity is not `:off`
1006
- # and it is not in `disable:`. Default profiles map it to `:off`, so a
1007
- # normal run never activates the recorder (pending the external WD4
1008
- # corpus FP gate — see ADR-24 § "Slice 4"); a project opts in via
1009
- # `severity_overrides:`.
898
+ # Memoised: the rule fires only when its resolved severity is not `:off` and it is not in `disable:`.
899
+ # Default profiles map it to `:off`, so a normal run never activates the recorder (pending the
900
+ # external WD4 corpus FP gate see ADR-24 § "Slice 4"); a project opts in via `severity_overrides:`.
1010
901
  def self_undefined_rule_active?
1011
902
  return @self_undefined_rule_active unless @self_undefined_rule_active.nil?
1012
903
 
@@ -1023,15 +914,11 @@ module Rigor
1023
914
  end
1024
915
  end
1025
916
 
1026
- # v0.0.2 #10 — fail-soft fallback explanation. When
1027
- # `--explain` is set the runner additionally walks the
1028
- # file with `Rigor::Inference::CoverageScanner` and emits
1029
- # one `:info` diagnostic per directly-unrecognized node,
1030
- # naming the node class and the type the engine fell back
1031
- # to. The CoverageScanner is the canonical "first-event-
1032
- # per-node" probe: it already filters out pass-through
1033
- # wrappers (`ProgramNode`, `StatementsNode`,
1034
- # `ParenthesesNode`) so the explain stream is attributable
917
+ # v0.0.2 #10 — fail-soft fallback explanation. When `--explain` is set the runner additionally walks
918
+ # the file with `Rigor::Inference::CoverageScanner` and emits one `:info` diagnostic per
919
+ # directly-unrecognized node, naming the node class and the type the engine fell back to. The
920
+ # CoverageScanner is the canonical "first-event-per-node" probe: it already filters out pass-through
921
+ # wrappers (`ProgramNode`, `StatementsNode`, `ParenthesesNode`) so the explain stream is attributable
1035
922
  # to the leaf node that actually triggered the fallback.
1036
923
  def explain_diagnostics(path, root, scope)
1037
924
  return [] unless @explain