rigortype 0.2.6 → 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 (432) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/manual/02-cli-reference.md +2 -1
  4. data/docs/manual/04-diagnostics.md +7 -4
  5. data/docs/manual/06-baseline.md +5 -2
  6. data/docs/manual/08-skills.md +21 -0
  7. data/docs/manual/15-type-protection-coverage.md +6 -4
  8. data/lib/rigor/analysis/baseline.rb +56 -96
  9. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  10. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  11. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  12. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  13. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  14. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  15. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  16. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  17. data/lib/rigor/analysis/check_rules.rb +174 -274
  18. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  19. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  20. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  21. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  22. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  23. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  24. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  25. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  26. data/lib/rigor/analysis/diagnostic.rb +48 -75
  27. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  28. data/lib/rigor/analysis/fact_store.rb +12 -22
  29. data/lib/rigor/analysis/incremental.rb +47 -63
  30. data/lib/rigor/analysis/incremental_session.rb +74 -100
  31. data/lib/rigor/analysis/project_scan.rb +12 -23
  32. data/lib/rigor/analysis/result.rb +3 -5
  33. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  34. data/lib/rigor/analysis/run_stats.rb +38 -61
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  38. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  39. data/lib/rigor/analysis/runner.rb +220 -333
  40. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  41. data/lib/rigor/analysis/worker_session.rb +64 -102
  42. data/lib/rigor/ast/type_node.rb +6 -8
  43. data/lib/rigor/ast.rb +6 -10
  44. data/lib/rigor/bleeding_edge.rb +27 -35
  45. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  46. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  48. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  49. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  50. data/lib/rigor/cache/descriptor.rb +58 -94
  51. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  52. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  53. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  54. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  55. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  56. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  57. data/lib/rigor/cache/rbs_environment.rb +11 -19
  58. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  59. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  60. data/lib/rigor/cache/store.rb +96 -151
  61. data/lib/rigor/cli/annotate_command.rb +51 -88
  62. data/lib/rigor/cli/baseline_command.rb +10 -17
  63. data/lib/rigor/cli/check_command.rb +121 -199
  64. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  65. data/lib/rigor/cli/ci_detector.rb +9 -14
  66. data/lib/rigor/cli/command.rb +8 -14
  67. data/lib/rigor/cli/coverage_command.rb +45 -20
  68. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  69. data/lib/rigor/cli/coverage_report.rb +0 -2
  70. data/lib/rigor/cli/coverage_scan.rb +7 -11
  71. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  72. data/lib/rigor/cli/diff_command.rb +10 -20
  73. data/lib/rigor/cli/docs_command.rb +26 -40
  74. data/lib/rigor/cli/doctor_command.rb +8 -9
  75. data/lib/rigor/cli/explain_command.rb +4 -7
  76. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  77. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  78. data/lib/rigor/cli/lsp_command.rb +11 -19
  79. data/lib/rigor/cli/mcp_command.rb +4 -6
  80. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  81. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  82. data/lib/rigor/cli/options.rb +11 -19
  83. data/lib/rigor/cli/plugin_command.rb +18 -30
  84. data/lib/rigor/cli/plugins_command.rb +29 -51
  85. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  86. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  87. data/lib/rigor/cli/protection_renderer.rb +5 -7
  88. data/lib/rigor/cli/protection_report.rb +27 -17
  89. data/lib/rigor/cli/renderable.rb +4 -6
  90. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  91. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  92. data/lib/rigor/cli/skill_command.rb +70 -36
  93. data/lib/rigor/cli/skill_describe.rb +40 -64
  94. data/lib/rigor/cli/trace_command.rb +9 -16
  95. data/lib/rigor/cli/trace_renderer.rb +35 -51
  96. data/lib/rigor/cli/triage_command.rb +6 -10
  97. data/lib/rigor/cli/triage_renderer.rb +1 -2
  98. data/lib/rigor/cli/type_of_command.rb +14 -23
  99. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_command.rb +6 -10
  101. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  102. data/lib/rigor/cli/type_scan_report.rb +2 -3
  103. data/lib/rigor/cli/upgrade_command.rb +2 -3
  104. data/lib/rigor/cli.rb +16 -28
  105. data/lib/rigor/config_audit.rb +35 -44
  106. data/lib/rigor/configuration/dependencies.rb +41 -70
  107. data/lib/rigor/configuration/severity_profile.rb +28 -42
  108. data/lib/rigor/configuration.rb +150 -241
  109. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  110. data/lib/rigor/environment/class_registry.rb +19 -26
  111. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  112. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  113. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  114. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  115. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  116. data/lib/rigor/environment/rbs_loader.rb +433 -503
  117. data/lib/rigor/environment/reflection.rb +28 -53
  118. data/lib/rigor/environment/reporters.rb +13 -24
  119. data/lib/rigor/environment.rb +176 -291
  120. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  121. data/lib/rigor/flow_contribution/element.rb +7 -12
  122. data/lib/rigor/flow_contribution/fact.rb +33 -57
  123. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  124. data/lib/rigor/flow_contribution/merger.rb +32 -47
  125. data/lib/rigor/flow_contribution.rb +37 -55
  126. data/lib/rigor/inference/acceptance.rb +133 -219
  127. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  128. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  129. data/lib/rigor/inference/budget_trace.rb +43 -63
  130. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  131. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  132. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  133. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  134. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  135. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  136. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  137. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  138. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  139. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  140. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  141. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  142. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  143. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  144. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  145. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  146. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  147. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  148. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  149. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  150. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  151. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  152. data/lib/rigor/inference/def_return_typer.rb +8 -14
  153. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  154. data/lib/rigor/inference/expression_typer.rb +774 -1152
  155. data/lib/rigor/inference/fallback.rb +8 -12
  156. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  157. data/lib/rigor/inference/flow_tracer.rb +25 -36
  158. data/lib/rigor/inference/hkt_body.rb +45 -68
  159. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  160. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  161. data/lib/rigor/inference/hkt_registry.rb +50 -76
  162. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  163. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  164. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  165. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  166. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  167. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  168. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  169. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  170. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  171. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  172. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  173. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  174. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  175. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  176. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  177. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  178. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  179. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  180. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  181. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  182. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  183. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  184. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  185. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  186. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  187. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  188. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  189. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  190. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  191. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  192. data/lib/rigor/inference/mutation_widening.rb +115 -158
  193. data/lib/rigor/inference/narrowing.rb +509 -727
  194. data/lib/rigor/inference/origin_lookup.rb +38 -0
  195. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  196. data/lib/rigor/inference/precision_scanner.rb +26 -35
  197. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  198. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  199. data/lib/rigor/inference/protection_scanner.rb +16 -18
  200. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  201. data/lib/rigor/inference/scope_indexer.rb +473 -817
  202. data/lib/rigor/inference/statement_evaluator.rb +706 -1115
  203. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  204. data/lib/rigor/inference/synthetic_method.rb +9 -16
  205. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  206. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  207. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  208. data/lib/rigor/language_server/buffer_table.rb +11 -18
  209. data/lib/rigor/language_server/completion_provider.rb +69 -116
  210. data/lib/rigor/language_server/debouncer.rb +16 -25
  211. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  212. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  213. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  214. data/lib/rigor/language_server/hover_provider.rb +8 -11
  215. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  216. data/lib/rigor/language_server/loop.rb +14 -19
  217. data/lib/rigor/language_server/project_context.rb +46 -74
  218. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  219. data/lib/rigor/language_server/server.rb +55 -83
  220. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  221. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  222. data/lib/rigor/language_server/uri.rb +8 -13
  223. data/lib/rigor/language_server.rb +4 -6
  224. data/lib/rigor/mcp/loop.rb +2 -3
  225. data/lib/rigor/mcp/server.rb +4 -7
  226. data/lib/rigor/mcp.rb +3 -6
  227. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  228. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  229. data/lib/rigor/plugin/base.rb +335 -518
  230. data/lib/rigor/plugin/blueprint.rb +14 -23
  231. data/lib/rigor/plugin/box.rb +18 -29
  232. data/lib/rigor/plugin/fact_store.rb +16 -26
  233. data/lib/rigor/plugin/inflector.rb +37 -53
  234. data/lib/rigor/plugin/io_boundary.rb +33 -56
  235. data/lib/rigor/plugin/isolation.rb +42 -55
  236. data/lib/rigor/plugin/load_error.rb +10 -15
  237. data/lib/rigor/plugin/loader.rb +30 -49
  238. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  239. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  240. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  241. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  242. data/lib/rigor/plugin/macro.rb +10 -15
  243. data/lib/rigor/plugin/manifest.rb +85 -144
  244. data/lib/rigor/plugin/node_context.rb +14 -22
  245. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  246. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  247. data/lib/rigor/plugin/registry.rb +132 -205
  248. data/lib/rigor/plugin/services.rb +21 -33
  249. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  250. data/lib/rigor/plugin/trust_policy.rb +24 -38
  251. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  252. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  253. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  254. data/lib/rigor/protection/mutator.rb +50 -70
  255. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  256. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  257. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  258. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  259. data/lib/rigor/rbs_extended.rb +107 -197
  260. data/lib/rigor/reflection.rb +68 -86
  261. data/lib/rigor/scope/discovery_index.rb +14 -19
  262. data/lib/rigor/scope.rb +255 -310
  263. data/lib/rigor/sig_gen/classification.rb +6 -10
  264. data/lib/rigor/sig_gen/generator.rb +208 -309
  265. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  266. data/lib/rigor/sig_gen/method_candidate.rb +15 -15
  267. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  268. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  269. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  270. data/lib/rigor/sig_gen/renderer.rb +7 -13
  271. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  272. data/lib/rigor/sig_gen/write_result.rb +8 -16
  273. data/lib/rigor/sig_gen/writer.rb +141 -173
  274. data/lib/rigor/sig_gen.rb +3 -6
  275. data/lib/rigor/signature_path_audit.rb +24 -30
  276. data/lib/rigor/source/constant_path.rb +10 -14
  277. data/lib/rigor/source/literals.rb +31 -45
  278. data/lib/rigor/source/node_locator.rb +9 -11
  279. data/lib/rigor/source/node_walker.rb +9 -13
  280. data/lib/rigor/source.rb +3 -4
  281. data/lib/rigor/testing.rb +16 -20
  282. data/lib/rigor/triage/catalogue.rb +38 -62
  283. data/lib/rigor/triage.rb +31 -52
  284. data/lib/rigor/trinary.rb +9 -13
  285. data/lib/rigor/type/acceptance_router.rb +4 -6
  286. data/lib/rigor/type/accepts_result.rb +10 -14
  287. data/lib/rigor/type/app.rb +19 -27
  288. data/lib/rigor/type/bot.rb +4 -6
  289. data/lib/rigor/type/bound_method.rb +10 -15
  290. data/lib/rigor/type/combinator.rb +165 -257
  291. data/lib/rigor/type/constant.rb +23 -34
  292. data/lib/rigor/type/data_class.rb +10 -15
  293. data/lib/rigor/type/data_instance.rb +14 -20
  294. data/lib/rigor/type/difference.rb +21 -32
  295. data/lib/rigor/type/dynamic.rb +3 -5
  296. data/lib/rigor/type/hash_shape.rb +32 -18
  297. data/lib/rigor/type/integer_range.rb +11 -16
  298. data/lib/rigor/type/intersection.rb +27 -42
  299. data/lib/rigor/type/nominal.rb +10 -15
  300. data/lib/rigor/type/plain_lattice.rb +9 -13
  301. data/lib/rigor/type/refined.rb +67 -114
  302. data/lib/rigor/type/singleton.rb +4 -6
  303. data/lib/rigor/type/struct_class.rb +11 -16
  304. data/lib/rigor/type/struct_instance.rb +15 -21
  305. data/lib/rigor/type/tuple.rb +14 -19
  306. data/lib/rigor/type/union.rb +30 -42
  307. data/lib/rigor/type_node/generic.rb +14 -26
  308. data/lib/rigor/type_node/identifier.rb +12 -19
  309. data/lib/rigor/type_node.rb +3 -12
  310. data/lib/rigor/value_semantics.rb +16 -21
  311. data/lib/rigor/version.rb +1 -1
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  314. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  315. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  318. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  319. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  322. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  323. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +107 -83
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  326. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  327. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  332. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  333. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -236
  334. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  337. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  338. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  339. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  340. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  341. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  342. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  343. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  344. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  345. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  346. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  347. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  348. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  349. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  350. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  351. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  352. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  353. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  354. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  357. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  358. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  359. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  360. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  361. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  362. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +20 -36
  363. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +23 -30
  364. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  365. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  366. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  369. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  370. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  371. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  374. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  384. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  389. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  390. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  392. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  393. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  394. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  395. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  398. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  399. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  400. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  401. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  409. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  410. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +182 -328
  411. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  412. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  413. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  414. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  415. data/sig/rigor/scope.rbs +6 -0
  416. data/skills/rigor-ask/SKILL.md +21 -1
  417. data/skills/rigor-baseline-reduce/SKILL.md +16 -0
  418. data/skills/rigor-ci-setup/SKILL.md +96 -249
  419. data/skills/rigor-doctor/SKILL.md +39 -49
  420. data/skills/rigor-doctor/references/01-checks.md +52 -0
  421. data/skills/rigor-editor-setup/SKILL.md +14 -0
  422. data/skills/rigor-mcp-setup/SKILL.md +14 -0
  423. data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
  424. data/skills/rigor-plugin-author/SKILL.md +16 -0
  425. data/skills/rigor-plugin-review/SKILL.md +174 -0
  426. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
  427. data/skills/rigor-plugin-tune/SKILL.md +21 -2
  428. data/skills/rigor-project-init/SKILL.md +16 -0
  429. data/skills/rigor-protection-uplift/SKILL.md +15 -0
  430. data/skills/rigor-rbs-setup/SKILL.md +15 -0
  431. data/skills/rigor-upgrade/SKILL.md +16 -0
  432. metadata +8 -4
