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
@@ -11,51 +11,38 @@ require_relative "descriptor"
11
11
 
12
12
  module Rigor
13
13
  module Cache
14
- # Filesystem-backed cache store. Schema, layout, file format,
15
- # atomicity, and locking are fixed by [ADR-6](../../../docs/adr/6-cache-persistence-backend.md);
16
- # callers use `#fetch_or_compute` (producer-keyed),
17
- # `#fetch_or_validate` (record-and-validate for discovered-dep
18
- # caches, ADR-45), `#stats`, `#evict!`, and `.disk_inventory`,
19
- # plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
14
+ # Filesystem-backed cache store. Schema, layout, file format, atomicity, and locking are fixed by
15
+ # [ADR-6](../../../docs/adr/6-cache-persistence-backend.md); callers use `#fetch_or_compute`
16
+ # (producer-keyed), `#fetch_or_validate` (record-and-validate for discovered-dep caches, ADR-45), `#stats`,
17
+ # `#evict!`, and `.disk_inventory`, plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
20
18
  #
21
- # Read failures (missing file, bad magic, format-version mismatch,
22
- # corrupt SHA-256 trailer, un-inflatable or unmarshal-able payload)
23
- # are silently treated as cache misses; the producer block reruns
24
- # and the next write replaces the bad entry. The trailing SHA-256
25
- # catches accidental corruption (partial writes, FS errors); it is
26
- # **not** a security boundary, per ADR-2's trusted-gem trust model.
19
+ # Read failures (missing file, bad magic, format-version mismatch, corrupt SHA-256 trailer, un-inflatable
20
+ # or unmarshal-able payload) are silently treated as cache misses; the producer block reruns and the next
21
+ # write replaces the bad entry. The trailing SHA-256 catches accidental corruption (partial writes, FS
22
+ # errors); it is **not** a security boundary, per ADR-2's trusted-gem trust model.
27
23
  class Store # rubocop:disable Metrics/ClassLength
28
- # On-disk byte-layout version. Bumped on incompatible format
29
- # changes (independent of {Descriptor::SCHEMA_VERSION}, which
30
- # covers the descriptor schema rather than the byte layout).
31
- # v2 (ADR-54 WD2): the value payload is zlib-deflated on write
32
- # and inflated on read Marshal blobs compress to 13–16 % at
33
- # an inflate cost an order of magnitude below their
34
- # `Marshal.load`. v1 entries fail the header check and read as
35
- # silent misses; the `schema_version.txt` marker additionally
36
- # carries this version, so the first writable run after a bump
37
- # clears the root and reclaims the unreadable bytes.
24
+ # On-disk byte-layout version. Bumped on incompatible format changes (independent of
25
+ # {Descriptor::SCHEMA_VERSION}, which covers the descriptor schema rather than the byte layout). v2
26
+ # (ADR-54 WD2): the value payload is zlib-deflated on write and inflated on read — Marshal blobs
27
+ # compress to 13–16 % at an inflate cost an order of magnitude below their `Marshal.load`. v1 entries
28
+ # fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
29
+ # this version, so the first writable run after a bump clears the root and reclaims the unreadable
30
+ # bytes.
38
31
  FORMAT_VERSION = 2
39
32
 
40
- # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
41
- # format version.
33
+ # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
42
34
  HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
43
35
 
44
36
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
45
37
 
46
38
  # @param root [String] cache root directory.
47
- # @param read_only [Boolean] when true, every disk-side
48
- # side-effect is suppressed: `fetch_or_compute` still
49
- # reads existing entries (hits) and still runs the
50
- # producer block on miss, but it does NOT write the
51
- # produced value to disk, does NOT update the
52
- # `schema_version.txt` marker, and does NOT touch the
53
- # on-disk root directory. The in-process memo is still
54
- # populated so repeated lookups within the same run stay
55
- # cheap. Used by editor mode so multiple buffer-mode
56
- # invocations can read from the same cache concurrently
57
- # without churning it. See
58
- # `docs/design/20260516-editor-mode.md` § "Cache behaviour".
39
+ # @param read_only [Boolean] when true, every disk-side side-effect is suppressed: `fetch_or_compute`
40
+ # still reads existing entries (hits) and still runs the producer block on miss, but it does NOT
41
+ # write the produced value to disk, does NOT update the `schema_version.txt` marker, and does NOT
42
+ # touch the on-disk root directory. The in-process memo is still populated so repeated lookups within
43
+ # the same run stay cheap. Used by editor mode so multiple buffer-mode invocations can read from the
44
+ # same cache concurrently without churning it. See `docs/design/20260516-editor-mode.md` § "Cache
45
+ # behaviour".
59
46
  def initialize(root:, read_only: false, max_bytes: nil)
