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
@@ -8,72 +8,61 @@ require_relative "rbs_hierarchy"
8
8
 
9
9
  module Rigor
10
10
  class Environment
11
- # Loads RBS class declarations and method definitions from disk and
12
- # exposes them to the inference engine in a small, stable surface.
11
+ # Loads RBS class declarations and method definitions from disk and exposes them to the inference engine
12
+ # in a small, stable surface.
13
13
  #
14
- # Slice 4 phase 1 only enabled the RBS *core* signatures shipped with
15
- # the `rbs` gem (`Object`, `Integer`, `String`, `Array`, ...). Phase
16
- # 2a adds opt-in stdlib library loading (`pathname`, `json`,
17
- # `tempfile`, ...) and arbitrary-directory signature loading
18
- # (typically the project's local `sig/` tree). Both are off by
19
- # default on `RbsLoader.default` so the core-only fast path stays
20
- # cheap; project-aware loading is opted into through
21
- # {Environment.for_project} or by constructing a custom loader.
14
+ # Slice 4 phase 1 only enabled the RBS *core* signatures shipped with the `rbs` gem (`Object`, `Integer`,
15
+ # `String`, `Array`, ...). Phase 2a adds opt-in stdlib library loading (`pathname`, `json`, `tempfile`,
16
+ # ...) and arbitrary-directory signature loading (typically the project's local `sig/` tree). Both are
17
+ # off by default on `RbsLoader.default` so the core-only fast path stays cheap; project-aware loading is
18
+ # opted into through {Environment.for_project} or by constructing a custom loader.
22
19
  #
23
- # The default instance is shared across the process: building the
24
- # core RBS environment costs hundreds of milliseconds and the data
25
- # is read-only. The shared instance is frozen, but holds a mutable
26
- # state hash for lazy memoization of the heavy `RBS::Environment`
27
- # and `RBS::DefinitionBuilder` -- the user-visible API stays purely
28
- # functional.
20
+ # The default instance is shared across the process: building the core RBS environment costs hundreds of
21
+ # milliseconds and the data is read-only. The shared instance is frozen, but holds a mutable state hash
22
+ # for lazy memoization of the heavy `RBS::Environment` and `RBS::DefinitionBuilder` -- the user-visible
23
+ # API stays purely functional.
29
24
  #
30
25
  # See docs/internal-spec/inference-engine.md for the binding contract.
31
26
  # rubocop:disable Metrics/ClassLength
32
27
  class RbsLoader
33
- # Buffer name stamped on the `module` declarations synthesized by
34
- # {.synthesize_missing_namespaces}. Re-read off the built env by
35
- # {#synthesized_namespaces} so the analysis layer can surface an
36
- # `:info` diagnostic naming the project's malformed-RBS namespaces
37
- # — robust across the marshalled env cache, since the sentinel
38
- # rides along on each synthetic declaration's location.
28
+ # Buffer name stamped on the `module` declarations synthesized by {.synthesize_missing_namespaces}.
29
+ # Re-read off the built env by {#synthesized_namespaces} so the analysis layer can surface an `:info`
30
+ # diagnostic naming the project's malformed-RBS namespaces robust across the marshalled env cache,
31
+ # since the sentinel rides along on each synthetic declaration's location.
39
32
  SYNTHETIC_NAMESPACE_BUFFER = "(rigor: synthesized namespaces)"
40
33
 
41
- # Buffer name stamped on the stub `class` / `module` declarations
42
- # synthesized by {.stub_missing_referenced_types} for types the
43
- # project's RBS references but no loaded signature declares.
44
- # {#synthesized_stub_types} reads them back off the built env (so
45
- # the answer survives the marshalled env cache), and
46
- # {#synthesized_type_names} folds them together with the
47
- # namespace stubs into the set {MethodDispatcher} resolves to
48
- # `Dynamic[Top]` (no false `call.undefined-method`).
34
+ # Buffer name stamped on the stub `class` / `module` declarations synthesized by
35
+ # {.stub_missing_referenced_types} for types the project's RBS references but no loaded signature
36
+ # declares. {#synthesized_stub_types} reads them back off the built env (so the answer survives the
37
+ # marshalled env cache), and {#synthesized_type_names} folds them together with the namespace stubs
38
+ # into the set {MethodDispatcher} resolves to `Dynamic[Top]` (no false `call.undefined-method`).
49
39
  SYNTHETIC_STUB_BUFFER = "(rigor: synthesized stub types)"
50
40
 
41
+ # Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
42
+ # before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
43
+ # errors buries the signal.
44
+ QUARANTINE_WARN_LIMIT = 10
45
+
51
46
  class << self
52
47
  def default
53
48
  @default ||= new.freeze
54
49
  end
55
50
 
56
- # Used by tests to discard the cached default loader; production
57
- # code MUST NOT call this. The shared loader holds a several-MB
58
- # RBS::Environment, so dropping it during a normal run wastes the
59
- # cost of rebuilding it.
51
+ # Used by tests to discard the cached default loader; production code MUST NOT call this. The shared
52
+ # loader holds a several-MB RBS::Environment, so dropping it during a normal run wastes the cost of
53
+ # rebuilding it.
60
54
  def reset_default!
61
55
  @default = nil
62
56
  end
63
57
 
64
- # Builds an `RBS::Environment` from explicit `libraries` and
65
- # `signature_paths`. Stateless surface so the v0.0.9
66
- # {Cache::RbsEnvironment} producer can build an env on cache
67
- # miss without holding a loader instance, and the
68
- # instance-side {#build_env} delegates here so the
69
- # implementation stays single-rooted.
58
+ # Builds an `RBS::Environment` from explicit `libraries` and `signature_paths`. Stateless surface so
59
+ # the v0.0.9 {Cache::RbsEnvironment} producer can build an env on cache miss without holding a loader
60
+ # instance, and the instance-side {#build_env} delegates here so the implementation stays
61
+ # single-rooted.
70
62
  #
71
- # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are
72
- # loaded on top of `signature_paths` so the per-gem RBS
73
- # bundled with Rigor itself is in scope for every analysis
74
- # run. The gem stubs are intentionally read-only and
75
- # appended LAST so user-supplied `signature_paths` win on
76
- # name conflicts.
63
+ # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the
64
+ # per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
65
+ # intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
77
66
  def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
78
67
  rbs_loader = RBS::EnvironmentLoader.new
79
68
  libraries.each do |library|
@@ -81,59 +70,51 @@ module Rigor
81
70
 
82
71
  rbs_loader.add(library: library, version: nil)
83
72
  end
84
- signature_paths.each do |path|
85
- path = Pathname(path) unless path.is_a?(Pathname)
86
- rbs_loader.add(path: path) if path.directory?
87
- end
73
+ # Project `signature_paths:` are loaded per-file by {.add_project_signatures} AFTER `from_loader`,
74
+ # NOT added to the loader here: `RBS::Environment.from_loader` parses every added file all-or-nothing,
75
+ # so one unparseable user `.rbs` raises `RBS::ParsingError` and collapses the WHOLE env to nil (every
76
+ # type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06
77
+ # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
78
+ # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
88
79
  vendored_gem_sig_paths.each do |path|
89
80
  rbs_loader.add(path: path) if path.directory?
90
81
  end
91
- # Rigor-owned core overlay — loaded LAST so an upstream
92
- # declaration always wins on conflict; these reopenings only
93
- # fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which
94
- # upstream RBS declares on the concrete subclasses but not on
95
- # the abstract `Numeric` that Rigor's arithmetic-chain widening
96
- # produces).
82
+ # Rigor-owned core overlay — loaded LAST so an upstream declaration always wins on conflict; these
83
+ # reopenings only fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which upstream RBS
84
+ # declares on the concrete subclasses but not on the abstract `Numeric` that Rigor's
85
+ # arithmetic-chain widening produces).
97
86
  core_overlay_sig_paths.each do |path|
98
87
  rbs_loader.add(path: path) if path.directory?
99
88
  end
100
89
  env = RBS::Environment.from_loader(rbs_loader)
90
+ add_project_signatures(env, signature_paths)
101
91
  add_virtual_rbs(env, virtual_rbs)
102
92
  synthesize_missing_namespaces(env)
103
93
  resolved = env.resolve_type_names
104
94
  stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
105
95
  end
106
96
 
107
- # ADR-5 robustness, second tier. A project `signature_paths:`
108
- # RBS that *references* a type no loaded signature declares
109
- # `def x: () -> DRb::DRbServer` when the `drb` RBS is not
110
- # available, or a stale reference to its own removed
111
- # `Textbringer::EditorError` makes
112
- # `RBS::DefinitionBuilder#build_instance` raise
113
- # `NoTypeFoundError`, and (per RBS's all-or-nothing per-class
114
- # build) that single unresolved reference takes down EVERY
115
- # method on the class, not just the one signature. Observed on
116
- # shugo/textbringer: one `DRb::DRbServer` reference left the
117
- # whole `Textbringer::Commands` module — including its
118
- # 186-call-site `define_command` DSL — resolving as
97
+ # ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
98
+ # signature declares `def x: () -> DRb::DRbServer` when the `drb` RBS is not available, or a stale
99
+ # reference to its own removed `Textbringer::EditorError` makes
100
+ # `RBS::DefinitionBuilder#build_instance` raise `NoTypeFoundError`, and (per RBS's all-or-nothing
101
+ # per-class build) that single unresolved reference takes down EVERY method on the class, not just
102
+ # the one signature. Observed on shugo/textbringer: one `DRb::DRbServer` reference left the whole
103
+ # `Textbringer::Commands` module including its 186-call-site `define_command` DSL — resolving as
119
104
  # `Dynamic[Top]`.
120
105
  #