@@ -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,18 +297,29 @@ 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).
304
+ #
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.
294
313
  def append_stub_declarations(base_env, missing)
314
+ declared = declared_type_names(base_env)
295
315
  names = missing.to_set
296
316
  missing.each do |name|
297
317
  parts = name.split("::")
298
318
  (1...parts.length).each { |i| names << parts[0, i].join("::") }
299
319
  end
320
+ names = names.reject { |name| declared.include?(name) }.to_set
321
+ return if names.empty?
322
+
300
323
  source = names.sort_by { |n| n.count(":") }.map do |name|
301
324
  keyword = names.any? { |other| other != name && other.start_with?("#{name}::") } ? "module" : "class"
302
325
  "#{keyword} #{name}\nend\n"
@@ -308,15 +331,21 @@ module Rigor
308
331
  nil
309
332
  end
310
333
 
311
- # ADR-32 WD4 merge synthesised-from-source RBS strings
312
- # into the freshly-built environment. Each entry is a
313
- # `[virtual_filename, rbs_source]` pair. `virtual_filename`
314
- # is purely for diagnostic provenance (RBS parse errors
315
- # cite it) — it is not a real file path. Per WD6 the
316
- # synthesizer-emit path is responsible for catching its
317
- # own parse errors and returning `nil` rather than
318
- # garbage; this method assumes its input is parseable
319
- # and only rescues `RBS::ParsingError` as a fail-soft.
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.
336
+ def declared_type_names(env)
337
+ names = env.class_decls.keys.map { |n| n.to_s.sub(/\A::/, "") }
338
+ if env.respond_to?(:class_alias_decls)
339
+ names.concat(env.class_alias_decls.keys.map { |n| n.to_s.sub(/\A::/, "") })
340
+ end
341
+ names.to_set
342
+ end
343
+
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.
320
349
  def add_virtual_rbs(env, virtual_rbs)