60
47
  @root = root.to_s.dup.freeze
61
48
  @read_only = read_only
@@ -65,40 +52,30 @@ module Rigor
65
52
  @misses = 0
66
53
  @writes = 0
67
54
  @by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
68
- # Process-level in-memory layer keyed by
69
- # `(producer_id, cache_key)`. Avoids the disk read +
70
- # `Marshal.load` cost (the dominant share of repeated
71
- # cache-hit calls per stackprof) when many short-lived
72
- # `Analysis::Runner` instances share one `Store` the
73
- # spec process, the LSP daemon's repeated re-check
74
- # path, and any other "many runs, same project" loop.
75
- # Keys are content-derived (descriptor digests), so
76
- # cross-fixture contamination is impossible.
55
+ # Process-level in-memory layer keyed by `(producer_id, cache_key)`. Avoids the disk read +
56
+ # `Marshal.load` cost (the dominant share of repeated cache-hit calls per stackprof) when many
57
+ # short-lived `Analysis::Runner` instances share one `Store` — the spec process, the LSP daemon's
58
+ # repeated re-check path, and any other "many runs, same project" loop. Keys are content-derived
59
+ # (descriptor digests), so cross-fixture contamination is impossible.
77
60
  @memo = {}
78
- # `Analysis::Runner` walks files concurrently (file-
79
- # level parallelism); the per-file workers share one
80
- # Store. The monitor guards `@memo` + the counter
81
- # hashes against concurrent writes. The Monitor is
82
- # re-entrant so producer blocks can recursively
83
- # consult the Store (e.g. one cache layer building on
84
- # another) without dead-locking.
61
+ # `Analysis::Runner` walks files concurrently (file-level parallelism); the per-file workers share
62
+ # one Store. The monitor guards `@memo` + the counter hashes against concurrent writes. The Monitor
63
+ # is re-entrant so producer blocks can recursively consult the Store (e.g. one cache layer building
64
+ # on another) without dead-locking.
85
65
  @monitor = Monitor.new
86
66
  end
87
67
 
88
68
  attr_reader :root
89
69
 
90
- # @return [Boolean] whether this Store suppresses disk writes
91
- # (`schema_version.txt`, entry creation). Reads are
92
- # unaffected.
70
+ # @return [Boolean] whether this Store suppresses disk writes (`schema_version.txt`, entry creation).
71
+ # Reads are unaffected.
93
72
  def read_only?
94
73
  @read_only
95
74
  end
96
75
 
97
- # Returns a frozen snapshot of this Store's per-run hit / miss /
98
- # write counters. The bookkeeping is in-memory only — every new
99
- # `Store.new` starts at zero so the counters reflect activity
100
- # against this specific instance rather than the on-disk cache
101
- # state. Disk-level state is reported separately by
76
+ # Returns a frozen snapshot of this Store's per-run hit / miss / write counters. The bookkeeping is
77
+ # in-memory only — every new `Store.new` starts at zero — so the counters reflect activity against this
78
+ # specific instance rather than the on-disk cache state. Disk-level state is reported separately by
102
79
  # {.disk_inventory}.
103
80
  #
104
81
  # @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
@@ -109,26 +86,19 @@ module Rigor
109
86
  end
110
87
  end
111
88
 
112
- # Walks the on-disk cache rooted at `root` and reports a
113
- # producer-level inventory. Used by `rigor check --cache-stats`
114
- # to surface cache size and per-producer entry counts without
115
- # depending on in-process counters (which only reflect the
116
- # current run).
89
+ # Walks the on-disk cache rooted at `root` and reports a producer-level inventory. Used by
90
+ # `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
91
+ # in-process counters (which only reflect the current run).
117
92
  #
118
- # @return [Hash] `{ root:, schema_version:, total_entries:,
119
- # total_bytes:, producers: [{ id:, entries:, bytes: }, ...] }`.
120
- # When the root does not exist or has no schema-version
121
- # marker, `schema_version` is nil and the producer list is
122
- # empty.
93
+ # @return [Hash] `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
94
+ # bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
95
+ # nil and the producer list is empty.
123
96
  #