121
- # We synthesize an empty stub for each such referenced-but-
122
- # undeclared type so the rest of the class builds. A leaf type
123
- # is stubbed as `class`, its enclosing namespaces as `module`.
124
- # Stubbed types carry no methods, so a call against a value of
125
- # a stubbed type would otherwise mis-fire `call.undefined-method`;
126
- # {MethodDispatcher} consults {#synthesized_type_names} and
127
- # resolves such calls to `Dynamic[Top]` instead (the same
128
- # no-false-positive contract as the dependency-source tier).
106
+ # We synthesize an empty stub for each such referenced-but- undeclared type so the rest of the class
107
+ # builds. A leaf type is stubbed as `class`, its enclosing namespaces as `module`. Stubbed types
108
+ # carry no methods, so a call against a value of a stubbed type would otherwise mis-fire
109
+ # `call.undefined-method`; {MethodDispatcher} consults {#synthesized_type_names} and resolves such
110
+ # calls to `Dynamic[Top]` instead (the same no-false-positive contract as the dependency-source
111
+ # tier).
129
112
  #
130
- # Detection re-uses RBS's own builder (correct by construction):
131
- # build every PROJECT class and read the missing name out of the
132
- # raised error. Bounded to `signature_paths` classes (stdlib /
133
- # vendored RBS is well-formed) and to {MAX_STUB_PASSES}
134
- # iterations — a fresh stub can expose a deeper reference the
135
- # first build error hid, but empty stubs reference nothing, so
136
- # the fixpoint converges quickly.
113
+ # Detection re-uses RBS's own builder (correct by construction): build every PROJECT class and read
114
+ # the missing name out of the raised error. Bounded to `signature_paths` classes (stdlib / vendored
115
+ # RBS is well-formed) and to {MAX_STUB_PASSES} iterations a fresh stub can expose a deeper
116
+ # reference the first build error hid, but empty stubs reference nothing, so the fixpoint converges
117
+ # quickly.
137
118
  MAX_STUB_PASSES = 5
138
119
 
139
120
  def stub_missing_referenced_types(base_env, resolved, project_files)
@@ -149,20 +130,15 @@ module Rigor
149
130
  resolved
150
131
  end
151
132
 
152
- # Robustness (ADR-5): a project whose RBS declares qualified
153
- # names (`class Foo::Bar`) without ever declaring the enclosing
154
- # namespace (`module Foo`) is invalid by upstream RBS rules —
155
- # `RBS::DefinitionBuilder#build_instance` raises
156
- # `NoTypeFoundError: Could not find ::Foo`, which the loader's
157
- # fail-soft rescue turns into a silent dispatch miss (every
158
- # method on every such class degrades to `Dynamic[Top]`). This
159
- # is a common authoring mistake (e.g. shugo/textbringer ships a
160
- # `sig/` that `rbs validate` itself rejects). Rather than let an
161
- # otherwise-usable signature set contribute nothing, synthesize
162
- # an empty `module` declaration for each undeclared enclosing
163
- # namespace so the definitions build. We only ever add names
164
- # that are absent — a genuinely-declared namespace (module or
165
- # class, here or in a loaded gem) is left untouched.
133
+ # Robustness (ADR-5): a project whose RBS declares qualified names (`class Foo::Bar`) without ever
134
+ # declaring the enclosing namespace (`module Foo`) is invalid by upstream RBS rules —
135
+ # `RBS::DefinitionBuilder#build_instance` raises `NoTypeFoundError: Could not find ::Foo`, which the
136
+ # loader's fail-soft rescue turns into a silent dispatch miss (every method on every such class
137
+ # degrades to `Dynamic[Top]`). This is a common authoring mistake (e.g. shugo/textbringer ships a
138
+ # `sig/` that `rbs validate` itself rejects). Rather than let an otherwise-usable signature set
139
+ # contribute nothing, synthesize an empty `module` declaration for each undeclared enclosing
140
+ # namespace so the definitions build. We only ever add names that are absent — a genuinely-declared
141
+ # namespace (module or class, here or in a loaded gem) is left untouched.
166
142
  def synthesize_missing_namespaces(env)
167
143
  missing = collect_missing_namespaces(env)
168
144
  return if missing.empty?
@@ -172,16 +148,14 @@ module Rigor
172
148
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
173
149
  add_parsed_decls(env, buffer, directives, decls)
174
150
  rescue ::RBS::BaseError
175
- # Fail-soft: synthesis is an opportunistic uplift, never a
176
- # hard requirement. A parse failure here just leaves the env
177
- # as it was (dispatch misses on the affected classes).
151
+ # Fail-soft: synthesis is an opportunistic uplift, never a hard requirement. A parse failure here
152
+ # just leaves the env as it was (dispatch misses on the affected classes).
178
153
  nil
179
154
  end
180
155
 
181
- # Returns the `::`-stripped names of every enclosing namespace
182
- # that some declaration references but no declaration defines,
183
- # shallowest-first so the synthesized source declares `Foo`
184
- # before `Foo::Bar`.
156
+ # Returns the `::`-stripped names of every enclosing namespace that some declaration references but
157
+ # no declaration defines, shallowest-first so the synthesized source declares `Foo` before
158
+ # `Foo::Bar`.
185
159
  def collect_missing_namespaces(env)
186
160
  declared = env.class_decls.keys.to_set
187
161
  missing = {}
@@ -196,10 +170,9 @@ module Rigor
196
170
  missing.sort_by { |_name, depth| depth }.map(&:first)
197
171
  end
198
172
 
199
- # The absolute paths of every `.rbs` file under the project's
200
- # `signature_paths:` (NOT vendored / stdlib RBS — those are
201
- # well-formed, so attempting to build them would only waste
202
- # time). Used to scope the referenced-type build sweep.
173
+ # The absolute paths of every `.rbs` file under the project's `signature_paths:` (NOT vendored /
174
+ # stdlib RBS — those are well-formed, so attempting to build them would only waste time). Used to
175
+ # scope the referenced-type build sweep.
203
176
  def project_sig_files(signature_paths)
204
177
  signature_paths.flat_map do |path|
205
178
  path = Pathname(path) unless path.is_a?(Pathname)
@@ -209,10 +182,57 @@ module Rigor
209
182
  end.to_set
210
183
  end
211
184
 
212
- # Builds every project class (instance + singleton side) and
213
- # returns the `::`-stripped names of the types whose absence
214
- # raised `NoTypeFoundError`. Only the FIRST missing reference
215
- # per class surfaces per build, which is why the caller loops.
185
+ # Load the project's `signature_paths:` RBS files into `env` ONE FILE AT A TIME, quarantining any file
186
+ # that fails to parse rather than letting it collapse the whole env (the `from_loader` batch parse is
187
+ # all-or-nothing see {.build_env_for}). A quarantined file's declarations are simply absent, so calls
188
+ # into the types it would have declared read `Dynamic[top]`; the rest of the project's (and all
189
+ # bundled) RBS still loads. The user is told which files were skipped via
190
+ # {RbsLoader#warn_about_quarantined_signatures}, so this is a graceful degrade, not a silent one.
191
+ #
192
+ # Buffer names are the file's absolute path (matching {.project_sig_files}) so {.project_entry?} — which
193
+ # attributes a `class_decls` entry to the project by buffer name — still recognises these declarations.
194
+ # Sorted for a deterministic add order (the env feeds the cache, ADR-54).
195
+ def add_project_signatures(env, signature_paths)
196
+ project_sig_files(signature_paths).sort.each do |file|
197
+ parsed = parse_signature_file(file)
198
+ next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
199
+
200
+ buffer, directives, decls = parsed
201
+ add_parsed_decls(env, buffer, directives, decls)
202
+ end
203
+ end
204
+
205
+ # Parse one project `.rbs` into `[buffer, directives, decls]`, or nil when it is unparseable /
206
+ # unreadable. Mirrors `RBS::EnvironmentLoader#each_signature`'s per-file parse so the decls register
207
+ # identically to the loader's batch path.
208
+ def parse_signature_file(file)
209
+ buffer = ::RBS::Buffer.new(name: file, content: File.read(file, encoding: "UTF-8"))
210
+ _buffer, directives, decls = ::RBS::Parser.parse_signature(buffer)
211
+ [buffer, directives, decls]
212
+ rescue ::RBS::ParsingError, Errno::ENOENT, Errno::EISDIR, Errno::EACCES
213
+ nil
214
+ end
215
+
216
+ # The project `signature_paths:` files that FAIL to parse, as `[absolute_path, first_error_line]` pairs
217
+ # (sorted, deterministic). Detection is independent of {.add_project_signatures} so the warning fires
218
+ # even on a cache hit (where the env was already built with the file quarantined). Cheap: it only
219
+ # re-parses the user's own (usually small) `sig/` set, and returns empty immediately when there is no
220
+ # `signature_paths:`.
221
+ def quarantined_project_signatures(signature_paths)
222
+ project_sig_files(signature_paths).sort.filter_map do |file|
223
+ buffer = ::RBS::Buffer.new(name: file, content: File.read(file, encoding: "UTF-8"))
224
+ ::RBS::Parser.parse_signature(buffer)
225
+ nil
226
+ rescue ::RBS::ParsingError => e
227
+ [file, e.message.to_s.lines.first.to_s.strip]
228
+ rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
229
+ nil
230
+ end
231
+ end
232
+
233
+ # Builds every project class (instance + singleton side) and returns the `::`-stripped names of the
234
+ # types whose absence raised `NoTypeFoundError`. Only the FIRST missing reference per class surfaces
235
+ # per build, which is why the caller loops.
216
236
  def unresolved_referenced_types(env, project_files)
217
237
  builder = ::RBS::DefinitionBuilder.new(env: env)
218
238
  missing = []
@@ -225,21 +245,18 @@ module Rigor
225
245
  name = e.message[/Could not find (\S+)/, 1]
226
246
  missing << name.sub(/\A::/, "") if name
227
247
  rescue ::RBS::BaseError
228
- # Other build failures (duplicate decl, mixin cycle, ...)
229
- # are not ours to repair here — leave them fail-soft.
248
+ # Other build failures (duplicate decl, mixin cycle, ...) are not ours to repair here — leave
249
+ # them fail-soft.
230
250
  end
231
251
  end
232
252
  missing.uniq
233
253
  end
234
254
 