321
350
  return if virtual_rbs.nil? || virtual_rbs.empty?
322
351
 
@@ -327,31 +356,27 @@ module Rigor
327
356
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
328
357
  add_parsed_decls(env, buffer, directives, decls)
329
358
  rescue ::RBS::BaseError
330
- # WD6 fail-soft: a single broken virtual RBS contribution
331
- # does not pull the whole env down. The plugin layer
332
- # records a `source-rbs-synthesis-failed` info diagnostic
333
- # 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.
334
362
  end
335
363
  end
336
364
 
337
- # Per-gem `data/vendored_gem_sigs/<gem>/` directories that
338
- # ship with Rigor. Each subdirectory is one gem's RBS surface
339
- # (the `<gem>.rbs` file is the typical content; `LICENSE.upstream`
340
- # records provenance). Coverage is deliberately scoped to the
341
- # native-extension and "everywhere in Rails" gems whose absence
342
- # dominated `call.undefined-method` noise in the real-world
343
- # 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`.
344
370
  VENDORED_GEM_SIGS_ROOT = File.expand_path(
345
371
  "../../../data/vendored_gem_sigs",
346
372
  __dir__
347
373
  ).freeze
348
374
  private_constant :VENDORED_GEM_SIGS_ROOT
349
375
 
350
- # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens
351
- # Ruby core classes to add methods upstream `ruby/rbs` omits but
352
- # which every concrete value answers at runtime loaded last so
353
- # upstream always wins on conflict. Public so the cache descriptor
354
- # 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.
355
380
  CORE_OVERLAY_SIGS_ROOT = File.expand_path(
356
381
  "../../../data/core_overlay",
357
382
  __dir__
@@ -363,24 +388,20 @@ module Rigor
363
388
  [Pathname(CORE_OVERLAY_SIGS_ROOT)]
364
389
  end
365
390
 
366
- # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`),
367
- # ADR-72. Unlike the unconditional `core_overlay`, each gem's
368
- # overlay is loaded ONLY when that gem is locked in the
369
- # project's Gemfile.lock but ships no RBS of its own
370
- # {Environment.for_project} decides eligibility and passes the
371
- # already-filtered gem-name set here. One directory per gem name
372
- # 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?`.
373
396
  GEM_OVERLAY_SIGS_ROOT = File.expand_path(
374
397
  "../../../data/gem_overlay",
375
398
  __dir__
376
399
  ).freeze
377
400
 
378
- # @param gem_names [Enumerable<String>] overlay-eligible
379
- # Gemfile.lock gem names (the caller filters to the
380
- # `:missing`-coverage, no-conflicting-plugin set).
381
- # @return [Array<Pathname>] the bundled overlay directory for
382
- # each gem that ships one; empty when none match or the
383
- # 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.
384
405
  def gem_overlay_sig_paths(gem_names)
385
406
  return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
386
407
 
@@ -398,14 +419,10 @@ module Rigor
398
419
  end
399
420
  end
400
421
 
401
- # Gem names whose RBS ships under
402
- # `data/vendored_gem_sigs/<gem>/`. The directory walk is
403
- # the source of truth (the `README.md` sibling is not a
404
- # gem and is excluded). Callers building the RBS env use
405
- # this set to drop the matching `rbs collection install`
406
- # directory before it double-declares against the
407
- # vendored copy — the same hazard `DEFAULT_LIBRARIES`
408
- # 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
409
426
  # `RbsCollectionDiscovery`'s `skip_gem_names:`.
410
427
  def vendored_gem_names
411
428
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
@@ -418,48 +435,32 @@ module Rigor
418
435
 
419
436
  attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
420
437
 
421
- # @param libraries [Array<String, Symbol>] stdlib library names to
422
- # load on top of core (e.g., `["pathname", "json"]`). Empty by
423
- # default. Each entry MUST correspond to a directory under the
424
- # `rbs` gem's `stdlib/` tree; unknown names are silently dropped
425
- # on environment build (the underlying `RBS::EnvironmentLoader`
426
- # raises and we fail-soft).
427
- # @param signature_paths [Array<String, Pathname>] additional
428
- # directories of `.rbs` files to load (typically the project's
429
- # `sig/` tree). Non-existent or non-directory paths are filtered
430
- # out at build time so the loader stays robust to fixtures and
431
- # bare repositories.
432
- # @param cache_store [Rigor::Cache::Store, nil] the persistent
433
- # cache the loader threads through to `RbsEnvironment`,
434
- # `RbsKnownClassNames`, `RbsConstantTable`,
435
- # `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
436
- # producers. Pass `nil` (the default) to skip caching; the
437
- # runner threads its own Store through here when enabled.
438
- # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 —
439
- # `[virtual_filename, rbs_source]` pairs synthesised from
440
- # project source by a plugin's
441
- # `Manifest#source_rbs_synthesizer`. Merged into the env
442
- # after `signature_paths:` and the vendored stubs. Pass
443
- # `[]` (the default) when no synthesizer-emitting plugin
444
- # 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.
445
453
  def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [])