124
- # The `schema_version.txt` marker content. Covers BOTH
125
- # invalidation axes: the descriptor schema and the on-disk byte
126
- # layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the
127
- # old entries permanently unreadable (header mismatch miss)
128
- # but, alone, would never reclaim their bytes — they can sit
129
- # below the eviction cap forever. Folding the format version
130
- # into the marker routes the bump through the established
131
- # clear-the-root path instead.
97
+ # The `schema_version.txt` marker content. Covers BOTH invalidation axes: the descriptor schema and the
98
+ # on-disk byte layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the old entries permanently
99
+ # unreadable (header mismatch → miss) but, alone, would never reclaim their bytes — they can sit below
100
+ # the eviction cap forever. Folding the format version into the marker routes the bump through the
101
+ # established clear-the-root path instead.
132
102
  def self.schema_marker_value
133
103
  "#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
134
104
  end
@@ -166,31 +136,21 @@ module Rigor
166
136
  end
167
137
  private_class_method :collect_producers
168
138
 
169
- # @param producer_id [String] stable cache namespace; only
170
- # `[a-z][a-z0-9._-]*` is accepted.
171
- # @param params [Hash] producer inputs; mixed into the cache key
172
- # via {Descriptor#cache_key_for}.
173
- # @param descriptor [Rigor::Cache::Descriptor] the invalidation
174
- # descriptor for the value being cached.
175
- # @param serialize [#call, nil] optional callable that turns the
176
- # producer's return value into a binary `String`. Defaults to
177
- # `Marshal.dump(value).b`. Producers whose return values are
178
- # not `Marshal`-clean (RBS-native objects with `RBS::Location`
179
- # members, raw `IO`, …) MUST provide a serialiser. The pair
180
- # `(serialize, deserialize)` MUST round-trip a producer that
181
- # reads with one strategy and writes with another corrupts
182
- # its own cache slice.
183
- # @param deserialize [#call, nil] optional callable that turns
184
- # bytes back into the producer's value. Defaults to
185
- # `Marshal.load`. Any exception (`StandardError`) raised by
186
- # the deserialiser is treated as a cache miss — the entry is
187
- # considered corrupt, the producer block reruns, and the
188
- # next write overwrites it. This is consistent with the
189
- # fault-tolerance contract for the default `Marshal.load`
139
+ # @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
140
+ # @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
141
+ # @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
142
+ # @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
143
+ # `String`. Defaults to `Marshal.dump(value).b`. Producers whose return values are not
144
+ # `Marshal`-clean (RBS-native objects with `RBS::Location` members, raw `IO`, …) MUST provide a
145
+ # serialiser. The pair `(serialize, deserialize)` MUST round-trip — a producer that reads with one
146
+ # strategy and writes with another corrupts its own cache slice.
147
+ # @param deserialize [#call, nil] optional callable that turns bytes back into the producer's value.
148
+ # Defaults to `Marshal.load`. Any exception (`StandardError`) raised by the deserialiser is treated as
149
+ # a cache miss — the entry is considered corrupt, the producer block reruns, and the next write
150
+ # overwrites it. This is consistent with the fault-tolerance contract for the default `Marshal.load`
190
151
  # path.
191
152
  # @yieldreturn the value to cache.
192
- # @return the cached value (loaded from disk on hit; produced by
193
- # the block on miss).
153
+ # @return the cached value (loaded from disk on hit; produced by the block on miss).
194
154
  def fetch_or_compute(producer_id:, params:, descriptor:,
195
155
  serialize: nil, deserialize: nil, &block)
196
156
  validate_producer_id!(producer_id)
@@ -224,19 +184,15 @@ module Rigor
224
184
  value
225
185
  end
226
186
 
227
- # ADR-45 — record-and-validate variant. Unlike {fetch_or_compute},
228
- # which keys the entry on its descriptor (so the inputs MUST be
229
- # known before running), this keys on `key_descriptor` (the stable
230
- # inputs known up front) and stores, alongside the value, a
231
- # `dependency_descriptor` of the files the value actually read
232
- # including inputs discovered DURING the computation (e.g. a plugin
233
- # reading a file mid-analysis). On the next run the stored
234
- # dependencies are re-validated against the filesystem
187
+ # ADR-45 — record-and-validate variant. Unlike {fetch_or_compute}, which keys the entry on its
188
+ # descriptor (so the inputs MUST be known before running), this keys on `key_descriptor` (the stable
189
+ # inputs known up front) and stores, alongside the value, a `dependency_descriptor` of the files the
190
+ # value actually read including inputs discovered DURING the computation (e.g. a plugin reading a
191
+ # file mid-analysis). On the next run the stored dependencies are re-validated against the filesystem
235
192
  # ({Descriptor#fresh?}); a stale dependency forces a recompute.