235
- # Normalises a `class_decls` entry's representative declaration
236
- # across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
237
- # RBS 4.x exposes it as `entry.primary_decl` (the AST declaration
238
- # directly); RBS 3.x exposes `entry.primary` (a wrapper whose
239
- # `#decl` is the AST declaration). Returns the AST declaration, or
240
- # nil when neither accessor is present. Without this guard,
241
- # `class_decl_paths` crashed under RBS 3.x with
242
- # `undefined method 'primary_decl'`.
255
+ # Normalises a `class_decls` entry's representative declaration across the gemspec's supported RBS
256
+ # range (`rbs >= 3.0, < 5.0`). RBS 4.x exposes it as `entry.primary_decl` (the AST declaration
257
+ # directly); RBS 3.x exposes `entry.primary` (a wrapper whose `#decl` is the AST declaration).
258
+ # Returns the AST declaration, or nil when neither accessor is present. Without this guard,
259
+ # `class_decl_paths` crashed under RBS 3.x with `undefined method 'primary_decl'`.
243
260
  def primary_decl_for(entry)
244
261
  if entry.respond_to?(:primary_decl)
245
262
  entry.primary_decl
@@ -249,18 +266,14 @@ module Rigor
249
266
  end
250
267
  end
251
268
 
252
- # Appends freshly-parsed declarations to an `RBS::Environment`
253
- # across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
254
- # RBS 4.x wraps the declarations in an `RBS::Source::RBS` and
255
- # takes them through `env.add_source`; RBS 3.x has neither
256
- # `RBS::Source` nor `add_source` and instead registers them with
257
- # `env.add_signature(buffer:, directives:, decls:)` (a bare
258
- # `env << decl` is NOT enough — it skips the `signatures` table
259
- # that `resolve_type_names` rebuilds from, so the synthesized
260
- # declarations silently vanish on resolve). Without this guard
261
- # the synthesis paths (`synthesize_missing_namespaces`,
262
- # `append_stub_declarations`, `add_virtual_rbs`) crashed under
263
- # RBS 3.x with `uninitialized constant RBS::Source`.
269
+ # Appends freshly-parsed declarations to an `RBS::Environment` across the gemspec's supported RBS
270
+ # range (`rbs >= 3.0, < 5.0`). RBS 4.x wraps the declarations in an `RBS::Source::RBS` and takes them
271
+ # through `env.add_source`; RBS 3.x has neither `RBS::Source` nor `add_source` and instead registers
272
+ # them with `env.add_signature(buffer:, directives:, decls:)` (a bare `env << decl` is NOT enough —
273
+ # it skips the `signatures` table that `resolve_type_names` rebuilds from, so the synthesized
274
+ # declarations silently vanish on resolve). Without this guard the synthesis paths
275
+ # (`synthesize_missing_namespaces`, `append_stub_declarations`, `add_virtual_rbs`) crashed under RBS
276
+ # 3.x with `uninitialized constant RBS::Source`.
264
277
  def add_parsed_decls(env, buffer, directives, decls)
265
278
  decls ||= []
266
279
  directives ||= []
@@ -273,9 +286,8 @@ module Rigor
273
286
  end
274
287
  end
275
288
 
276
- # True when a `class_decls` entry was declared in one of the
277
- # project's own signature files (by declaration location), so
278
- # the sweep skips the bundled stdlib / vendored universe.
289
+ # True when a `class_decls` entry was declared in one of the project's own signature files (by
290
+ # declaration location), so the sweep skips the bundled stdlib / vendored universe.
279
291
  def project_entry?(entry, project_files)
280
292
  decl = primary_decl_for(entry)
281
293
  location = decl&.location
@@ -285,25 +297,19 @@ module Rigor
285
297
  project_files.include?(File.expand_path(buffer_name.to_s))
286
298
  end
287
299
 
288
- # Adds empty stub declarations for the missing referenced types
289
- # (and any enclosing namespace they need) to the pre-resolve
290
- # env, tagged with {SYNTHETIC_STUB_BUFFER}. A name that is a
291
- # prefix of another name is declared `module` (it is a
292
- # namespace); a leaf is declared `class` (referenced types
300
+ # Adds empty stub declarations for the missing referenced types (and any enclosing namespace they
301
+ # need) to the pre-resolve env, tagged with {SYNTHETIC_STUB_BUFFER}. A name that is a prefix of
302
+ # another name is declared `module` (it is a namespace); a leaf is declared `class` (referenced types
293
303
  # appear in instance position far more often than as mixins).
294
304
  #
295
- # Names already declared in `base_env` are skipped — exactly the
296
- # `declared.include?` guard {.collect_missing_namespaces} applies.
297
- # Without it, stubbing a nested reference (`Foo::Bar::Baz`) re-emits
298
- # its enclosing prefix (`Foo::Bar`) as a `module`, and when that
299
- # prefix is already a `class` in the project's own `sig/` the kind
300
- # mismatch makes `resolve_type_names` raise
301
- # `RBS::DuplicatedDeclarationError`, collapsing the WHOLE env to nil
302
- # (every type-of query then degrades to `Dynamic[Top]`). One
303
- # malformed `.rbs` must not disproportionately blind the analysis:
304
- # a subclass sig that references an inherited nested type
305
- # (`class GitAdapter; def x: () -> GitAdapter::Revision`) was the
306
- # real-world trigger — see the 2026-07-04 redmine onboarding note.
305
+ # Names already declared in `base_env` are skipped — exactly the `declared.include?` guard
306
+ # {.collect_missing_namespaces} applies. Without it, stubbing a nested reference (`Foo::Bar::Baz`)
307
+ # re-emits its enclosing prefix (`Foo::Bar`) as a `module`, and when that prefix is already a `class`
308
+ # in the project's own `sig/` the kind mismatch makes `resolve_type_names` raise
309
+ # `RBS::DuplicatedDeclarationError`, collapsing the WHOLE env to nil (every type-of query then
310
+ # degrades to `Dynamic[Top]`). One malformed `.rbs` must not disproportionately blind the analysis: a
311
+ # subclass sig that references an inherited nested type (`class GitAdapter; def x: () ->
312
+ # GitAdapter::Revision`) was the real-world trigger see the 2026-07-04 redmine onboarding note.
307
313
  def append_stub_declarations(base_env, missing)
308
314
  declared = declared_type_names(base_env)
309
315
  names = missing.to_set
@@ -325,9 +331,8 @@ module Rigor
325
331
  nil
326
332
  end
327
333
 
328
- # The `::`-stripped names of every class / module / class-alias
329
- # declaration already present in `env`, so the synthesis paths
330
- # never re-declare (and thereby duplicate) a real declaration.
334
+ # The `::`-stripped names of every class / module / class-alias declaration already present in
335
+ # `env`, so the synthesis paths never re-declare (and thereby duplicate) a real declaration.
331
336
  def declared_type_names(env)
332
337
  names = env.class_decls.keys.map { |n| n.to_s.sub(/\A::/, "") }
333
338
  if env.respond_to?(:class_alias_decls)
@@ -336,15 +341,11 @@ module Rigor
336
341
  names.to_set
337
342
  end
338
343
 
339
- # ADR-32 WD4 — merge synthesised-from-source RBS strings
340
- # into the freshly-built environment. Each entry is a
341
- # `[virtual_filename, rbs_source]` pair. `virtual_filename`
342
- # is purely for diagnostic provenance (RBS parse errors
343
- # cite it) it is not a real file path. Per WD6 the
344
- # synthesizer-emit path is responsible for catching its
345
- # own parse errors and returning `nil` rather than
346
- # garbage; this method assumes its input is parseable
347
- # and only rescues `RBS::ParsingError` as a fail-soft.
344
+ # ADR-32 WD4 — merge synthesised-from-source RBS strings into the freshly-built environment. Each
345
+ # entry is a `[virtual_filename, rbs_source]` pair. `virtual_filename` is purely for diagnostic
346
+ # provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit
347
+ # path is responsible for catching its own parse errors and returning `nil` rather than garbage; this
348
+ # method assumes its input is parseable and only rescues `RBS::ParsingError` as a fail-soft.
348
349
  def add_virtual_rbs(env, virtual_rbs)
349
350
  return if virtual_rbs.nil? || virtual_rbs.empty?
350
351
 
@@ -355,31 +356,27 @@ module Rigor
355
356
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
356
357
  add_parsed_decls(env, buffer, directives, decls)
357
358
  rescue ::RBS::BaseError
358
- # WD6 fail-soft: a single broken virtual RBS contribution
359
- # does not pull the whole env down. The plugin layer
360
- # records a `source-rbs-synthesis-failed` info diagnostic
361
- # in slice 2; here we just skip the entry.
359
+ # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down. The
360
+ # plugin layer records a `source-rbs-synthesis-failed` info diagnostic in slice 2; here we just
361
+ # skip the entry.
362
362
  end
363
363
  end
364
364
 
365
- # Per-gem `data/vendored_gem_sigs/<gem>/` directories that
366
- # ship with Rigor. Each subdirectory is one gem's RBS surface
367
- # (the `<gem>.rbs` file is the typical content; `LICENSE.upstream`
368
- # records provenance). Coverage is deliberately scoped to the
369
- # native-extension and "everywhere in Rails" gems whose absence
370
- # dominated `call.undefined-method` noise in the real-world
371
- # survey at `docs/notes/20260515-real-world-rails-survey.md`.
365
+ # Per-gem `data/vendored_gem_sigs/<gem>/` directories that ship with Rigor. Each subdirectory is one
366
+ # gem's RBS surface (the `<gem>.rbs` file is the typical content; `LICENSE.upstream` records
367
+ # provenance). Coverage is deliberately scoped to the native-extension and "everywhere in Rails" gems
368
+ # whose absence dominated `call.undefined-method` noise in the real-world survey at
369
+ # `docs/notes/20260515-real-world-rails-survey.md`.
372
370
  VENDORED_GEM_SIGS_ROOT = File.expand_path(
373
371
  "../../../data/vendored_gem_sigs",
374
372
  __dir__
375
373
  ).freeze
376
374
  private_constant :VENDORED_GEM_SIGS_ROOT
377
375
 