446
454
  @libraries = libraries.map(&:to_s).freeze
447
455
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
448
456
  @cache_store = cache_store
449
457
  @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
450
- # Per-loader memoization bucket. Held as a single
451
- # mutable Hash so the loader instance itself can be
452
- # `.freeze`d (per ADR-15 reflection-facade contract)
453
- # without losing the lazy-memo behaviour. Slot names
454
- # currently consulted: `:env`, `:env_loaded`,
455
- # `:env_build_warned`, `:builder`, `:reflection`,
456
- # `:instance_definitions_table`,
457
- # `:singleton_definitions_table`. Constructed via
458
- # `Hash.new` (NOT a `{ ... }` literal) so Rigor's
459
- # `HashShape` narrowing doesn't infer a fixed key set
460
- # from the initial state and fold post-initial slot
461
- # reads (e.g. `@state[:env_loaded]`) to a constant
462
- # `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`.
463
464
  @state = Hash.new # rubocop:disable Style/EmptyLiteral
464
465
  @instance_definition_cache = {}
465
466
  @singleton_definition_cache = {}
@@ -467,51 +468,41 @@ module Rigor
467
468
  @hierarchy = RbsHierarchy.new(self)
468
469
  end
469
470
 
470
- # The enclosing namespaces {.synthesize_missing_namespaces} had to
471
- # invent because the project's `signature_paths:` RBS declared
472
- # qualified names (`class Foo::Bar`) without ever declaring `Foo`.
473
- # Recovered by scanning the built env for class/module entries
474
- # whose every declaration originated from the synthetic buffer, so
475
- # the answer survives the marshalled-env cache (where no build-time
476
- # collector would). Returns `::`-stripped names, shallowest-first.
477
- # Empty for a well-formed sig set (the common case) and whenever
478
- # 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.
479
477
  def synthesized_namespaces
480
478
  names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
481
479
  end
482
480
 
483
- # The referenced-but-undeclared types
484
- # {.stub_missing_referenced_types} stubbed so the project classes
485
- # that mention them could build (e.g. an unavailable
486
- # `DRb::DRbServer`, or a stale `Textbringer::EditorError`).
487
- # Recovered off the built env like {#synthesized_namespaces}, so
488
- # 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.
489
485
  def synthesized_stub_types