236
193
  #
237
- # The block MUST return `[value, dependency_descriptor]`. Disk reads
238
- # are not in-process-memoisedvalidation always re-checks the
239
- # filesystem — but a single run only looks up once.
194
+ # The block MUST return `[value, dependency_descriptor]`. Disk reads are not in-process-memoised —
195
+ # validation always re-checks the filesystem but a single run only looks up once.
240
196
  def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
241
197
  validate_producer_id!(producer_id)
242
198
  ensure_schema_version!
@@ -253,9 +209,8 @@ module Rigor
253
209
  value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
254
210
  wrote = false
255
211
  unless @read_only
256
- # A cache write must never break the run. If the value is not
257
- # Marshal-clean (or any disk error occurs) skip caching and
258
- # return the freshly-computed value — the next run recomputes.
212
+ # A cache write must never break the run. If the value is not Marshal-clean (or any disk error
213
+ # occurs) skip caching and return the freshly-computed value — the next run recomputes.
259
214
  begin
260
215
  write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
261
216
  wrote = true
@@ -270,14 +225,12 @@ module Rigor
270
225
  value
271
226
  end
272
227
 
273
- # ADR-6 § "Eviction" — LRU pass over the on-disk cache. No-op when
274
- # `max_bytes:` was not configured or the store is read-only.
275
- # Walks all `.entry` files, sorts by mtime ascending (oldest = least
276
- # recently used), and unlinks from the oldest until the total is at
277
- # or below the cap. Touch-on-disk-read ({read_entry}) is the
278
- # cross-process LRU signal: every disk hit (not in-process-memo hit)
279
- # updates the mtime so recently-read entries survive the eviction pass.
280
- # Any FS error is swallowed — eviction must never break a run.
228
+ # ADR-6 § "Eviction" — LRU pass over the on-disk cache. No-op when `max_bytes:` was not configured or
229
+ # the store is read-only. Walks all `.entry` files, sorts by mtime ascending (oldest = least recently
230
+ # used), and unlinks from the oldest until the total is at or below the cap. Touch-on-disk-read
231
+ # ({read_entry}) is the cross-process LRU signal: every disk hit (not in-process-memo hit) updates the
232
+ # mtime so recently-read entries survive the eviction pass. Any FS error is swallowed — eviction must
233
+ # never break a run.
281
234
  def evict!
282
235
  return if @max_bytes.nil? || @read_only
283
236
 
@@ -324,9 +277,8 @@ module Rigor
324
277
  File.join(@root, producer_id, key[0, 2], "#{key[2..]}.entry")
325
278
  end
326
279
 
327
- # Reads and validates one entry file. Any failure (missing,
328
- # short, bad magic, bad version, bad checksum, unmarshal-able)
329
- # returns nil so the caller treats it as a cache miss.
280
+ # Reads and validates one entry file. Any failure (missing, short, bad magic, bad version, bad
281
+ # checksum, unmarshal-able) returns nil so the caller treats it as a cache miss.
330
282
  def read_entry(path, deserialize: nil)
331
283
  return nil unless File.file?(path)
332
284
 
@@ -344,8 +296,8 @@ module Rigor
344
296
  Entry.new(descriptor_bytes, value)
345
297
  end
346
298
 
347
- # Validates the magic + format-version header and the trailing
348
- # SHA-256 over everything before the trailer.
299
+ # Validates the magic + format-version header and the trailing SHA-256 over everything before the
300
+ # trailer.
349
301
  def envelope_valid?(bytes)
350
302
  return false if bytes.bytesize < HEADER.bytesize + 32
351
303
  return false unless bytes.byteslice(0, HEADER.bytesize) == HEADER
@@ -354,8 +306,8 @@ module Rigor
354
306
  Digest::SHA256.digest(bytes.byteslice(0, bytes.bytesize - 32)) == trailer
355
307
  end
356
308
 
357
- # Splits the body into (descriptor_bytes, value_bytes). Returns
358
- # `[nil, nil]` on a malformed varint or length-overrun.
309
+ # Splits the body into (descriptor_bytes, value_bytes). Returns `[nil, nil]` on a malformed varint or
310
+ # length-overrun.
359
311
  def parse_body(body)
360
312
  offset = 0