378
- # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens
379
- # Ruby core classes to add methods upstream `ruby/rbs` omits but
380
- # which every concrete value answers at runtime loaded last so
381
- # upstream always wins on conflict. Public so the cache descriptor
382
- # can digest these files into the env-blob key.
376
+ # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens Ruby core classes to add methods
377
+ # upstream `ruby/rbs` omits but which every concrete value answers at runtime — loaded last so
378
+ # upstream always wins on conflict. Public so the cache descriptor can digest these files into the
379
+ # env-blob key.
383
380
  CORE_OVERLAY_SIGS_ROOT = File.expand_path(
384
381
  "../../../data/core_overlay",
385
382
  __dir__
@@ -391,24 +388,20 @@ module Rigor
391
388
  [Pathname(CORE_OVERLAY_SIGS_ROOT)]
392
389
  end
393
390
 
394
- # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`),
395
- # ADR-72. Unlike the unconditional `core_overlay`, each gem's
396
- # overlay is loaded ONLY when that gem is locked in the
397
- # project's Gemfile.lock but ships no RBS of its own
398
- # {Environment.for_project} decides eligibility and passes the
399
- # already-filtered gem-name set here. One directory per gem name
400
- # keeps the membership check a cheap `File.directory?`.
391
+ # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`), ADR-72. Unlike the unconditional
392
+ # `core_overlay`, each gem's overlay is loaded ONLY when that gem is locked in the project's
393
+ # Gemfile.lock but ships no RBS of its own {Environment.for_project} decides eligibility and passes
394
+ # the already-filtered gem-name set here. One directory per gem name keeps the membership check a
395
+ # cheap `File.directory?`.
401
396
  GEM_OVERLAY_SIGS_ROOT = File.expand_path(
402
397
  "../../../data/gem_overlay",
403
398
  __dir__
404
399
  ).freeze
405
400
 
406
- # @param gem_names [Enumerable<String>] overlay-eligible
407
- # Gemfile.lock gem names (the caller filters to the
408
- # `:missing`-coverage, no-conflicting-plugin set).
409
- # @return [Array<Pathname>] the bundled overlay directory for
410
- # each gem that ships one; empty when none match or the
411
- # overlay root is absent.
401
+ # @param gem_names [Enumerable<String>] overlay-eligible Gemfile.lock gem names (the caller filters
402
+ # to the `:missing`-coverage, no-conflicting-plugin set).
403
+ # @return [Array<Pathname>] the bundled overlay directory for each gem that ships one; empty when
404
+ # none match or the overlay root is absent.
412
405
  def gem_overlay_sig_paths(gem_names)
413
406
  return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
414
407
 
@@ -426,14 +419,10 @@ module Rigor
426
419
  end
427
420
  end
428
421
 
429
- # Gem names whose RBS ships under
430
- # `data/vendored_gem_sigs/<gem>/`. The directory walk is
431
- # the source of truth (the `README.md` sibling is not a
432
- # gem and is excluded). Callers building the RBS env use
433
- # this set to drop the matching `rbs collection install`
434
- # directory before it double-declares against the
435
- # vendored copy — the same hazard `DEFAULT_LIBRARIES`
436
- # creates for stdlib-extracted gems. See
422
+ # Gem names whose RBS ships under `data/vendored_gem_sigs/<gem>/`. The directory walk is the source
423
+ # of truth (the `README.md` sibling is not a gem and is excluded). Callers building the RBS env use
424
+ # this set to drop the matching `rbs collection install` directory before it double-declares against
425
+ # the vendored copy the same hazard `DEFAULT_LIBRARIES` creates for stdlib-extracted gems. See
437
426
  # `RbsCollectionDiscovery`'s `skip_gem_names:`.
438
427
  def vendored_gem_names
439
428
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
@@ -446,48 +435,32 @@ module Rigor
446
435
 
447
436
  attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
448
437
 
449
- # @param libraries [Array<String, Symbol>] stdlib library names to
450
- # load on top of core (e.g., `["pathname", "json"]`). Empty by
451
- # default. Each entry MUST correspond to a directory under the
452
- # `rbs` gem's `stdlib/` tree; unknown names are silently dropped
453
- # on environment build (the underlying `RBS::EnvironmentLoader`
454
- # raises and we fail-soft).
455
- # @param signature_paths [Array<String, Pathname>] additional
456
- # directories of `.rbs` files to load (typically the project's
457
- # `sig/` tree). Non-existent or non-directory paths are filtered
458
- # out at build time so the loader stays robust to fixtures and
459
- # bare repositories.
460
- # @param cache_store [Rigor::Cache::Store, nil] the persistent
461
- # cache the loader threads through to `RbsEnvironment`,
462
- # `RbsKnownClassNames`, `RbsConstantTable`,
463
- # `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
464
- # producers. Pass `nil` (the default) to skip caching; the
465
- # runner threads its own Store through here when enabled.
466
- # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 —
467
- # `[virtual_filename, rbs_source]` pairs synthesised from
468
- # project source by a plugin's
469
- # `Manifest#source_rbs_synthesizer`. Merged into the env
470
- # after `signature_paths:` and the vendored stubs. Pass
471
- # `[]` (the default) when no synthesizer-emitting plugin
472
- # is loaded.
438
+ # @param libraries [Array<String, Symbol>] stdlib library names to load on top of core (e.g.,
439
+ # `["pathname", "json"]`). Empty by default. Each entry MUST correspond to a directory under the
440
+ # `rbs` gem's `stdlib/` tree; unknown names are silently dropped on environment build (the underlying
441
+ # `RBS::EnvironmentLoader` raises and we fail-soft).
442
+ # @param signature_paths [Array<String, Pathname>] additional directories of `.rbs` files to load
443
+ # (typically the project's `sig/` tree). Non-existent or non-directory paths are filtered out at
444
+ # build time so the loader stays robust to fixtures and bare repositories.
445
+ # @param cache_store [Rigor::Cache::Store, nil] the persistent cache the loader threads through to
446
+ # `RbsEnvironment`, `RbsKnownClassNames`, `RbsConstantTable`, `RbsClassAncestorTable`, and
447
+ # `RbsClassTypeParamNames` producers. Pass `nil` (the default) to skip caching; the runner threads
448
+ # its own Store through here when enabled.
449
+ # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
450
+ # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
451
+ # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
452
+ # synthesizer-emitting plugin is loaded.
473
453
  def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [])
474
454
  @libraries = libraries.map(&:to_s).freeze
475
455
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
476
456
  @cache_store = cache_store
477
457
  @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
478
- # Per-loader memoization bucket. Held as a single
479
- # mutable Hash so the loader instance itself can be
480
- # `.freeze`d (per ADR-15 reflection-facade contract)
481
- # without losing the lazy-memo behaviour. Slot names
482
- # currently consulted: `:env`, `:env_loaded`,
483
- # `:env_build_warned`, `:builder`, `:reflection`,
484
- # `:instance_definitions_table`,
485
- # `:singleton_definitions_table`. Constructed via
486
- # `Hash.new` (NOT a `{ ... }` literal) so Rigor's
487
- # `HashShape` narrowing doesn't infer a fixed key set
488
- # from the initial state and fold post-initial slot
489
- # reads (e.g. `@state[:env_loaded]`) to a constant
490
- # `nil`.
458
+ # Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be
459
+ # `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
460
+ # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:builder`, `:reflection`,
461
+ # `:instance_definitions_table`, `:singleton_definitions_table`. Constructed via `Hash.new` (NOT a
462
+ # `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't infer a fixed key set from the initial
463
+ # state and fold post-initial slot reads (e.g. `@state[:env_loaded]`) to a constant `nil`.
491
464
  @state = Hash.new # rubocop:disable Style/EmptyLiteral
492
465
  @instance_definition_cache = {}
493
466
  @singleton_definition_cache = {}
@@ -495,51 +468,41 @@ module Rigor
495
468
  @hierarchy = RbsHierarchy.new(self)
496
469
  end
497
470
 
498
- # The enclosing namespaces {.synthesize_missing_namespaces} had to
499
- # invent because the project's `signature_paths:` RBS declared
500
- # qualified names (`class Foo::Bar`) without ever declaring `Foo`.
501
- # Recovered by scanning the built env for class/module entries
502
- # whose every declaration originated from the synthetic buffer, so
503
- # the answer survives the marshalled-env cache (where no build-time
504
- # collector would). Returns `::`-stripped names, shallowest-first.
505
- # Empty for a well-formed sig set (the common case) and whenever
506
- # the env failed to build.
471
+ # The enclosing namespaces {.synthesize_missing_namespaces} had to invent because the project's
472
+ # `signature_paths:` RBS declared qualified names (`class Foo::Bar`) without ever declaring `Foo`.
473
+ # Recovered by scanning the built env for class/module entries whose every declaration originated from
474
+ # the synthetic buffer, so the answer survives the marshalled-env cache (where no build-time collector
475
+ # would). Returns `::`-stripped names, shallowest-first. Empty for a well-formed sig set (the common
476
+ # case) and whenever the env failed to build.
507
477
  def synthesized_namespaces
508
478
  names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
509
479
  end
510
480
 
511
- # The referenced-but-undeclared types
512
- # {.stub_missing_referenced_types} stubbed so the project classes
513
- # that mention them could build (e.g. an unavailable
514
- # `DRb::DRbServer`, or a stale `Textbringer::EditorError`).
515
- # Recovered off the built env like {#synthesized_namespaces}, so
516
- # it survives the marshalled-env cache.
481
+ # The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
482
+ # that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
483
+ # `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
484
+ # survives the marshalled-env cache.
517
485
  def synthesized_stub_types
518
486
  names_synthesized_in(SYNTHETIC_STUB_BUFFER)
519
487
  end
520
488
 