490
486
  names_synthesized_in(SYNTHETIC_STUB_BUFFER)
491
487
  end
492
488
 
493
- # Every type name Rigor invented to make an otherwise-inert /
494
- # unbuildable project signature set resolve both the namespace
495
- # stubs and the referenced-type stubs. {MethodDispatcher} resolves
496
- # a call whose receiver is one of these (and that no real
497
- # signature answered) to `Dynamic[Top]`, so the empty stub never
498
- # mis-fires `call.undefined-method`. Memoised; empty (and cheap)
499
- # 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.
500
494
  def synthesized_type_names
501
495
  @state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set
502
496
  end
503
497
 
504
- # Returns true when an RBS class or module declaration with the given
505
- # name is loaded. Accepts unprefixed or top-level-prefixed names
506
- # ("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
507
500
  # negative results both cache).
508
501
  #
509
- # When `cache_store` is set, the loader fetches the entire set of
510
- # known class / module / alias names once (per process) through
511
- # {Cache::RbsKnownClassNames.fetch} and answers `class_known?`
512
- # from the in-memory Set. Cold runs pay a single env walk and
513
- # persist the result; warm runs (and a separate loader sharing
514
- # 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.
515
506
  def class_known?(name)
516
507
  key = name.to_s
517
508
  return @class_known_cache[key] if @class_known_cache.key?(key)
@@ -523,16 +514,14 @@ module Rigor
523
514
  end
524
515
  end
525
516
 
526
- # Returns true when the named RBS declaration is a Module
527
- # (`RBS::AST::Declarations::Module`) rather than a Class. The
528
- # `user_class_fallback_receiver` tier consults this to route
529
- # `Nominal[M].some_kernel_method` (where M is a module mixin
530
- # like `PP::ObjectMixin`) through the `Nominal[Object]`
531
- # fallback, because every concrete includer of M sees Kernel
532
- # / 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.
533
522
  #
534
- # Returns false for classes, for unknown names, and when the
535
- # 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).
536
525
  def rbs_module?(name)
537
526
  return false if env.nil?
538
527
 
@@ -545,10 +534,9 @@ module Rigor
545
534
  false
546
535
  end
547
536
 
548
- # Yields every known class / module / alias name (top-level
549
- # prefixed) currently loaded into the environment. The cache
550
- # producer that materialises the known-name set uses this so
551
- # 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?}.
552
540
  def each_known_class_name
553
541
  return enum_for(:each_known_class_name) unless block_given?
554
542
  return if env.nil?
@@ -556,22 +544,16 @@ module Rigor
556
544
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
557
545
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
558
546
  rescue ::RBS::BaseError
559
- # fail-soft: a broken RBS environment yields no names.
560
- # Analyzer-internal errors (NameError, NoMethodError,
561
- # LoadError) are NOT swallowed those are bugs and
562
- # must surface so they don't hide silently the way the
563
- # 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.
564
550
  end
565
551
 
566
- # ADR-20 slice 2e — iterates over every `%a{...}`
567
- # annotation attached to a class- or module-level
568
- # declaration in the loaded RBS environment, yielding
569
- # `(annotation_string, source_location)` pairs. Used by
570
- # {Rigor::Inference::HktRegistry.scan_rbs_loader} to
571
- # find `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
572
- # directives in user-authored overlays and merge them
573
- # into the per-`Environment` HKT registry. Yields nothing
574
- # 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
575
557
  # {#each_known_class_name}).
576
558
  def each_class_decl_annotation
577
559
  return enum_for(:each_class_decl_annotation) unless block_given?
@@ -585,20 +567,15 @@ module Rigor
585
567
  end
586
568
  end
587
569
  rescue ::RBS::BaseError, ::Ractor::IsolationError
588
- # fail-soft: matches each_known_class_name's policy.
589
- # Ractor::IsolationError surfaces when the scan is
590
- # invoked from a non-main Ractor pool worker before
591
- # ADR-15's full deep-freeze migration completes — the
592
- # worker falls back to the base (builtins-only)
593
- # 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.
594
573
  end
595
574
 
596
- # Like {#each_class_decl_annotation}, but also yields the
597
- # owning class / module's RBS name as the first block
598
- # argument: `(class_name, annotation_string, location)`. Used
599
- # by {Rigor::RbsExtended::ConformanceChecker} to resolve a
600
- # `rigor:v1:conforms-to` directive back to the class it
601
- # 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.
602
579
  def each_class_decl_annotation_with_name
603
580
  return enum_for(:each_class_decl_annotation_with_name) unless block_given?
604
581
  return if env.nil?
@@ -614,16 +591,12 @@ module Rigor
614
591
  # fail-soft: see #each_class_decl_annotation.
615
592
  end
616
593
 
617
- # Returns a frozen `Hash<String, String>` mapping each loaded
618
- # class / module name (top-level prefixed) to the file path of
619
- # its FIRST declaration's RBS source. Used by
620
- # {Rigor::Analysis::RunStats} to attribute the type universe
621
- # between "project sig/" (paths under the configured
622
- # `signature_paths`) and "bundled" (everything else RBS
623
- # core, stdlib libraries, gem-bundled RBS). Each value is a
624
- # frozen `String` so the whole result is `Ractor.shareable?`
625
- # — the Phase 4b worker pool ships a snapshot back to the
626
- # 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.
627
600
  def class_decl_paths
628
601
  return {}.freeze if env.nil?
629
602
 
@@ -646,17 +619,14 @@ module Rigor
646
619
  {}.freeze
647
620
  end
648
621
 
649
- # @return [RBS::Definition, nil] the resolved instance definition
650
- # for `class_name`, or nil when the class is unknown or its
651
- # definition cannot be built (RBS may raise on broken hierarchies;
652
- # 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).
653
625
  #