361
313
  descriptor_len, offset = read_varint(body, offset)
@@ -374,9 +326,8 @@ module Rigor
374
326
  LOAD_FAILED = Object.new.freeze
375
327
  private_constant :LOAD_FAILED
376
328
 
377
- # Inflates the stored value payload (ADR-54 WD2), then hands the
378
- # raw bytes to the deserialiser. Any failure corrupt deflate
379
- # stream included — reads as a miss.
329
+ # Inflates the stored value payload (ADR-54 WD2), then hands the raw bytes to the deserialiser. Any
330
+ # failure corrupt deflate stream includedreads as a miss.
380
331
  def safe_load(bytes, deserialize)
381
332
  raw = Zlib::Inflate.inflate(bytes)
382
333
  if deserialize
@@ -430,17 +381,13 @@ module Rigor
430
381
  end
431
382
 
432
383
  def ensure_schema_version!
433
- # Read-only stores never touch the cache root — no mkdir,
434
- # no marker write, no destructive clear on schema
435
- # mismatch. A stale or wrong-schema marker simply yields
436
- # nothing back (entries read through the version check
437
- # are content-keyed, so a write under the new schema
438
- # never collides with a read under the old). The next
439
- # writable run will repair the cache.
384
+ # Read-only stores never touch the cache root — no mkdir, no marker write, no destructive clear on
385
+ # schema mismatch. A stale or wrong-schema marker simply yields nothing back (entries read through
386
+ # the version check are content-keyed, so a write under the new schema never collides with a read
387
+ # under the old). The next writable run will repair the cache.
440
388
  return if @read_only
441
- # The marker is process-stable; one check per Store is
442
- # enough (a benign double-check under a thread race just
443
- # repeats idempotent work).
389
+ # The marker is process-stable; one check per Store is enough (a benign double-check under a thread
390
+ # race just repeats idempotent work).
444
391
  return if @schema_version_ensured
445
392
 
446
393
  @schema_version_ensured = true
@@ -465,9 +412,8 @@ module Rigor
465
412
  end
466
413
  end
467
414
 
468
- # LEB128 unsigned varint encoder/decoder. Lengths fit easily in
469
- # five bytes (cap at 2^35); the cache layer never writes a value
470
- # larger than that in practice.
415
+ # LEB128 unsigned varint encoder/decoder. Lengths fit easily in five bytes (cap at 2^35); the cache
416
+ # layer never writes a value larger than that in practice.
471
417
  def write_varint(bytes, value)
472
418
  raise ArgumentError, "varint must be non-negative" if value.negative?
473
419
 
@@ -482,9 +428,8 @@ module Rigor
482
428
  end
483
429
  end
484
430
 
485
- # Updates both atime and mtime of `path` to the current time — the
486
- # cross-process LRU signal used by {evict!}. Best-effort: any FS
487
- # error (read-only mount, deleted file) is silently ignored.
431
+ # Updates both atime and mtime of `path` to the current time — the cross-process LRU signal used by
432
+ # {evict!}. Best-effort: any FS error (read-only mount, deleted file) is silently ignored.
488
433
  def touch_for_lru(path)
489
434
  now = Time.now
490
435
  File.utime(now, now, path)
@@ -492,8 +437,8 @@ module Rigor
492
437
  nil
493
438
  end
494
439
 
495
- # Returns an array of `{ path:, mtime:, bytes: }` hashes for every
496
- # `.entry` file under the cache root, skipping unreadable entries.
440
+ # Returns an array of `{ path:, mtime:, bytes: }` hashes for every `.entry` file under the cache root,
441
+ # skipping unreadable entries.
497
442
  def collect_entry_stats
498
443
  Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
499
444
  stat = File.stat(path)
@@ -19,37 +19,26 @@ module Rigor
19
19
  class CLI
20
20
  # Executes `rigor annotate FILE`.
21
21
  #
22
- # For every source line the command finds the expression the
23
- # line evaluates to the last statement that ends on the line
24
- # (so `1; 2; 3` reports `3`), or, for a line that no statement
25
- # closes, the widest expression ending there (so the `if nil`
26
- # header reports its condition). It infers that expression's
27
- # type and appends a `#=> <type>` comment (the xmpfilter /
28
- # seeing_is_believing convention).
22
+ # For every source line the command finds the expression the line evaluates to — the last statement that ends on the
23
+ # line (so `1; 2; 3` reports `3`), or, for a line that no statement closes, the widest expression ending there (so
24
+ # the `if nil` header reports its condition). It infers that expression's type and appends a `#=> <type>` comment
25
+ # (the xmpfilter / seeing_is_believing convention).
29
26
  #