521
- # Every type name Rigor invented to make an otherwise-inert /
522
- # unbuildable project signature set resolve both the namespace
523
- # stubs and the referenced-type stubs. {MethodDispatcher} resolves
524
- # a call whose receiver is one of these (and that no real
525
- # signature answered) to `Dynamic[Top]`, so the empty stub never
526
- # mis-fires `call.undefined-method`. Memoised; empty (and cheap)
527
- # for the common well-formed sig set.
489
+ # Every type name Rigor invented to make an otherwise-inert / unbuildable project signature set resolve
490
+ # both the namespace stubs and the referenced-type stubs. {MethodDispatcher} resolves a call whose
491
+ # receiver is one of these (and that no real signature answered) to `Dynamic[Top]`, so the empty stub
492
+ # never mis-fires `call.undefined-method`. Memoised; empty (and cheap) for the common well-formed sig
493
+ # set.
528
494
  def synthesized_type_names
529
495
  @state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set
530
496
  end
531
497
 
532
- # Returns true when an RBS class or module declaration with the given
533
- # name is loaded. Accepts unprefixed or top-level-prefixed names
534
- # ("Integer" or "::Integer"). Memoized per-name (positive and
498
+ # Returns true when an RBS class or module declaration with the given name is loaded. Accepts
499
+ # unprefixed or top-level-prefixed names ("Integer" or "::Integer"). Memoized per-name (positive and
535
500
  # negative results both cache).
536
501
  #
537
- # When `cache_store` is set, the loader fetches the entire set of
538
- # known class / module / alias names once (per process) through
539
- # {Cache::RbsKnownClassNames.fetch} and answers `class_known?`
540
- # from the in-memory Set. Cold runs pay a single env walk and
541
- # persist the result; warm runs (and a separate loader sharing
542
- # the same Store) skip the env walk entirely.
502
+ # When `cache_store` is set, the loader fetches the entire set of known class / module / alias names
503
+ # once (per process) through {Cache::RbsKnownClassNames.fetch} and answers `class_known?` from the
504
+ # in-memory Set. Cold runs pay a single env walk and persist the result; warm runs (and a separate
505
+ # loader sharing the same Store) skip the env walk entirely.
543
506
  def class_known?(name)
544
507
  key = name.to_s
545
508
  return @class_known_cache[key] if @class_known_cache.key?(key)
@@ -551,16 +514,14 @@ module Rigor
551
514
  end
552
515
  end
553
516
 
554
- # Returns true when the named RBS declaration is a Module
555
- # (`RBS::AST::Declarations::Module`) rather than a Class. The
556
- # `user_class_fallback_receiver` tier consults this to route
557
- # `Nominal[M].some_kernel_method` (where M is a module mixin
558
- # like `PP::ObjectMixin`) through the `Nominal[Object]`
559
- # fallback, because every concrete includer of M sees Kernel
560
- # / Object instance methods as part of its own ancestor chain.
517
+ # Returns true when the named RBS declaration is a Module (`RBS::AST::Declarations::Module`) rather
518
+ # than a Class. The `user_class_fallback_receiver` tier consults this to route
519
+ # `Nominal[M].some_kernel_method` (where M is a module mixin like `PP::ObjectMixin`) through the
520
+ # `Nominal[Object]` fallback, because every concrete includer of M sees Kernel / Object instance
521
+ # methods as part of its own ancestor chain.
561
522
  #
562
- # Returns false for classes, for unknown names, and when the
563
- # RBS environment failed to build (fail-soft).
523
+ # Returns false for classes, for unknown names, and when the RBS environment failed to build
524
+ # (fail-soft).
564
525
  def rbs_module?(name)
565
526
  return false if env.nil?
566
527
 
@@ -573,10 +534,9 @@ module Rigor
573
534
  false
574
535
  end
575
536
 
576
- # Yields every known class / module / alias name (top-level
577
- # prefixed) currently loaded into the environment. The cache
578
- # producer that materialises the known-name set uses this so
579
- # it never recurses back through {#class_known?}.
537
+ # Yields every known class / module / alias name (top-level prefixed) currently loaded into the
538
+ # environment. The cache producer that materialises the known-name set uses this so it never recurses
539
+ # back through {#class_known?}.
580
540
  def each_known_class_name
581
541
  return enum_for(:each_known_class_name) unless block_given?
582
542
  return if env.nil?
@@ -584,22 +544,16 @@ module Rigor
584
544
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
585
545
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
586
546
  rescue ::RBS::BaseError
587
- # fail-soft: a broken RBS environment yields no names.
588
- # Analyzer-internal errors (NameError, NoMethodError,
589
- # LoadError) are NOT swallowed those are bugs and
590
- # must surface so they don't hide silently the way the
591
- # v0.0.9 cache `Cache::Descriptor` regression did.
547
+ # fail-soft: a broken RBS environment yields no names. Analyzer-internal errors (NameError,
548
+ # NoMethodError, LoadError) are NOT swallowed — those are bugs and must surface so they don't hide
549
+ # silently the way the v0.0.9 cache `Cache::Descriptor` regression did.
592
550
  end
593
551
 
594
- # ADR-20 slice 2e — iterates over every `%a{...}`
595
- # annotation attached to a class- or module-level
596
- # declaration in the loaded RBS environment, yielding
597
- # `(annotation_string, source_location)` pairs. Used by
598
- # {Rigor::Inference::HktRegistry.scan_rbs_loader} to
599
- # find `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
600
- # directives in user-authored overlays and merge them
601
- # into the per-`Environment` HKT registry. Yields nothing
602
- # when the env failed to build (fail-soft, same shape as
552
+ # ADR-20 slice 2e — iterates over every `%a{...}` annotation attached to a class- or module-level
553
+ # declaration in the loaded RBS environment, yielding `(annotation_string, source_location)` pairs.
554
+ # Used by {Rigor::Inference::HktRegistry.scan_rbs_loader} to find `rigor:v1:hkt_register` /
555
+ # `rigor:v1:hkt_define` directives in user-authored overlays and merge them into the per-`Environment`
556
+ # HKT registry. Yields nothing when the env failed to build (fail-soft, same shape as
603
557
  # {#each_known_class_name}).
604
558
  def each_class_decl_annotation
605
559
  return enum_for(:each_class_decl_annotation) unless block_given?
@@ -613,20 +567,15 @@ module Rigor
613
567
  end
614
568
  end
615
569
  rescue ::RBS::BaseError, ::Ractor::IsolationError
616
- # fail-soft: matches each_known_class_name's policy.
617
- # Ractor::IsolationError surfaces when the scan is
618
- # invoked from a non-main Ractor pool worker before
619
- # ADR-15's full deep-freeze migration completes — the
620
- # worker falls back to the base (builtins-only)
621
- # registry rather than crashing.
570
+ # fail-soft: matches each_known_class_name's policy. Ractor::IsolationError surfaces when the scan
571
+ # is invoked from a non-main Ractor pool worker before ADR-15's full deep-freeze migration completes
572
+ # the worker falls back to the base (builtins-only) registry rather than crashing.
622
573
  end
623
574
 
624
- # Like {#each_class_decl_annotation}, but also yields the
625
- # owning class / module's RBS name as the first block
626
- # argument: `(class_name, annotation_string, location)`. Used
627
- # by {Rigor::RbsExtended::ConformanceChecker} to resolve a
628
- # `rigor:v1:conforms-to` directive back to the class it
629
- # annotates. Same fail-soft policy as the un-named variant.
575
+ # Like {#each_class_decl_annotation}, but also yields the owning class / module's RBS name as the
576
+ # first block argument: `(class_name, annotation_string, location)`. Used by
577
+ # {Rigor::RbsExtended::ConformanceChecker} to resolve a `rigor:v1:conforms-to` directive back to the
578
+ # class it annotates. Same fail-soft policy as the un-named variant.
630
579
  def each_class_decl_annotation_with_name
631
580
  return enum_for(:each_class_decl_annotation_with_name) unless block_given?
632
581
  return if env.nil?
@@ -642,16 +591,12 @@ module Rigor
642
591
  # fail-soft: see #each_class_decl_annotation.
643
592
  end
644
593
 
645
- # Returns a frozen `Hash<String, String>` mapping each loaded
646
- # class / module name (top-level prefixed) to the file path of
647
- # its FIRST declaration's RBS source. Used by
648
- # {Rigor::Analysis::RunStats} to attribute the type universe
649
- # between "project sig/" (paths under the configured
650
- # `signature_paths`) and "bundled" (everything else RBS
651
- # core, stdlib libraries, gem-bundled RBS). Each value is a
652
- # frozen `String` so the whole result is `Ractor.shareable?`
653
- # — the Phase 4b worker pool ships a snapshot back to the
654
- # coordinator on the first `:prepare` message.
594
+ # Returns a frozen `Hash<String, String>` mapping each loaded class / module name (top-level prefixed)
595
+ # to the file path of its FIRST declaration's RBS source. Used by {Rigor::Analysis::RunStats} to
596
+ # attribute the type universe between "project sig/" (paths under the configured `signature_paths`)
597
+ # and "bundled" (everything else RBS core, stdlib libraries, gem-bundled RBS). Each value is a frozen
598
+ # `String` so the whole result is `Ractor.shareable?` — the Phase 4b worker pool ships a snapshot back
599
+ # to the coordinator on the first `:prepare` message.
655
600
  def class_decl_paths
656
601
  return {}.freeze if env.nil?
657
602
 
@@ -674,17 +619,14 @@ module Rigor
674
619
  {}.freeze
675
620
  end
676
621
 
677
- # @return [RBS::Definition, nil] the resolved instance definition
678
- # for `class_name`, or nil when the class is unknown or its
679
- # definition cannot be built (RBS may raise on broken hierarchies;
680
- # we fail-soft and return nil so the caller can fall back).
622
+ # @return [RBS::Definition, nil] the resolved instance definition for `class_name`, or nil when the
623
+ # class is unknown or its definition cannot be built (RBS may raise on broken hierarchies; we
624
+ # fail-soft and return nil so the caller can fall back).
681
625
  #
682
- # Built on demand from the (possibly cache-loaded) env; the
683
- # in-memory `@instance_definition_cache` keeps the per-process
684
- # short-circuit. ADR-54 WD1 retired the definitions disk blob:
685
- # given a cached env, `Marshal.load`-ing every definition was
686
- # measurably slower (and allocation-heavier) than rebuilding
687
- # the ones a run actually touches.
626
+ # Built on demand from the (possibly cache-loaded) env; the in-memory `@instance_definition_cache`
627
+ # keeps the per-process short-circuit. ADR-54 WD1 retired the definitions disk blob: given a cached
628
+ # env, `Marshal.load`-ing every definition was measurably slower (and allocation-heavier) than
629
+ # rebuilding the ones a run actually touches.
688
630
  def instance_definition(class_name)