654
- # Built on demand from the (possibly cache-loaded) env; the
655
- # in-memory `@instance_definition_cache` keeps the per-process
656
- # short-circuit. ADR-54 WD1 retired the definitions disk blob:
657
- # given a cached env, `Marshal.load`-ing every definition was
658
- # measurably slower (and allocation-heavier) than rebuilding
659
- # 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.
660
630
  def instance_definition(class_name)
661
631
  key = class_name.to_s
662
632
  return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
@@ -672,13 +642,10 @@ module Rigor
672
642
  definition.methods[method_name.to_sym]
673
643
  end
674
644
 
675
- # @return [Array<Symbol>, nil] every instance-method name on
676
- # `class_name`own, inherited, and included as resolved
677
- # by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when
678
- # the class definition cannot be built so callers can tell
679
- # "no methods" apart from "unknown class". Used by the
680
- # `rigor:v1:conforms-to` presence check
681
- # ({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}).
682
649
  def instance_method_names(class_name)
683
650
  definition = instance_definition(class_name)
684
651
  return nil unless definition
@@ -686,12 +653,10 @@ module Rigor
686
653
  definition.methods.keys
687
654
  end
688
655
 
689
- # @return [RBS::Definition, nil] the built definition for the RBS
690
- # interface `interface_name` (`_RewindableStream`), whose `.methods`
691
- # are the required members (including interface-ancestor members).
692
- # Returns `nil` when the name does not resolve to a loaded interface
693
- # (a typo, or the defining library / sig set is not on the load
694
- # 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.
695
660
  def interface_definition(interface_name)
696
661
  rbs_name = parse_type_name(interface_name)
697
662
  return nil unless rbs_name
@@ -703,24 +668,20 @@ module Rigor
703
668
  nil
704
669
  end
705
670
 
706
- # @return [Array<Symbol>, nil] every method name required by the RBS
707
- # interface `interface_name`, or nil when it does not resolve. Thin
708
- # 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.
709
673
  def interface_method_names(interface_name)
710
674
  interface_definition(interface_name)&.methods&.keys
711
675
  end
712
676
 
713
- # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`,
714
- # `int`, `range[int?]`, …) appearing in a method signature.
715
- # @return [RBS::Types::t, nil] the alias's aliased type one level out,
716
- # with type arguments substituted for a generic alias (`string` →
717
- # `::String | ::_ToStr`; `range[int?]` `::Range[int?] |
718
- # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see
719
- # through the alias that {Inference::RbsTypeTranslator} otherwise
720
- # degrades to `untyped`, which is why an interface/alias parameter
721
- # does not reject `nil`. `expand_alias2` handles the (rarer) generic
722
- # case — a `range[T]` param previously fell back to "admits", which
723
- # 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)`.
724
685
  def expand_type_alias(rbs_alias)
725
686
  return nil if env.nil?
726
687
 
@@ -733,15 +694,13 @@ module Rigor
733
694
  nil
734
695
  end
735
696
 
736
- # @return [RBS::Definition, nil] the resolved singleton (class
737
- # object) definition for `class_name`. The methods on this
738
- # definition are the *class methods* of `class_name`, including
739
- # those inherited from `Class` and `Module` for class types.
740
- # 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).
741
701
  #
742
- # Built on demand from the env with a per-process memo; the
743
- # same on-demand discipline as {#instance_definition} (ADR-54
744
- # 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).
745
704
  def singleton_definition(class_name)
746
705
  key = class_name.to_s
747
706
  return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
@@ -749,11 +708,10 @@ module Rigor
749
708
  @singleton_definition_cache[key] = build_singleton_definition(class_name)
750
709
  end
751
710
 
752
- # @return [RBS::Definition::Method, nil] the class method on
753
- # `class_name`. For example, `singleton_method(class_name:
754
- # "Integer", method_name: :sqrt)` returns the definition for
755
- # `Integer.sqrt`, while `singleton_method(class_name: "Foo",
756
- # 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.
757
715
  def singleton_method(class_name:, method_name:)
758
716
  definition = singleton_definition(class_name)
759
717
  return nil unless definition
@@ -761,25 +719,19 @@ module Rigor
761
719
  definition.methods[method_name.to_sym]
762
720
  end
763
721
 
764
- # Slice 4 phase 2d. Returns the class's declared type-parameter
765
- # names as Symbols (e.g., `[:Elem]` for `Array`, `[:K, :V]` for
766
- # `Hash`). Used by the dispatcher to build the substitution map
767
- # from receiver `type_args` into the method's return type. The
768
- # instance definition is the canonical source because singleton
769
- # methods (e.g., `Array.new`) parameterize over the same `Elem`
770
- # 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.
771
726
  #
772
- # Returns an empty array for non-generic classes and for unknown
773
- # names (the loader stays fail-soft). NOTE: in the `rbs` gem,
774
- # `RBS::Definition#type_params` returns `Array<Symbol>` directly,
775
- # 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).
776
730
  #
777
- # When `cache_store` is set, the loader fetches the entire
778
- # type-parameter-name table once (per process) through
779
- # {Cache::RbsClassTypeParamNames.fetch} and answers point
780
- # lookups from it. Cold runs build the table once and persist
781
- # it; warm runs (and a separate loader sharing the same Store)
782
- # 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.
783
735
  def class_type_param_names(class_name)
784
736
  if cache_store
785
737
  key = class_name.to_s.delete_prefix("::")
@@ -796,11 +748,9 @@ module Rigor
796
748
  @hierarchy.class_ordering(lhs, rhs)
797
749
  end
798
750
 
799
- # @return [Array<String>] every RBS-declared constant name
800
- # (top-level prefixed, e.g., `"::Math::PI"`) currently loaded
801
- # into the environment. Used by the cache producer that
802
- # materialises the constant-type table; ordinary callers
803
- # 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.
804
754
  def constant_names