30
- # The annotated source is re-parsed with Prism — a sanity gate,
31
- # since the appended text is always a comment and printed to
32
- # stdout. When colour is enabled and `bat`
33
- # (https://github.com/sharkdp/bat) is on PATH it is used for
34
- # highlighting; otherwise IRB-style highlighting via
35
- # {PrismColorizer}.
27
+ # The annotated source is re-parsed with Prism — a sanity gate, since the appended text is always a comment — and
28
+ # printed to stdout. When colour is enabled and `bat` (https://github.com/sharkdp/bat) is on PATH it is used for
29
+ # highlighting; otherwise IRB-style highlighting via {PrismColorizer}.
36
30
  class AnnotateCommand < Command
37
31
  USAGE = "Usage: rigor annotate [options] FILE"
38
32
 
39
- # Trailing `#=> …` annotation comment. Matched and stripped
40
- # before re-annotating so re-running is idempotent this
41
- # follows xmpfilter's convention of owning the `#=>` marker,
42
- # and also absorbs the older `#=> dump_type: <type>` spelling
43
- # (idempotency across re-runs). The leading `\s` requirement
44
- # keeps a `#=>` inside a string literal (no preceding
45
- # whitespace ambiguity aside) from matching mid-expression.
33
+ # Trailing `#=> …` annotation comment. Matched and stripped before re-annotating so re-running is idempotent —
34
+ # this follows xmpfilter's convention of owning the `#=>` marker, and also absorbs the older `#=> dump_type:
35
+ # <type>` spelling (idempotency across re-runs). The leading `\s` requirement keeps a `#=>` inside a string
36
+ # literal (no preceding whitespace ambiguity aside) from matching mid-expression.
46
37
  ANNOTATION_PATTERN = /\s+#=>(?:\s.*)?\z/
47
38
 
48
- # Arguments for highlighting through `bat`: the annotated
49
- # text arrives on stdin, so the language must be explicit;
50
- # `--style=plain` drops the grid/header chrome so the output
51
- # matches the PrismColorizer fallback line-for-line; paging
52
- # stays off because the CLI may itself sit in a pipeline.
39
+ # Arguments for highlighting through `bat`: the annotated text arrives on stdin, so the language must be explicit;
40
+ # `--style=plain` drops the grid/header chrome so the output matches the PrismColorizer fallback line-for-line;
41
+ # paging stays off because the CLI may itself sit in a pipeline.
53
42
  BAT_ARGS = %w[--language=ruby --style=plain --paging=never --color=always].freeze
54
43
 
55
44
  # @return [Integer] CLI exit status.
@@ -71,8 +60,7 @@ module Rigor
71
60
  private
72
61
 
73
62
  def parse_options
74
- # Default: colour a tty, unless `NO_COLOR` opts out. An
75
- # explicit `--color` / `--no-color` overrides both.
63
+ # Default: colour a tty, unless `NO_COLOR` opts out. An explicit `--color` / `--no-color` overrides both.
76
64
  options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil, format: :text }
77
65
 
78
66
  parser = OptionParser.new do |opts|
@@ -96,9 +84,8 @@ module Rigor
96
84
  options
97
85
  end
98
86
 
99
- # https://no-color.org — colour output is suppressed by
100
- # default when `NO_COLOR` is present and not an empty string,
101
- # regardless of its value.
87
+ # https://no-color.org — colour output is suppressed by default when `NO_COLOR` is present and not an empty
88
+ # string, regardless of its value.
102
89
  def no_color_env?
103
90
  value = ENV.fetch("NO_COLOR", nil)
104
91
  !value.nil? && !value.empty?
@@ -106,22 +93,17 @@ module Rigor
106
93
 
107
94
  def execute(file, options)
108
95
  configuration = Configuration.load(options.fetch(:config))
109
- # Force UTF-8 (with BOM tolerance) regardless of
110
- # `Encoding.default_external`. Under a minimal locale
111
- # the default is US-ASCII; reading multi-byte source
112
- # under that tag makes the post-parse `String#sub` /
113
- # `String#lines` calls in `#annotate` raise
114
- # `invalid byte sequence in US-ASCII`. Ruby source is
115
- # UTF-8 by convention (the parser's own assumption
116
- # absent a magic comment).
96
+ # Force UTF-8 (with BOM tolerance) regardless of `Encoding.default_external`. Under a minimal locale the default
97
+ # is US-ASCII; reading multi-byte source under that tag makes the post-parse `String#sub` / `String#lines` calls
98
+ # in `#annotate` raise `invalid byte sequence in US-ASCII`. Ruby source is UTF-8 by convention (the parser's own
99
+ # assumption absent a magic comment).
117
100
  source = File.read(file, mode: "r:bom|utf-8")