689
631
  key = class_name.to_s
690
632
  return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
@@ -700,13 +642,10 @@ module Rigor
700
642
  definition.methods[method_name.to_sym]
701
643
  end
702
644
 
703
- # @return [Array<Symbol>, nil] every instance-method name on
704
- # `class_name`own, inherited, and included as resolved
705
- # by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when
706
- # the class definition cannot be built so callers can tell
707
- # "no methods" apart from "unknown class". Used by the
708
- # `rigor:v1:conforms-to` presence check
709
- # ({Rigor::RbsExtended::ConformanceChecker}).
645
+ # @return [Array<Symbol>, nil] every instance-method name on `class_name` — own, inherited, and
646
+ # includedas resolved by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when the class
647
+ # definition cannot be built so callers can tell "no methods" apart from "unknown class". Used by the
648
+ # `rigor:v1:conforms-to` presence check ({Rigor::RbsExtended::ConformanceChecker}).
710
649
  def instance_method_names(class_name)
711
650
  definition = instance_definition(class_name)
712
651
  return nil unless definition
@@ -714,12 +653,10 @@ module Rigor
714
653
  definition.methods.keys
715
654
  end
716
655
 
717
- # @return [RBS::Definition, nil] the built definition for the RBS
718
- # interface `interface_name` (`_RewindableStream`), whose `.methods`
719
- # are the required members (including interface-ancestor members).
720
- # Returns `nil` when the name does not resolve to a loaded interface
721
- # (a typo, or the defining library / sig set is not on the load
722
- # path). Fail-soft on RBS build errors.
656
+ # @return [RBS::Definition, nil] the built definition for the RBS interface `interface_name`
657
+ # (`_RewindableStream`), whose `.methods` are the required members (including interface-ancestor
658
+ # members). Returns `nil` when the name does not resolve to a loaded interface (a typo, or the
659
+ # defining library / sig set is not on the load path). Fail-soft on RBS build errors.
723
660
  def interface_definition(interface_name)
724
661
  rbs_name = parse_type_name(interface_name)
725
662
  return nil unless rbs_name
@@ -731,24 +668,20 @@ module Rigor
731
668
  nil
732
669
  end
733
670
 
734
- # @return [Array<Symbol>, nil] every method name required by the RBS
735
- # interface `interface_name`, or nil when it does not resolve. Thin
736
- # accessor over {#interface_definition} for the presence check.
671
+ # @return [Array<Symbol>, nil] every method name required by the RBS interface `interface_name`, or nil
672
+ # when it does not resolve. Thin accessor over {#interface_definition} for the presence check.
737
673
  def interface_method_names(interface_name)
738
674
  interface_definition(interface_name)&.methods&.keys
739
675
  end
740
676
 
741
- # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`,
742
- # `int`, `range[int?]`, …) appearing in a method signature.
743
- # @return [RBS::Types::t, nil] the alias's aliased type one level out,
744
- # with type arguments substituted for a generic alias (`string` →
745
- # `::String | ::_ToStr`; `range[int?]` `::Range[int?] |
746
- # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see
747
- # through the alias that {Inference::RbsTypeTranslator} otherwise
748
- # degrades to `untyped`, which is why an interface/alias parameter
749
- # does not reject `nil`. `expand_alias2` handles the (rarer) generic
750
- # case — a `range[T]` param previously fell back to "admits", which
751
- # suppressed e.g. `MatchData#[](nil)`.
677
+ # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`, `int`, `range[int?]`, …)
678
+ # appearing in a method signature.
679
+ # @return [RBS::Types::t, nil] the alias's aliased type one level out, with type arguments substituted
680
+ # for a generic alias (`string` → `::String | ::_ToStr`; `range[int?]` → `::Range[int?] |
681
+ # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see through the alias that
682
+ # {Inference::RbsTypeTranslator} otherwise degrades to `untyped`, which is why an interface/alias
683
+ # parameter does not reject `nil`. `expand_alias2` handles the (rarer) generic case — a `range[T]`
684
+ # param previously fell back to "admits", which suppressed e.g. `MatchData#[](nil)`.
752
685
  def expand_type_alias(rbs_alias)
753
686
  return nil if env.nil?
754
687
 
@@ -761,15 +694,13 @@ module Rigor
761
694
  nil
762
695
  end
763
696
 
764
- # @return [RBS::Definition, nil] the resolved singleton (class
765
- # object) definition for `class_name`. The methods on this
766
- # definition are the *class methods* of `class_name`, including
767
- # those inherited from `Class` and `Module` for class types.
768
- # Returns nil for unknown names and on RBS build errors (fail-soft).
697
+ # @return [RBS::Definition, nil] the resolved singleton (class object) definition for `class_name`. The
698
+ # methods on this definition are the *class methods* of `class_name`, including those inherited from
699
+ # `Class` and `Module` for class types. Returns nil for unknown names and on RBS build errors
700
+ # (fail-soft).
769
701
  #
770
- # Built on demand from the env with a per-process memo; the
771
- # same on-demand discipline as {#instance_definition} (ADR-54
772
- # WD1).
702
+ # Built on demand from the env with a per-process memo; the same on-demand discipline as
703
+ # {#instance_definition} (ADR-54 WD1).
773
704
  def singleton_definition(class_name)
774
705
  key = class_name.to_s
775
706
  return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
@@ -777,11 +708,10 @@ module Rigor
777
708
  @singleton_definition_cache[key] = build_singleton_definition(class_name)
778
709
  end
779
710
 
780
- # @return [RBS::Definition::Method, nil] the class method on
781
- # `class_name`. For example, `singleton_method(class_name:
782
- # "Integer", method_name: :sqrt)` returns the definition for
783
- # `Integer.sqrt`, while `singleton_method(class_name: "Foo",
784
- # method_name: :new)` returns Class#new for any class type.
711
+ # @return [RBS::Definition::Method, nil] the class method on `class_name`. For example,
712
+ # `singleton_method(class_name: "Integer", method_name: :sqrt)` returns the definition for
713
+ # `Integer.sqrt`, while `singleton_method(class_name: "Foo", method_name: :new)` returns Class#new
714
+ # for any class type.
785
715
  def singleton_method(class_name:, method_name:)
786
716
  definition = singleton_definition(class_name)
787
717
  return nil unless definition
@@ -789,25 +719,19 @@ module Rigor
789
719
  definition.methods[method_name.to_sym]
790
720
  end
791
721
 
792
- # Slice 4 phase 2d. Returns the class's declared type-parameter
793
- # names as Symbols (e.g., `[:Elem]` for `Array`, `[:K, :V]` for
794
- # `Hash`). Used by the dispatcher to build the substitution map
795
- # from receiver `type_args` into the method's return type. The
796
- # instance definition is the canonical source because singleton
797
- # methods (e.g., `Array.new`) parameterize over the same `Elem`
798
- # as instance methods.
722
+ # Slice 4 phase 2d. Returns the class's declared type-parameter names as Symbols (e.g., `[:Elem]` for
723
+ # `Array`, `[:K, :V]` for `Hash`). Used by the dispatcher to build the substitution map from receiver
724
+ # `type_args` into the method's return type. The instance definition is the canonical source because
725
+ # singleton methods (e.g., `Array.new`) parameterize over the same `Elem` as instance methods.
799
726
  #
800
- # Returns an empty array for non-generic classes and for unknown
801
- # names (the loader stays fail-soft). NOTE: in the `rbs` gem,
802
- # `RBS::Definition#type_params` returns `Array<Symbol>` directly,
803
- # not the AST `TypeParam` object (those live on the AST level).
727
+ # Returns an empty array for non-generic classes and for unknown names (the loader stays fail-soft).
728
+ # NOTE: in the `rbs` gem, `RBS::Definition#type_params` returns `Array<Symbol>` directly, not the AST
729
+ # `TypeParam` object (those live on the AST level).
804
730
  #
805
- # When `cache_store` is set, the loader fetches the entire
806
- # type-parameter-name table once (per process) through
807
- # {Cache::RbsClassTypeParamNames.fetch} and answers point
808
- # lookups from it. Cold runs build the table once and persist
809
- # it; warm runs (and a separate loader sharing the same Store)
810
- # skip the env walk entirely.
731
+ # When `cache_store` is set, the loader fetches the entire type-parameter-name table once (per
732
+ # process) through {Cache::RbsClassTypeParamNames.fetch} and answers point lookups from it. Cold runs
733
+ # build the table once and persist it; warm runs (and a separate loader sharing the same Store) skip
734
+ # the env walk entirely.
811
735
  def class_type_param_names(class_name)
812
736
  if cache_store
813
737
  key = class_name.to_s.delete_prefix("::")
@@ -824,11 +748,9 @@ module Rigor
824
748
  @hierarchy.class_ordering(lhs, rhs)
825
749
  end
826
750
 
827
- # @return [Array<String>] every RBS-declared constant name
828
- # (top-level prefixed, e.g., `"::Math::PI"`) currently loaded
829
- # into the environment. Used by the cache producer that
830
- # materialises the constant-type table; ordinary callers
831
- # should keep using {#constant_type} for point lookups.
751
+ # @return [Array<String>] every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
752
+ # currently loaded into the environment. Used by the cache producer that materialises the
753
+ # constant-type table; ordinary callers should keep using {#constant_type} for point lookups.
832
754
  def constant_names
833
755
  return [] if env.nil?
834
756
 
@@ -837,11 +759,9 @@ module Rigor
837
759
  []
838
760
  end
839
761
 