805
755
  return [] if env.nil?
806
756
 
@@ -809,11 +759,9 @@ module Rigor
809
759
  []
810
760
  end
811
761
 
812
- # Yields `(name, entry)` for every RBS constant declaration
813
- # currently loaded into the environment. The cache producer
814
- # uses this to materialise the constant-type table without
815
- # going back through {#constant_type} (which would recurse
816
- # 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).
817
765
  def each_constant_decl
818
766
  return enum_for(:each_constant_decl) unless block_given?
819
767
  return if env.nil?
@@ -825,22 +773,16 @@ module Rigor
825
773
  # fail-soft: a broken RBS environment yields no entries.
826
774
  end
827
775
 
828
- # Slice A constant-value lookup. Returns the translated
829
- # `Rigor::Type` for a non-class constant declaration
830
- # (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or
831
- # `nil` when no constant entry exists for `name` in the
832
- # loaded RBS environment. Callers MUST treat the return
833
- # value as authoritative when present and as "unknown" when
834
- # nil; the loader does NOT consult the class declarations
835
- # here — class objects are still resolved through
836
- # {#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`.
837
781
  #
838
- # When `cache_store` is set, the loader fetches the entire
839
- # translated constant table once (per process) through
840
- # {Cache::RbsConstantTable.fetch} and answers point lookups
841
- # from it. Cold runs pay the translation cost up-front and
842
- # write the result to disk; warm runs skip the translation
843
- # 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.
844
786
  def constant_type(name)
845
787
  rbs_name = parse_type_name(name)
846
788
  return nil unless rbs_name
@@ -854,27 +796,18 @@ module Rigor
854
796
  nil
855
797
  end
856
798
 
857
- # ADR-15 Phase 4b.x — eagerly drives every cached
858
- # producer (plus the eager definitions tables, computed
859
- # from the cached env since ADR-54 WD1) so a subsequent
860
- # worker Ractor can serve all of its RBS queries without
861
- # ever calling `RBS::EnvironmentLoader.new`.
862
- # The loader path that calls `EnvironmentLoader.new`
863
- # transitively reads a chain of non-`Ractor.shareable?`
864
- # module constants
865
- # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
866
- # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
867
- # `Gem::Requirement::DefaultRequirement`, …) and trips
868
- # `Ractor::IsolationError`. Pre-warming on the main
869
- # Ractor — env blob loaded, derived tables built — keeps
870
- # workers off that chain (`RBS::DefinitionBuilder` over
871
- # 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).
872
807
  #
873
- # No-op when `cache_store` is nil — without a Store the
874
- # worker has no choice but to build env via the loader,
875
- # so the caller MUST ensure pool mode runs with caching
876
- # enabled. Returns `self` so the call chains cleanly
877
- # 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.
878
811
  def prewarm
879
812
  return self if cache_store.nil?
880
813
 
@@ -888,13 +821,10 @@ module Rigor
888
821
  self
889
822
  end
890
823
 
891
- # ADR-54 WD4 — the shared cache descriptor for every RBS-derived
892
- # producer consulting this loader. Building it digests every
893
- # `.rbs` file under `signature_paths` + the vendored gem sigs,
894
- # and the result is identical across producers, so one build is
895
- # memoised per loader (on `@state`, alongside `:env` — the env
896
- # itself is loader-lifetime-memoised, so this adds no new
897
- # 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).
898
828
  def rbs_cache_descriptor
899
829
  @state[:rbs_cache_descriptor] ||= begin
900
830
  require_relative "../cache/rbs_descriptor"
@@ -902,19 +832,14 @@ module Rigor
902
832
  end
903
833
  end
904
834
 
905
- # ADR-15 Phase 2b — return the loader's read-only
906
- # query surface as a frozen, `Ractor.shareable?`
907
- # {Reflection} value object. Built lazily on first
908
- # access; the loader memoises so repeated calls return
909
- # 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.
910
838
  #
911
- # The Reflection consumes the loader's already-warmed
912
- # cache producers (or, when no `cache_store` is set,
913
- # eagerly walks the env). Once constructed, the
914
- # Reflection carries the derived tables independently
915
- # and never re-consults the loader — making it safe to
916
- # share across Ractors while the loader stays per-
917
- # 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.
918
843
  def reflection
919
844
  @state[:reflection] ||= begin
920
845
  require_relative "reflection"
@@ -931,11 +856,10 @@ module Rigor
931
856
 
932
857
  private
933
858
 
934
- # The `::`-stripped names of every class/module entry whose
935
- # declarations ALL originated from the given sentinel buffer
936
- # i.e. names Rigor synthesized, not names the project declared.
937
- # Reads off the built env so the answer survives the marshalled
938
- # 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.
939
863
  def names_synthesized_in(buffer_name)
940
864
  e = env
941
865
  return [] if e.nil?
@@ -950,13 +874,11 @@ module Rigor
950
874
  names.sort_by { |name| name.count("::") }
951
875
  end
952
876
 
953
- # Collects the AST declaration nodes behind a `class_decls`
954
- # entry across the supported RBS range (`rbs >= 3.0, < 5.0`).
955
- # RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding
956
- # bare AST declarations; RBS 3.x exposes `decls`, an array of
957
- # `MultiEntry::D` wrappers whose `#decl` is the AST declaration.
958
- # The single-`decl` shape is handled defensively so the loader
959
- # 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.
960
882
  def entry_declarations(entry)
961
883
  if entry.respond_to?(:each_decl)
962
884
  [].tap { |acc| entry.each_decl { |decl| acc << decl } }
@@ -969,9 +891,8 @@ module Rigor
969
891
  end
970
892
  end
971
893
 