118
101
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
119
102
  return 1 if parse_errors?(parse_result, file)
120
103
 
121
- # `converged_loop_recording` re-records fixpoint-tracked loop
122
- # bodies from their converged (post-writeback) bindings, so a
123
- # loop-body line annotates the joined widened type (`Integer`)
124
- # rather than a stale first-iterations constant (`1 | 2`).
104
+ # `converged_loop_recording` re-records fixpoint-tracked loop bodies from their converged (post-writeback)
105
+ # bindings, so a loop-body line annotates the joined widened type (`Integer`) rather than a stale
106
+ # first-iterations constant (`1 | 2`).
125
107
  scope_index = Inference::ScopeIndexer.index(
126
108
  parse_result.value, default_scope: base_scope(configuration),
127
109
  converged_loop_recording: true
@@ -137,11 +119,10 @@ module Rigor
137
119
  0
138
120
  end
139
121
 
140
- # `--format json` — emit the { line_number => type } map directly, the
141
- # same data the text renderer consumes, so clients (the playground,
142
- # editors) get structured annotations without reparsing the `#=> <type>`
143
- # comment grammar. Values are the short type descriptions the text form
144
- # also shows; keys are 1-based line numbers as strings (JSON object keys).
122
+ # `--format json` — emit the { line_number => type } map directly, the same data the text renderer consumes, so
123
+ # clients (the playground, editors) get structured annotations without reparsing the `#=> <type>` comment grammar.
124
+ # Values are the short type descriptions the text form also shows; keys are 1-based line numbers as strings (JSON
125
+ # object keys).
145
126
  def emit_json(line_types)
146
127
  annotations = line_types.keys.sort.to_h { |line| [line.to_s, line_types[line].describe(:short)] }
147
128
  @out.puts(JSON.generate({ "annotations" => annotations }))
@@ -165,8 +146,7 @@ module Rigor
165
146
  true
166
147
  end
167
148
 
168
- # Appends ` #=> <type>` to every line a type was inferred
169
- # for, aligning the comment column.
149
+ # Appends ` #=> <type>` to every line a type was inferred for, aligning the comment column.
170
150
  def annotate(source, line_types)
171
151
  lines = source.lines
172
152
  column = annotation_column(lines, line_types)
@@ -198,10 +178,8 @@ module Rigor
198
178
  rendered || PrismColorizer.colorize(annotated)
199
179
  end
200
180
 
201
- # Pipes the annotated source through `bat` and returns its
202
- # highlighted output, or nil when bat is unavailable or
203
- # fails (broken install, killed mid-write) — the caller
204
- # falls back to {PrismColorizer}. An explicit `--bat` with
181
+ # Pipes the annotated source through `bat` and returns its highlighted output, or nil when bat is unavailable or
182
+ # fails (broken install, killed mid-write) the caller falls back to {PrismColorizer}. An explicit `--bat` with
205
183
  # no bat on PATH warns instead of failing silently.
206
184
  def render_with_bat(annotated, forced: nil)
207
185
  executable = bat_executable
@@ -233,9 +211,8 @@ module Rigor
233
211
  end
234
212
  end
235
213
 
236
- # Walks a parsed program and resolves, per source line, the
237
- # type of the expression the line evaluates to. Used only by
238
- # {AnnotateCommand}.
214
+ # Walks a parsed program and resolves, per source line, the type of the expression the line evaluates to. Used only
215
+ # by {AnnotateCommand}.
239
216
  class LineTypeCollector
240
217
  def initialize(scope_index)
241
218
  @scope_index = scope_index
@@ -256,16 +233,11 @@ module Rigor
256
233
 
257
234
  private
258
235
 
259
- # Yields each statement node (a child of any `StatementsNode`
260
- # anywhere in the tree) in post-order: nested statements are
261
- # yielded before the enclosing statement that contains them.
262
- # `by_line[end_line] = type` overwrites earlier entries, so
263
- # post-order means the *outermost* statement closing a line
264
- # wins — for `b = if cond then :then else :else end` the
265
- # line resolves to the assignment's type (the if-expression's
266
- # union), not the else-branch's inner `:else`. Direct siblings
267
- # (`1; 2; 3`) are still yielded in source order so the last
268
- # sibling wins.
236
+ # Yields each statement node (a child of any `StatementsNode` anywhere in the tree) in post-order: nested
237
+ # statements are yielded before the enclosing statement that contains them. `by_line[end_line] = type` overwrites
238
+ # earlier entries, so post-order means the *outermost* statement closing a line wins — for `b = if cond then :then
239
+ # else :else end` the line resolves to the assignment's type (the if-expression's union), not the else-branch's
240
+ # inner `:else`. Direct siblings (`1; 2; 3`) are still yielded in source order so the last sibling wins.
269
241
  def each_statement(node, &block)
270
242
  return if node.nil?
271
243
 
@@ -279,8 +251,7 @@ module Rigor
279
251
  end
280
252
  end
281
253
 
282
- # For a line no statement closes (the `if` / block header
283
- # lines), fall back to the widest expression ending there.
254
+ # For a line no statement closes (the `if` / block header lines), fall back to the widest expression ending there.
284
255
  def fill_uncovered_lines(program, by_line)
285
256
  widest_per_line(program).each do |line, node|
286
257
  next if by_line.key?(line)
@@ -290,12 +261,10 @@ module Rigor
290
261
  end
291
262
  end
292
263
 
293
- # A `do |i|` header line's widest node is its BlockParametersNode —
294
- # not an expression, so evaluating it would only echo the
295
- # `Dynamic[top]` fallback. Annotate the line with the parameters'
296
- # inferred bindings instead (the single param's type, or a tuple
297
- # for multi-param blocks); decline (nil) when any param has no
298
- # plain name or no recorded binding, leaving the line bare.
264
+ # A `do |i|` header line's widest node is its BlockParametersNode — not an expression, so evaluating it would only
265
+ # echo the `Dynamic[top]` fallback. Annotate the line with the parameters' inferred bindings instead (the single
266
+ # param's type, or a tuple for multi-param blocks); decline (nil) when any param has no plain name or no recorded
267
+ # binding, leaving the line bare.
299
268
  def block_params_type(params_node)
300
269
  inner = params_node.parameters
301
270
  return nil if inner.nil? || inner.requireds.empty?
@@ -332,26 +301,20 @@ module Rigor
332
301
  node.compact_child_nodes.each { |child| walk(child, &block) }
333
302
  end
334
303
 
335
- # Types the node through the flow evaluator (not the bare
336
- # expression typer) under its recorded entry scope, so flow-only
337
- # forms type as the engine sees them — `i += 1` dispatches `+` on
338
- # `i`'s binding (`Integer`, post-fixpoint) instead of echoing the
339
- # RHS literal's `1`.
304
+ # Types the node through the flow evaluator (not the bare expression typer) under its recorded entry scope, so
305
+ # flow-only forms type as the engine sees them — `i += 1` dispatches `+` on `i`'s binding (`Integer`,
306
+ # post-fixpoint) instead of echoing the RHS literal's `1`.
340
307
  def type_of(node)
341
308
  Inference::StatementEvaluator.new(scope: @scope_index[node]).evaluate(node).first
342
309
  rescue StandardError
343
310
  nil
344
311
  end
345
312
 
346
- # For every `def`, replace the annotation on the header line
347
- # (where the `def` keyword sits) with the method's inferred
348
- # return type. The default annotation there comes from the
349
- # parameter list (`name` typing as `Dynamic[top]`), which is
350
- # noise; the return type is what readers actually want next
351
- # to the method signature. When the return type cannot be
352
- # inferred (empty body, scope-lookup miss, or any error
353
- # under `DefReturnTyper.call`), the entry is deleted so no
354
- # annotation is shown on that line.
313
+ # For every `def`, replace the annotation on the header line (where the `def` keyword sits) with the method's
314
+ # inferred return type. The default annotation there comes from the parameter list (`name` typing as
315
+ # `Dynamic[top]`), which is noise; the return type is what readers actually want next to the method signature.
316
+ # When the return type cannot be inferred (empty body, scope-lookup miss, or any error under
317
+ # `DefReturnTyper.call`), the entry is deleted so no annotation is shown on that line.
355
318
  def override_def_header_lines(program, by_line)
356
319
  each_def_node(program) do |def_node|
357
320
  line = def_node.location.start_line