840
- # Yields `(name, entry)` for every RBS constant declaration
841
- # currently loaded into the environment. The cache producer
842
- # uses this to materialise the constant-type table without
843
- # going back through {#constant_type} (which would recurse
844
- # back into the cache when `cache_store` is set).
762
+ # Yields `(name, entry)` for every RBS constant declaration currently loaded into the environment. The
763
+ # cache producer uses this to materialise the constant-type table without going back through
764
+ # {#constant_type} (which would recurse back into the cache when `cache_store` is set).
845
765
  def each_constant_decl
846
766
  return enum_for(:each_constant_decl) unless block_given?
847
767
  return if env.nil?
@@ -853,22 +773,16 @@ module Rigor
853
773
  # fail-soft: a broken RBS environment yields no entries.
854
774
  end
855
775
 
856
- # Slice A constant-value lookup. Returns the translated
857
- # `Rigor::Type` for a non-class constant declaration
858
- # (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or
859
- # `nil` when no constant entry exists for `name` in the
860
- # loaded RBS environment. Callers MUST treat the return
861
- # value as authoritative when present and as "unknown" when
862
- # nil; the loader does NOT consult the class declarations
863
- # here — class objects are still resolved through
864
- # {#class_known?} and `Environment#singleton_for_name`.
776
+ # Slice A constant-value lookup. Returns the translated `Rigor::Type` for a non-class constant
777
+ # declaration (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or `nil` when no constant entry
778
+ # exists for `name` in the loaded RBS environment. Callers MUST treat the return value as authoritative
779
+ # when present and as "unknown" when nil; the loader does NOT consult the class declarations here —
780
+ # class objects are still resolved through {#class_known?} and `Environment#singleton_for_name`.
865
781
  #
866
- # When `cache_store` is set, the loader fetches the entire
867
- # translated constant table once (per process) through
868
- # {Cache::RbsConstantTable.fetch} and answers point lookups
869
- # from it. Cold runs pay the translation cost up-front and
870
- # write the result to disk; warm runs skip the translation
871
- # entirely and pay only a `Marshal.load` of the table.
782
+ # When `cache_store` is set, the loader fetches the entire translated constant table once (per
783
+ # process) through {Cache::RbsConstantTable.fetch} and answers point lookups from it. Cold runs pay
784
+ # the translation cost up-front and write the result to disk; warm runs skip the translation entirely
785
+ # and pay only a `Marshal.load` of the table.
872
786
  def constant_type(name)
873
787
  rbs_name = parse_type_name(name)
874
788
  return nil unless rbs_name
@@ -882,27 +796,18 @@ module Rigor
882
796
  nil
883
797
  end
884
798
 
885
- # ADR-15 Phase 4b.x — eagerly drives every cached
886
- # producer (plus the eager definitions tables, computed
887
- # from the cached env since ADR-54 WD1) so a subsequent
888
- # worker Ractor can serve all of its RBS queries without
889
- # ever calling `RBS::EnvironmentLoader.new`.
890
- # The loader path that calls `EnvironmentLoader.new`
891
- # transitively reads a chain of non-`Ractor.shareable?`
892
- # module constants
893
- # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
894
- # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
895
- # `Gem::Requirement::DefaultRequirement`, …) and trips
896
- # `Ractor::IsolationError`. Pre-warming on the main
897
- # Ractor — env blob loaded, derived tables built — keeps
898
- # workers off that chain (`RBS::DefinitionBuilder` over
899
- # an already-built env does not touch it).
799
+ # ADR-15 Phase 4b.x — eagerly drives every cached producer (plus the eager definitions tables, computed
800
+ # from the cached env since ADR-54 WD1) so a subsequent worker Ractor can serve all of its RBS queries
801
+ # without ever calling `RBS::EnvironmentLoader.new`. The loader path that calls
802
+ # `EnvironmentLoader.new` transitively reads a chain of non-`Ractor.shareable?` module constants
803
+ # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`, `RBS::Repository::DEFAULT_STDLIB_ROOT`,
804
+ # `Gem::Requirement::DefaultRequirement`, …) and trips `Ractor::IsolationError`. Pre-warming on the
805
+ # main Ractor env blob loaded, derived tables built — keeps workers off that chain
806
+ # (`RBS::DefinitionBuilder` over an already-built env does not touch it).
900
807
  #
901
- # No-op when `cache_store` is nil — without a Store the
902
- # worker has no choice but to build env via the loader,
903
- # so the caller MUST ensure pool mode runs with caching
904
- # enabled. Returns `self` so the call chains cleanly
905
- # from the `Runner` pre-spawn hook.
808
+ # No-op when `cache_store` is nil — without a Store the worker has no choice but to build env via the
809
+ # loader, so the caller MUST ensure pool mode runs with caching enabled. Returns `self` so the call
810
+ # chains cleanly from the `Runner` pre-spawn hook.
906
811
  def prewarm
907
812
  return self if cache_store.nil?
908
813
 
@@ -916,13 +821,10 @@ module Rigor
916
821
  self
917
822
  end
918
823
 
919
- # ADR-54 WD4 — the shared cache descriptor for every RBS-derived
920
- # producer consulting this loader. Building it digests every
921
- # `.rbs` file under `signature_paths` + the vendored gem sigs,
922
- # and the result is identical across producers, so one build is
923
- # memoised per loader (on `@state`, alongside `:env` — the env
924
- # itself is loader-lifetime-memoised, so this adds no new
925
- # staleness class).
824
+ # ADR-54 WD4 — the shared cache descriptor for every RBS-derived producer consulting this loader.
825
+ # Building it digests every `.rbs` file under `signature_paths` + the vendored gem sigs, and the
826
+ # result is identical across producers, so one build is memoised per loader (on `@state`, alongside
827
+ # `:env` the env itself is loader-lifetime-memoised, so this adds no new staleness class).
926
828
  def rbs_cache_descriptor
927
829
  @state[:rbs_cache_descriptor] ||= begin
928
830
  require_relative "../cache/rbs_descriptor"
@@ -930,19 +832,14 @@ module Rigor
930
832
  end
931
833
  end
932
834
 
933
- # ADR-15 Phase 2b — return the loader's read-only
934
- # query surface as a frozen, `Ractor.shareable?`
935
- # {Reflection} value object. Built lazily on first
936
- # access; the loader memoises so repeated calls return
937
- # the same instance.
835
+ # ADR-15 Phase 2b — return the loader's read-only query surface as a frozen, `Ractor.shareable?`
836
+ # {Reflection} value object. Built lazily on first access; the loader memoises so repeated calls
837
+ # return the same instance.
938
838
  #
939
- # The Reflection consumes the loader's already-warmed
940
- # cache producers (or, when no `cache_store` is set,
941
- # eagerly walks the env). Once constructed, the
942
- # Reflection carries the derived tables independently
943
- # and never re-consults the loader — making it safe to
944
- # share across Ractors while the loader stays per-
945
- # process / per-Ractor for write-path operations.
839
+ # The Reflection consumes the loader's already-warmed cache producers (or, when no `cache_store` is
840
+ # set, eagerly walks the env). Once constructed, the Reflection carries the derived tables
841
+ # independently and never re-consults the loader making it safe to share across Ractors while the
842
+ # loader stays per- process / per-Ractor for write-path operations.
946
843
  def reflection
947
844
  @state[:reflection] ||= begin
948
845
  require_relative "reflection"
@@ -959,11 +856,10 @@ module Rigor
959
856
 
960
857
  private
961
858
 
962
- # The `::`-stripped names of every class/module entry whose
963
- # declarations ALL originated from the given sentinel buffer
964
- # i.e. names Rigor synthesized, not names the project declared.
965
- # Reads off the built env so the answer survives the marshalled
966
- # env cache; shallowest-first. Empty when the env failed to build.
859
+ # The `::`-stripped names of every class/module entry whose declarations ALL originated from the given
860
+ # sentinel buffer i.e. names Rigor synthesized, not names the project declared. Reads off the built
861
+ # env so the answer survives the marshalled env cache; shallowest-first. Empty when the env failed to
862
+ # build.
967
863
  def names_synthesized_in(buffer_name)
968
864
  e = env
969
865
  return [] if e.nil?
@@ -978,13 +874,11 @@ module Rigor
978
874
  names.sort_by { |name| name.count("::") }
979
875
  end
980
876
 
981
- # Collects the AST declaration nodes behind a `class_decls`
982
- # entry across the supported RBS range (`rbs >= 3.0, < 5.0`).
983
- # RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding
984
- # bare AST declarations; RBS 3.x exposes `decls`, an array of
985
- # `MultiEntry::D` wrappers whose `#decl` is the AST declaration.
986
- # The single-`decl` shape is handled defensively so the loader
987
- # survives an rbs-gem minor bump.
877
+ # Collects the AST declaration nodes behind a `class_decls` entry across the supported RBS range (`rbs
878
+ # >= 3.0, < 5.0`). RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding bare AST
879
+ # declarations; RBS 3.x exposes `decls`, an array of `MultiEntry::D` wrappers whose `#decl` is the AST
880
+ # declaration. The single-`decl` shape is handled defensively so the loader survives an rbs-gem minor
881
+ # bump.
988
882
  def entry_declarations(entry)
989
883
  if entry.respond_to?(:each_decl)
990
884
  [].tap { |acc| entry.each_decl { |decl| acc << decl } }
@@ -997,9 +891,8 @@ module Rigor
997
891
  end
998
892
  end
999
893
 
1000
- # True when an AST declaration was emitted into `buffer_name`
1001
- # (one of the synthetic-source sentinels) identified by the
1002
- # buffer name on its location.
894
+ # True when an AST declaration was emitted into `buffer_name` (one of the synthetic-source sentinels)
895
+ # identified by the buffer name on its location.
1003
896
  def synthetic_decl?(decl, buffer_name)
1004
897
  location = decl.respond_to?(:location) ? decl.location : nil
1005
898
  location&.buffer&.name.to_s == buffer_name
@@ -1026,23 +919,19 @@ module Rigor
1026
919
  end
1027
920
  end
1028
921
 