972
- # True when an AST declaration was emitted into `buffer_name`
973
- # (one of the synthetic-source sentinels) identified by the
974
- # 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.
975
896
  def synthetic_decl?(decl, buffer_name)
976
897
  location = decl.respond_to?(:location) ? decl.location : nil
977
898
  location&.buffer&.name.to_s == buffer_name
@@ -998,23 +919,19 @@ module Rigor
998
919
  end
999
920
  end
1000
921
 
1001
- # ADR-15 Phase 2b — the `Reflection` build path
1002
- # consumes these tables even when `cache_store` is nil
1003
- # (e.g. tests that build a `Reflection` without a
1004
- # persistent cache). The helper routes through the
1005
- # producer's `.fetch` when a store IS available, and
1006
- # 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.
1007
926
  def fetch_or_compute_producer(producer)
1008
927
  return producer.fetch(loader: self, store: cache_store) if cache_store
1009
928
 
1010
929
  producer.send(:compute, self)
1011
930
  end
1012
931
 
1013
- # ADR-15 Phase 2b — `Hash<String, Array<String>>` of
1014
- # normalised ancestor chains per class. Consumes the
1015
- # existing `RbsClassAncestorTable` producer when
1016
- # `cache_store` is set; falls back to the producer's
1017
- # `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}.
1018
935
  def ancestor_names_table
1019
936
  @ancestor_names_table ||= begin
1020
937
  require_relative "../cache/rbs_class_ancestor_table"
@@ -1041,27 +958,50 @@ module Rigor
1041
958
  translated unless translated.is_a?(Type::Bot)
1042
959
  end
1043
960
 
1044
- # The RBS environment for this loader. Memoised both on
1045
- # success AND on failure: when the env build raises
1046
- # (typically `RBS::DuplicatedDeclarationError` because a
1047
- # `signature_paths:` entry redeclares a constant or class
1048
- # already shipped by stdlib RBS), retrying on every
1049
- # subsequent `env` call would re-parse and re-resolve the
1050
- # whole sig set per AST node touched during analysis,
1051
- # multiplying per-file analysis cost by ~100x. Failures
1052
- # short-circuit to `nil` here and are surfaced to the user
1053
- # via `warn_about_env_build_failure_once` so the broken
1054
- # `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.
1055
967
  def env
1056
968
  return @state[:env] if @state[:env_loaded]
1057
969
 
1058
970
  @state[:env_loaded] = true
1059
971
  @state[:env] = cache_store ? cached_env : build_env
972
+ warn_about_quarantined_signatures
973
+ @state[:env]
1060
974
  rescue ::RBS::BaseError => e
1061
975
  warn_about_env_build_failure_once(e)
1062
976
  @state[:env] = nil
1063
977
  end
1064
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
+
1065
1005
  def warn_about_env_build_failure_once(error)
1066
1006
  return if @state[:env_build_warned]
1067
1007
 
@@ -1083,15 +1023,12 @@ module Rigor
1083
1023
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
1084
1024
  end
1085
1025
 
1086
- # Full `Hash<String, RBS::Definition>` tables for the
1087
- # {#prewarm} / {#reflection} consumers (ADR-15 Phase 2b/4b.x),
1088
- # which need every definition materialised up front. Built
1089
- # from the (cached) env via `RBS::DefinitionBuilder` ADR-54
1090
- # WD1 retired the disk blobs these used to `Marshal.load`
1091
- # (building all definitions from a cached env is faster), so
1092
- # the eager-table cost is now a compute, not a load. Keys stay
1093
- # in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`)
1094
- # — 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.
1095
1032
  def instance_definitions_table
1096
1033
  @state[:instance_definitions_table] ||= build_definitions_table do |name|
1097
1034
  build_instance_definition(name)
@@ -1151,15 +1088,12 @@ module Rigor
1151
1088
  nil
1152
1089
  end
1153
1090
 
1154
- # Resolve an RBS class/module ALIAS to its canonical declared name.
1155
- # `class Mutex = Thread::Mutex` lives only in `class_alias_decls`, so
1156
- # `class_known?` reports it (it checks that table) but the definition
1157
- # builder which only knows `class_decls` could not enumerate its
1158
- # methods, leaving alias classes (`Mutex`, and any `X = Y`) with no
1159
- # resolvable method surface. Normalising via the env (RBS's own alias
1160
- # resolution) before the `class_decls` guard fixes dispatch AND the
1161
- # `call.undefined-method` existence check on them. A non-alias name, or
1162
- # 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.
1163
1097
  def canonical_module_name(rbs_name)
1164
1098
  return rbs_name unless env.class_alias_decls.key?(rbs_name)
1165
1099
 
@@ -1168,14 +1102,12 @@ module Rigor
1168
1102
  rbs_name
1169
1103
  end
1170
1104
 
1171
- # Memoised on `@state` (the per-loader store also holding `:env` /
1172
- # `:builder`): `RBS::TypeName.parse` is a pure, deterministic
1173
- # function of the normalised string, and the `RBS::TypeName` it
1174
- # returns is a frozen value object safe to share across callers
1175
- # (every consumer only reads it `env.class_decls.key?` /
1176
- # `builder.build_*`). The same handful of class names are parsed
1177
- # on nearly every call-site dispatch, so this was a top allocation
1178
- # 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.
1179
1111
  def parse_type_name(name)
1180
1112
  s = name.to_s
1181
1113
  return nil if s.empty?
@@ -1197,10 +1129,8 @@ module Rigor
1197
1129
  return false unless rbs_name
1198
1130
  return false if env.nil?
1199
1131
 
1200
- # `RBS::Environment#class_decls` after `resolve_type_names`
1201
- # holds entries for both classes AND modules; the gem unifies
1202
- # them under one map post-resolution. Aliases live in their
1203
- # 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.
1204
1134
  env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
1205
1135
  rescue ::RBS::BaseError
1206
1136
  false