1029
- # ADR-15 Phase 2b — the `Reflection` build path
1030
- # consumes these tables even when `cache_store` is nil
1031
- # (e.g. tests that build a `Reflection` without a
1032
- # persistent cache). The helper routes through the
1033
- # producer's `.fetch` when a store IS available, and
1034
- # falls back to the producer's `.compute` otherwise.
922
+ # ADR-15 Phase 2b — the `Reflection` build path consumes these tables even when `cache_store` is nil
923
+ # (e.g. tests that build a `Reflection` without a persistent cache). The helper routes through the
924
+ # producer's `.fetch` when a store IS available, and falls back to the producer's `.compute`
925
+ # otherwise.
1035
926
  def fetch_or_compute_producer(producer)
1036
927
  return producer.fetch(loader: self, store: cache_store) if cache_store
1037
928
 
1038
929
  producer.send(:compute, self)
1039
930
  end
1040
931
 
1041
- # ADR-15 Phase 2b — `Hash<String, Array<String>>` of
1042
- # normalised ancestor chains per class. Consumes the
1043
- # existing `RbsClassAncestorTable` producer when
1044
- # `cache_store` is set; falls back to the producer's
1045
- # `compute` otherwise. Used by {#reflection}.
932
+ # ADR-15 Phase 2b — `Hash<String, Array<String>>` of normalised ancestor chains per class. Consumes
933
+ # the existing `RbsClassAncestorTable` producer when `cache_store` is set; falls back to the
934
+ # producer's `compute` otherwise. Used by {#reflection}.
1046
935
  def ancestor_names_table
1047
936
  @ancestor_names_table ||= begin
1048
937
  require_relative "../cache/rbs_class_ancestor_table"
@@ -1069,27 +958,50 @@ module Rigor
1069
958
  translated unless translated.is_a?(Type::Bot)
1070
959
  end
1071
960
 
1072
- # The RBS environment for this loader. Memoised both on
1073
- # success AND on failure: when the env build raises
1074
- # (typically `RBS::DuplicatedDeclarationError` because a
1075
- # `signature_paths:` entry redeclares a constant or class
1076
- # already shipped by stdlib RBS), retrying on every
1077
- # subsequent `env` call would re-parse and re-resolve the
1078
- # whole sig set per AST node touched during analysis,
1079
- # multiplying per-file analysis cost by ~100x. Failures
1080
- # short-circuit to `nil` here and are surfaced to the user
1081
- # via `warn_about_env_build_failure_once` so the broken
1082
- # `signature_paths:` entry is identifiable.
961
+ # The RBS environment for this loader. Memoised both on success AND on failure: when the env build
962
+ # raises (typically `RBS::DuplicatedDeclarationError` because a `signature_paths:` entry redeclares a
963
+ # constant or class already shipped by stdlib RBS), retrying on every subsequent `env` call would
964
+ # re-parse and re-resolve the whole sig set per AST node touched during analysis, multiplying per-file
965
+ # analysis cost by ~100x. Failures short-circuit to `nil` here and are surfaced to the user via
966
+ # `warn_about_env_build_failure_once` so the broken `signature_paths:` entry is identifiable.
1083
967
  def env
1084
968
  return @state[:env] if @state[:env_loaded]
1085
969
 
1086
970
  @state[:env_loaded] = true
1087
971
  @state[:env] = cache_store ? cached_env : build_env
972
+ warn_about_quarantined_signatures
973
+ @state[:env]
1088
974
  rescue ::RBS::BaseError => e
1089
975
  warn_about_env_build_failure_once(e)
1090
976
  @state[:env] = nil
1091
977
  end
1092
978
 
979
+ # Tell the user, once per run, which of their `signature_paths:` `.rbs` files were skipped because they
980
+ # do not parse ({RbsLoader.add_project_signatures} quarantines them so the rest of the env survives). A
981
+ # skipped file's types are absent, so calls into them read `Dynamic[top]` — silently, without this. This
982
+ # is the visibility half of the fix: a shrinking diagnostic count must never be mistaken for a clean run
983
+ # when it actually means "your sig/ stopped loading". No-op when `signature_paths:` is empty (the cost is
984
+ # then a single empty-set check) or every file parses.
985
+ def warn_about_quarantined_signatures
986
+ return if @state[:quarantine_warned]
987
+
988
+ quarantined = self.class.quarantined_project_signatures(@signature_paths)
989
+ return if quarantined.empty?
990
+
991
+ @state[:quarantine_warned] = true
992
+ listed = quarantined.first(QUARANTINE_WARN_LIMIT)
993
+ more = quarantined.size - listed.size
994
+ lines = listed.map { |_path, first_line| " - #{first_line}" }
995
+ lines << " … and #{more} more" if more.positive?
996
+ warn(
997
+ "rigor: skipped #{quarantined.size} unparseable RBS file(s) under `signature_paths:`.\n " \
998
+ "They were QUARANTINED so the rest of your RBS env still loads, but the types they\n " \
999
+ "declare are absent — calls into them read `Dynamic[top]`, so coverage and diagnostics\n " \
1000
+ "are reduced. Fix the parse error(s) to restore that coverage:\n" \
1001
+ "#{lines.join("\n")}"
1002
+ )
1003
+ end
1004
+
1093
1005
  def warn_about_env_build_failure_once(error)
1094
1006
  return if @state[:env_build_warned]
1095
1007
 
@@ -1111,15 +1023,12 @@ module Rigor
1111
1023
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
1112
1024
  end
1113
1025
 
1114
- # Full `Hash<String, RBS::Definition>` tables for the
1115
- # {#prewarm} / {#reflection} consumers (ADR-15 Phase 2b/4b.x),
1116
- # which need every definition materialised up front. Built
1117
- # from the (cached) env via `RBS::DefinitionBuilder` ADR-54
1118
- # WD1 retired the disk blobs these used to `Marshal.load`
1119
- # (building all definitions from a cached env is faster), so
1120
- # the eager-table cost is now a compute, not a load. Keys stay
1121
- # in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`)
1122
- # — the shape {Environment::Reflection} documents.
1026
+ # Full `Hash<String, RBS::Definition>` tables for the {#prewarm} / {#reflection} consumers (ADR-15
1027
+ # Phase 2b/4b.x), which need every definition materialised up front. Built from the (cached) env via
1028
+ # `RBS::DefinitionBuilder` ADR-54 WD1 retired the disk blobs these used to `Marshal.load` (building
1029
+ # all definitions from a cached env is faster), so the eager-table cost is now a compute, not a load.
1030
+ # Keys stay in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`) — the shape
1031
+ # {Environment::Reflection} documents.
1123
1032
  def instance_definitions_table
1124
1033
  @state[:instance_definitions_table] ||= build_definitions_table do |name|
1125
1034
  build_instance_definition(name)
@@ -1179,15 +1088,12 @@ module Rigor
1179
1088
  nil
1180
1089
  end
1181
1090
 
1182
- # Resolve an RBS class/module ALIAS to its canonical declared name.
1183
- # `class Mutex = Thread::Mutex` lives only in `class_alias_decls`, so
1184
- # `class_known?` reports it (it checks that table) but the definition
1185
- # builder which only knows `class_decls` could not enumerate its
1186
- # methods, leaving alias classes (`Mutex`, and any `X = Y`) with no
1187
- # resolvable method surface. Normalising via the env (RBS's own alias
1188
- # resolution) before the `class_decls` guard fixes dispatch AND the
1189
- # `call.undefined-method` existence check on them. A non-alias name, or
1190
- # one that does not normalise, is returned unchanged.
1091
+ # Resolve an RBS class/module ALIAS to its canonical declared name. `class Mutex = Thread::Mutex`
1092
+ # lives only in `class_alias_decls`, so `class_known?` reports it (it checks that table) but the
1093
+ # definition builder — which only knows `class_decls` could not enumerate its methods, leaving alias
1094
+ # classes (`Mutex`, and any `X = Y`) with no resolvable method surface. Normalising via the env (RBS's
1095
+ # own alias resolution) before the `class_decls` guard fixes dispatch AND the `call.undefined-method`
1096
+ # existence check on them. A non-alias name, or one that does not normalise, is returned unchanged.
1191
1097
  def canonical_module_name(rbs_name)
1192
1098
  return rbs_name unless env.class_alias_decls.key?(rbs_name)
1193
1099
 
@@ -1196,14 +1102,12 @@ module Rigor
1196
1102
  rbs_name
1197
1103
  end
1198
1104
 
1199
- # Memoised on `@state` (the per-loader store also holding `:env` /
1200
- # `:builder`): `RBS::TypeName.parse` is a pure, deterministic
1201
- # function of the normalised string, and the `RBS::TypeName` it
1202
- # returns is a frozen value object safe to share across callers
1203
- # (every consumer only reads it `env.class_decls.key?` /
1204
- # `builder.build_*`). The same handful of class names are parsed
1205
- # on nearly every call-site dispatch, so this was a top allocation
1206
- # site; caching the immutable result (nil included) removes it.
1105
+ # Memoised on `@state` (the per-loader store also holding `:env` / `:builder`): `RBS::TypeName.parse`
1106
+ # is a pure, deterministic function of the normalised string, and the `RBS::TypeName` it returns is a
1107
+ # frozen value object safe to share across callers (every consumer only reads it
1108
+ # `env.class_decls.key?` / `builder.build_*`). The same handful of class names are parsed on nearly
1109
+ # every call-site dispatch, so this was a top allocation site; caching the immutable result (nil
1110
+ # included) removes it.
1207
1111
  def parse_type_name(name)
1208
1112
  s = name.to_s
1209
1113
  return nil if s.empty?
@@ -1225,10 +1129,8 @@ module Rigor
1225
1129
  return false unless rbs_name
1226
1130
  return false if env.nil?
1227
1131
 
1228
- # `RBS::Environment#class_decls` after `resolve_type_names`
1229
- # holds entries for both classes AND modules; the gem unifies
1230
- # them under one map post-resolution. Aliases live in their
1231
- # own table.
1132
+ # `RBS::Environment#class_decls` after `resolve_type_names` holds entries for both classes AND
1133
+ # modules; the gem unifies them under one map post-resolution. Aliases live in their own table.
1232
1134
  env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
1233
1135
  rescue ::RBS::BaseError
1234
1136
  false