rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -4,7 +4,9 @@ require "rbs"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../inference/rbs_type_translator"
7
+ require_relative "../builtins/imported_refinements"
7
8
  require_relative "rbs_hierarchy"
9
+ require_relative "member_consistency"
8
10
 
9
11
  module Rigor
10
12
  class Environment
@@ -42,6 +44,19 @@ module Rigor
42
44
  # cache HIT. Never a real path, so it must never be reported as one (issue #696).
43
45
  CACHED_LOCATION_BUFFER_NAME = "<cached>"
44
46
 
47
+ # Issue #527 — the two directory trees whose `.rbs` files ARE Ruby's own surface: the `rbs` gem's
48
+ # `core/` and its bundled `stdlib/` repository. {#core_stdlib_classes} tests a declaration's file
49
+ # against them. Resolved once at load time, on the main Ractor: reading these constants walks a
50
+ # chain of non-`Ractor.shareable?` module constants and raises `Ractor::IsolationError` inside a
51
+ # worker (see {#prewarm}). Empty when the rbs version in use does not expose them, which switches
52
+ # the feature off rather than widening it.
53
+ CORE_STDLIB_ROOTS = begin
54
+ [::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT, ::RBS::Repository::DEFAULT_STDLIB_ROOT]
55
+ .compact.map { |root| "#{root}/".freeze }.freeze
56
+ rescue StandardError
57
+ [].freeze
58
+ end
59
+
45
60
  # Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
46
61
  # before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
47
62
  # errors buries the signal.
@@ -83,9 +98,13 @@ module Rigor
83
98
  # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
84
99
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
85
100
  add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
86
- env = RBS::Environment.from_loader(rbs_loader)
101
+ env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
87
102
  project_files = project_sig_files(signature_paths)
88
- add_project_signatures(env, signature_paths, deferred_signature_paths)
103
+ # Issue #1075 — decided from the inputs before either side enters `env`: the consistency rule may
104
+ # stand the `.rbs` member down, and that has to happen before its file is added.
105
+ consistency = member_consistency_for(project_files, virtual_rbs)
106
+ add_project_signatures(env, signature_paths, deferred_signature_paths,
107
+ standdowns: consistency.signature_standdowns)
89
108
  # Issue #928 — the capability-role catalog goes in AFTER the project's own signatures, per
90
109
  # declaration, so a project that already declares one of these interface names keeps its own
91
110
  # (`add_capability_role_signatures`). The rest of the bundled RBS cannot be ordered that way (it
@@ -93,10 +112,9 @@ module Rigor
93
112
  # instead — and losing a whole `sig/` file to a name as ordinary as `_Closable` is exactly what
94
113
  # shipping a top-level interface catalog would otherwise cost.
95
114
  add_capability_role_signatures(env)
96
- # Issue #824 — the project's own signatures are already in `env` at this point, so the inline
97
- # contribution can be asked the question that keeps the two from colliding: which of its members
98
- # does a `.rbs` file already declare? Those stand down per member ({.add_virtual_rbs}).
99
- add_virtual_rbs(env, virtual_rbs, sig_member_owners: signature_member_owners(project_files, virtual_rbs))
115
+ # Issues #824 / #1075 — every member a `.rbs` file also declares was ranked above; the inline side of
116
+ # each pair the `.rbs` won stands down here ({.add_virtual_rbs}), so the two never collide.
117
+ add_virtual_rbs(env, virtual_rbs, inline_standdowns: consistency.inline_standdowns)
100
118
  synthesize_missing_namespaces(env)
101
119
  # Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
102
120
  # constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
@@ -155,6 +173,55 @@ module Rigor
155
173
  false
156
174
  end
157
175
 
176
+ # Issue #1109 — the `rbs` gem (a `DEFAULT_LIBRARIES` entry, so in every run) ships the shims its
177
+ # own Steep check needs under `sig/shims/`, and one of them reopens a core module: `enumerable.rbs`
178
+ # prepends a `(2) -> Enumerator[[Elem, Elem], void]` overload to `Enumerable#each_slice`. That is
179
+ # a claim about rbs's own call sites, not about Ruby — `[1, 2, 3].each_slice(2)` yields `[3]` last
180
+ # — and it leaked into every analysed project: the blockless form typed as a fixed pair (so a
181
+ # `nil` guard on the second slot folded away) and the block form returned `void` instead of the
182
+ # receiver. Unloading the file leaves core's `Enumerator[Array[Elem], self]`, which agrees with the
183
+ # block form's `Array[Elem]`. The other shims (`bundler.rbs`, `rubygems.rbs`) only add missing
184
+ # declarations and stay. Fails soft to the env as built: a relocated or absent shim, or an rbs API
185
+ # shape this code does not recognise, keeps the pre-#1109 behaviour rather than failing the build.
186
+ # Because errors are rescued, the guard spec in `rbs_loader_spec.rb` is what catches a broken unload,
187
+ # so it must stay green on both lines of the RBS compatibility matrix.
188
+ RBS_LIBRARY = "rbs"
189
+ private_constant :RBS_LIBRARY
190
+ UNSOUND_CORE_SHIMS = %w[shims/enumerable.rbs].freeze
191
+ private_constant :UNSOUND_CORE_SHIMS
192
+
193
+ def unload_upstream_core_shims(env, rbs_loader)
194
+ return env unless env.respond_to?(:unload)
195
+
196
+ shim_names = upstream_core_shim_names(rbs_loader)
197
+ return env if shim_names.empty?
198
+
199
+ buffers = loaded_signature_buffers(env)&.select { |buffer| shim_names.include?(buffer.name.to_s) }
200
+ buffers.nil? || buffers.empty? ? env : env.unload(buffers)
201
+ rescue StandardError
202
+ env
203
+ end
204
+
205
+ # rbs 4.x lists its sources through `each_rbs_source`; rbs 3.x keys `signatures` by `Buffer`, and its
206
+ # `unload` matches those very objects with `include?`, so the env's own keys are what it must receive.
207
+ def loaded_signature_buffers(env)
208
+ if env.respond_to?(:each_rbs_source)
209
+ env.each_rbs_source.map(&:buffer)
210
+ elsif env.respond_to?(:signatures)
211
+ env.signatures.keys
212
+ end
213
+ end
214
+
215
+ def upstream_core_shim_names(rbs_loader)
216
+ names = Set.new
217
+ rbs_loader.each_dir do |source, dir|
218
+ next unless source.respond_to?(:name) && source.name == RBS_LIBRARY
219
+
220
+ UNSOUND_CORE_SHIMS.each { |relative| names << Pathname(dir).join(relative).to_s }
221
+ end
222
+ names
223
+ end
224
+
158
225
  # True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
159
226
  # tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
160
227
  # (the synthesizer's own WD6 skip, reported separately).
@@ -333,16 +400,21 @@ module Rigor
333
400
  # Buffer names are the file's absolute path (matching {.project_sig_files}) so {.project_entry?} — which
334
401
  # attributes a `class_decls` entry to the project by buffer name — still recognises these declarations.
335
402
  # Sorted for a deterministic add order (the env feeds the cache, ADR-54).
336
- def add_project_signatures(env, signature_paths, deferred_paths = [])
403
+ #
404
+ # @param standdowns — issue #1075 — `{signature_path => Set[[class, method, kind]]}`, the members an
405
+ # inline declaration displaced because it was the more precise side of a consistent pair
406
+ # ({MemberConsistency::Resolution#signature_standdowns}). They are removed before the file is added.
407
+ def add_project_signatures(env, signature_paths, deferred_paths = [], standdowns: {})
337
408
  deferred = project_sig_files(deferred_paths)
338
409
  (project_sig_files(signature_paths) - deferred).sort.each do |file|
339
410
  parsed = parse_signature_file(file)
340
411
  next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
341
412
 
342
413
  buffer, directives, decls = parsed
414
+ strip_members!(decls, standdowns[file]) if standdowns.key?(file)
343
415
  add_project_parsed_decls(env, buffer, directives, decls)
344
416
  end
345
- add_deferred_signatures(env, deferred)
417
+ add_deferred_signatures(env, deferred, standdowns)
346
418
  end
347
419
 
348
420
  # Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
@@ -380,7 +452,7 @@ module Rigor
380
452
  # (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
381
453
  # exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
382
454
  # innocent ones is the change to make if that ever stops being true.
383
- def add_deferred_signatures(env, deferred_files)
455
+ def add_deferred_signatures(env, deferred_files, standdowns = {})
384
456
  deferred_files.sort.each do |file|
385
457
  parsed = parse_signature_file(file)
386
458
  next if parsed.nil?
@@ -388,6 +460,7 @@ module Rigor
388
460
  buffer, directives, decls = parsed
389
461
  next if generic_arity_conflict(env, decls)
390
462
 
463
+ strip_members!(decls, standdowns[file]) if standdowns.key?(file)
391
464
  add_project_parsed_decls(env, buffer, directives, decls)
392
465
  end
393
466
  end
@@ -827,12 +900,13 @@ module Rigor
827
900
  # provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit
828
901
  # path is responsible for catching its own parse errors and returning `nil` rather than garbage; this
829
902
  # method assumes its input is parseable and only rescues `RBS::ParsingError` as a fail-soft.
830
- # @param sig_member_owners — issue #824 — `{[class, method, kind] => signature_path}`
831
- # from {.signature_member_owners}. Every member it names is removed from the inline contribution
832
- # before the declaration is added, so `sig/` wins per MEMBER instead of the two colliding and
833
- # costing the whole class its method surface (ADR-32 WD13). The stand-down is reported, never
834
- # silent: {RbsLoader#inline_member_standdowns} re-derives the same set for the run's `:info` row.
835
- def add_virtual_rbs(env, virtual_rbs, sig_member_owners: {})
903
+ # @param inline_standdowns — issue #1075 — the `[class, method, kind]` keys whose inline member
904
+ # stands down because a project `.rbs` declares the same member and ADR-112 WD5's consistency rule
905
+ # let the `.rbs` bind ({MemberConsistency::Resolution#inline_standdowns}). Removing them before the
906
+ # declaration is added keeps the two from colliding and costing the whole class its method surface.
907
+ # The outcome is reported, never silent: {RbsLoader#member_consistency} re-derives the same records
908
+ # for the run's rows.
909
+ def add_virtual_rbs(env, virtual_rbs, inline_standdowns: Set.new)
836
910
  return if virtual_rbs.nil? || virtual_rbs.empty?
837
911
 
838
912
  virtual_rbs.each do |filename, content|
@@ -843,7 +917,7 @@ module Rigor
843
917
 
844
918
  buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
845
919
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
846
- strip_members_declared_in_signatures!(decls, sig_member_owners) unless sig_member_owners.empty?
920
+ strip_members!(decls, inline_standdowns) unless inline_standdowns.empty?
847
921
  add_parsed_decls(env, buffer, directives, decls)
848
922
  rescue ::RBS::BaseError
849
923
  # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
@@ -863,7 +937,8 @@ module Rigor
863
937
  end
864
938
  end
865
939
 
866
- # Issue #824 / ADR-32 WD13 — the member-level half of inline-vs-`sig/` precedence.
940
+ # Issue #1075 / ADR-112 WD5 — the member-level half of inline-vs-`sig/` consistency, which replaced
941
+ # #824's "`sig/` wins per member" (ADR-32 WD13).
867
942
  #
868
943
  # A method declared BOTH by a project `.rbs` and by an inline annotation in its own `.rb` lands twice
869
944
  # in one `RBS::Environment`: rbs merges the two sources into a single `ClassEntry` and ranks neither,
@@ -872,135 +947,149 @@ module Rigor
872
947
  # it, real methods and typos alike, reads `Dynamic[top]`. Measured on Rigor's own tree while
873
948
  # [#779](https://github.com/rigortype/rigor/pull/779) was evaluated: 17 of 234 annotated files
874
949
  # overlapped `sig/`, and the collision took 44 classes down. Steep 2.1 / rbs 4.1 have no precedence
875
- # rule to follow here (Steep reports the same collision as a signature diagnostic and the class still
876
- # fails to build), so the spec's "SHOULD follow Steep 2.0" pointer resolves to nothing and the choice
877
- # is Rigor's: the reviewed, `sig-gen`-managed `.rbs` wins, per member.
950
+ # rule to follow here (ADR-32 WD13), so exactly one of the two members has to go before the
951
+ # environment is built. {MemberConsistency} decides which: the more precise of two consistent
952
+ # declarations binds, and on a contradiction or an undecidable pair the `.rbs` does.
878
953
  #
879
- # @return `{[class_name, method_name, kind] => signature_path}` for every
880
- # method-shaped member the project's own `signature_paths:` files declare and the inline sources
881
- # also name. Empty — and free — for a project that contributes no inline RBS, which is every
882
- # project without a `source_rbs_synthesizer:` plugin.
954
+ # @return a {MemberConsistency::Resolution}. {MemberConsistency::EMPTY} — and free — for a project
955
+ # that contributes no inline RBS, which is every project without a `source_rbs_synthesizer:` plugin.
956
+ #
957
+ # Derived from the loader's INPUTS — the synthesized sources and the project's signature files —
958
+ # never from a built environment, for {RbsLoader#quarantined_signatures}' reason turned inside out: a
959
+ # cache HIT never runs the build, and a stripped member leaves no trace in the environment to read
960
+ # back. The build and {RbsLoader#member_consistency} call this same function on the same two inputs,
961
+ # so the members they strip and the rows they report agree warm and cold.
962
+ def member_consistency_for(project_files, virtual_rbs, proof: nil)
963
+ return MemberConsistency::EMPTY if virtual_rbs.nil? || virtual_rbs.empty?
964
+
965
+ signature_members, shadowable = signature_member_index(project_files, virtual_rbs)
966
+ inline_members = inline_member_entries(virtual_rbs, all: !signature_members.empty?)
967
+ MemberConsistency.resolve(signature_members, inline_members, shadowable: shadowable, proof: proof)
968
+ end
969
+
970
+ # `[owners, shadowable]`. `owners` is `{[class_name, method_name, kind] => [signature_path, member,
971
+ # visibility]}` for every method-shaped member the project's own `signature_paths:` files declare and
972
+ # the inline sources also name; the first file in sorted order owns a key two `.rbs` files declare
973
+ # (that pair fails the definition build anyway). `shadowable` is every `::` segment of every class /
974
+ # module name the inline sources and the signature files declare — the names a relative reference
975
+ # might resolve to inside an enclosing namespace ({MemberConsistency::Comparator#faithful?}).
883
976
  #
884
977
  # The parse is scoped by a substring pre-filter over each file's text: a declaration has to spell its
885
978
  # own name, so a signature file mentioning none of the names the inline sources declare cannot
886
979
  # collide with them. The filter over-approximates (it matches a name in a comment or a type position
887
980
  # too), so it can only cost an unnecessary parse, never miss a collision.
888
- def signature_member_owners(project_files, virtual_rbs)
889
- return {} if project_files.nil? || virtual_rbs.nil? || virtual_rbs.empty?
890
-
981
+ def signature_member_index(project_files, virtual_rbs)
891
982
  names = inline_declared_name_segments(virtual_rbs)
892
- return {} if names.empty?
983
+ return [{}, names] if project_files.nil? || project_files.empty? || names.empty?
893
984
 
894
- project_files.to_a.sort.each_with_object({}) do |file, owners|
895
- decls = parse_signature_file_if_mentions(file, names)
896
- next if decls.nil?
985
+ shadowable = names.dup
986
+ owners = project_files.to_a.sort.each_with_object({}) do |file, index|
987
+ content = read_signature_text(file)
988
+ next if content.nil?
897
989
 
898
- each_declared_member(decls) do |class_name, member|
899
- member_method_keys(member).each do |method_name, kind|
900
- owners[[class_name, method_name, kind]] ||= file
901
- end
902
- end
990
+ merge_declared_segments(content, shadowable)
991
+ next if names.none? { |name| content.include?(name) }
992
+
993
+ index_signature_members(index, file, parse_signature_source(file, content))
903
994
  end
995
+ [owners, shadowable]
904
996
  end
905
997
 
906
- # The `[class, method, kind, signature_path, virtual_buffer_name]` record for every inline member
907
- # {.add_virtual_rbs} stands down, sorted and deduplicated.
908
- #
909
- # Derived from the loader's own INPUTS — the synthesized sources and the project's signature files —
910
- # never from the built environment, for {RbsLoader#quarantined_signatures}' reason turned inside out:
911
- # a cache HIT never runs the build, and the stripped member leaves no trace in the environment to
912
- # read back. The same two inputs answer identically warm and cold, which is what makes the reported
913
- # set independent of cache state.
914
- def inline_member_standdowns_for(signature_paths:, virtual_rbs:)
915
- owners = signature_member_owners(project_sig_files(signature_paths), virtual_rbs)
916
- return [].freeze if owners.empty?
917
-
918
- records = virtual_rbs.flat_map do |name, content|
919
- next [] if content.nil? || content.empty? || invalid_encoding?(content.to_s)
920
-
921
- decls = parse_signature_source(name, content.to_s)
922
- decls.nil? ? [] : member_standdowns_in(name, decls, owners)
998
+ def index_signature_members(index, file, decls)
999
+ return if decls.nil?
1000
+
1001
+ each_declared_member(decls) do |class_name, member, visibility|
1002
+ member_method_keys(member).each do |method_name, kind|
1003
+ index[[class_name, method_name, kind]] ||= [file, member, visibility].freeze
1004
+ end
923
1005
  end
924
- records.uniq.sort_by { |record| [record[0], record[1].to_s, record[2].to_s] }.freeze
925
1006
  end
926
1007
 
927
- def member_standdowns_in(virtual_name, decls, owners)
928
- records = []
929
- each_declared_member(decls) do |class_name, member|
930
- member_method_keys(member).each do |method_name, kind|
931
- signature_path = owners[[class_name, method_name, kind]]
932
- next if signature_path.nil?
1008
+ # The directive heads a refinement {MemberConsistency.refinement_conflicts} checks, used to skip
1009
+ # parsing an inline source that carries none when no `.rbs` overlaps it. Narrower than `rigor:v1:` on
1010
+ # purpose: rbs-inline's output marks every skeleton `%a{rigor:v1:inferred-return}`, so that prefix is
1011
+ # in nearly every synthesized source.
1012
+ REFINEMENT_DIRECTIVE_MARKS = ["rigor:v1:return:", "rigor:v1:param:"].freeze
1013
+ private_constant :REFINEMENT_DIRECTIVE_MARKS
933
1014
 
934
- records << [class_name, method_name, kind, signature_path, virtual_name.to_s].freeze
1015
+ # `[[virtual_name, class_name, member], ...]` for the inline members {MemberConsistency.resolve}
1016
+ # reads: every member when a `.rbs` file may overlap them (`all:`), otherwise only the members of a
1017
+ # source that carries a `rigor:v1:return:` / `rigor:v1:param:` refinement, which is checked against
1018
+ # its own declared type. Entries {.add_virtual_rbs} would skip are skipped here too.
1019
+ def inline_member_entries(virtual_rbs, all:)
1020
+ virtual_rbs.each_with_object([]) do |(name, content), entries|
1021
+ content = content.to_s
1022
+ next if content.empty? || invalid_encoding?(content)
1023
+ next unless all || REFINEMENT_DIRECTIVE_MARKS.any? { |mark| content.include?(mark) }
1024
+
1025
+ decls = parse_signature_source(name, content)
1026
+ next if decls.nil?
1027
+
1028
+ each_declared_member(decls) do |class_name, member, visibility|
1029
+ entries << [name.to_s, class_name, member, visibility]
935
1030
  end
936
1031
  end
937
- records
938
1032
  end
939
1033
 
940
- # Removes, in place, every member `owners` says a project signature already declares. Nested
941
- # declarations are descended into after the rejection, and a declaration member contributes no method
942
- # key, so nesting is never itself removed.
943
- def strip_members_declared_in_signatures!(decls, owners, prefix = [])
1034
+ # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`).
1035
+ # Nested declarations are descended into after the rejection, and a declaration member contributes no
1036
+ # method key, so nesting is never itself removed.
1037
+ def strip_members!(decls, keys, prefix = [])
944
1038
  Array(decls).each do |decl|
945
1039
  next unless declaration_with_members?(decl)
946
1040
 
947
1041
  inner = prefix + [decl.name.to_s.delete_prefix("::")]
948
1042
  class_name = inner.join("::")
949
1043
  decl.members.reject! do |member|
950
- member_method_keys(member).any? { |method_name, kind| owners.key?([class_name, method_name, kind]) }
1044
+ member_method_keys(member).any? { |method_name, kind| keys.include?([class_name, method_name, kind]) }
951
1045
  end
952
- strip_members_declared_in_signatures!(decl.members, owners, inner)
1046
+ strip_members!(decl.members, keys, inner)
953
1047
  end
954
1048
  end
955
1049
 
956
- # Yields `[class_name, member]` for every member of every class / module declaration in `decls`,
957
- # descending through nesting. The enclosing path is accumulated and the name rendered `::`-stripped,
958
- # so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is what makes a
959
- # `sig/` declaration and an inline one comparable at all.
1050
+ # Yields `[class_name, member, visibility]` for every member of every class / module declaration in
1051
+ # `decls`, descending through nesting. The enclosing path is accumulated and the name rendered
1052
+ # `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is
1053
+ # what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the member's
1054
+ # own (`private def x`) or else the one the enclosing `private` / `public` section sets; a singleton
1055
+ # method takes no section, as in Ruby.
960
1056
  def each_declared_member(decls, prefix = [], &block)
961
1057
  Array(decls).each do |decl|
962
1058
  next unless declaration_with_members?(decl)
963
1059
 
964
1060
  inner = prefix + [decl.name.to_s.delete_prefix("::")]
965
1061
  class_name = inner.join("::")
966
- decl.members.each { |member| block.call(class_name, member) }
1062
+ section = :public
1063
+ decl.members.each do |member|
1064
+ case member
1065
+ when ::RBS::AST::Members::Private then section = :private
1066
+ when ::RBS::AST::Members::Public then section = :public
1067
+ else block.call(class_name, member, member_visibility(member, section))
1068
+ end
1069
+ end
967
1070
  each_declared_member(decl.members, inner, &block)
968
1071
  end
969
1072
  end
970
1073
 
1074
+ def member_visibility(member, section)
1075
+ own = member.respond_to?(:visibility) ? member.visibility : nil
1076
+ return own if own
1077
+
1078
+ member.respond_to?(:kind) && member.kind == :singleton ? :public : section
1079
+ end
1080
+
971
1081
  def declaration_with_members?(decl)
972
1082
  (decl.is_a?(::RBS::AST::Declarations::Module) || decl.is_a?(::RBS::AST::Declarations::Class)) &&
973
1083
  decl.respond_to?(:members)
974
1084
  end
975
1085
 
976
- # The `[method_name, kind]` pairs one RBS member contributes to a definition build, mirroring
977
- # `RBS::DefinitionBuilder::MethodBuilder#build_instance` / `#build_singleton` — which is what decides
978
- # whether two members collide. An attribute contributes its reader and / or writer name (`foo`,
979
- # `foo=`), an alias its new name, and `def self?.x` both sides. An `overloading?` member (`def x: ...
980
- # | ...`) contributes NOTHING: rbs files those under `overloads` rather than `originals`, so they are
981
- # designed to compose with an existing declaration and must not stand down against it.
1086
+ # {MemberConsistency.member_method_keys} — the `[method_name, kind]` pairs a member contributes to a
1087
+ # definition build, which is what decides whether two members collide.
982
1088
  def member_method_keys(member)
983
- case member
984
- when ::RBS::AST::Members::MethodDefinition then method_definition_keys(member)
985
- when ::RBS::AST::Members::AttrReader then [[member.name, member.kind]]
986
- when ::RBS::AST::Members::AttrWriter then [[:"#{member.name}=", member.kind]]
987
- when ::RBS::AST::Members::AttrAccessor then [[member.name, member.kind], [:"#{member.name}=", member.kind]]
988
- when ::RBS::AST::Members::Alias then [[member.new_name, member.kind]]
989
- else []
990
- end
1089
+ MemberConsistency.member_method_keys(member)
991
1090
  end
992
1091
 
993
- def method_definition_keys(member)
994
- return [] if member.respond_to?(:overloading?) && member.overloading?
995
-
996
- case member.kind
997
- when :instance then [[member.name, :instance]]
998
- when :singleton then [[member.name, :singleton]]
999
- else [[member.name, :instance], [member.name, :singleton]] # `def self?.x` defines both sides
1000
- end
1001
- end
1002
-
1003
- # A class / module declaration header, as the pre-filter in {.signature_member_owners} reads it out of
1092
+ # A class / module declaration header, as the pre-filter in {.signature_member_index} reads it out of
1004
1093
  # synthesized RBS text.
1005
1094
  INLINE_DECLARATION_NAME = /^[ \t]*(?:class|module)[ \t]+([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)/
1006
1095
  private_constant :INLINE_DECLARATION_NAME
@@ -1016,18 +1105,20 @@ module Rigor
1016
1105
  content = content.to_s
1017
1106
  next if content.empty? || invalid_encoding?(content)
1018
1107
 
1019
- content.scan(INLINE_DECLARATION_NAME) { |(declared)| acc.merge(declared.split("::")) }
1108
+ merge_declared_segments(content, acc)
1020
1109
  end
1021
1110
  end
1022
1111
 
1023
- # Parses one project signature file, or nil when it is unreadable, not valid UTF-8, unparseable, or
1024
- # mentions none of `names` (so nothing in it can collide with the inline sources).
1025
- def parse_signature_file_if_mentions(file, names)
1026
- content = File.read(file, encoding: "UTF-8")
1027
- return nil if invalid_encoding?(content)
1028
- return nil if names.none? { |name| content.include?(name) }
1112
+ # Adds every `::` segment of every class / module header in `content` to `segments`. The pattern has
1113
+ # one capture group, which every match fills.
1114
+ def merge_declared_segments(content, segments)
1115
+ content.scan(INLINE_DECLARATION_NAME) { segments.merge(Regexp.last_match(1).to_s.split("::")) }
1116
+ end
1029
1117
 
1030
- parse_signature_source(file, content)
1118
+ # One project signature file's text, or nil when it is unreadable or not valid UTF-8.
1119
+ def read_signature_text(file)
1120
+ content = File.read(file, encoding: "UTF-8")
1121
+ invalid_encoding?(content) ? nil : content
1031
1122
  rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
1032
1123
  nil
1033
1124
  end
@@ -1056,9 +1147,10 @@ module Rigor
1056
1147
  private_constant :VENDORED_GEM_SIGS_ROOT
1057
1148
 
1058
1149
  # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens Ruby core classes to add methods
1059
- # upstream `ruby/rbs` omits but which every concrete value answers at runtime — loaded last so
1060
- # upstream always wins on conflict. Public so the cache descriptor can digest these files into the
1061
- # env-blob key.
1150
+ # upstream `ruby/rbs` omits but which every concrete value answers at runtime. A direct `def` that
1151
+ # another source also declares is a `DuplicatedMethodDefinitionError`, not a precedence question, so a
1152
+ # method a project `sig/` or a later rbs is likely to declare goes on a module the class extends
1153
+ # (`process.rbs`). Public so the cache descriptor can digest these files into the env-blob key.
1062
1154
  CORE_OVERLAY_SIGS_ROOT = File.expand_path(
1063
1155
  "../../../data/core_overlay",
1064
1156
  __dir__
@@ -1128,6 +1220,10 @@ module Rigor
1128
1220
  # `core_overlay/pathname.rbs` also carries an `| ...` continuation but is deliberately NOT listed:
1129
1221
  # its base (`Pathname#expand_path`) lives in rbs's `core/pathname.rbs` on every supported rbs
1130
1222
  # release (3.10 / 4.0 / 4.1), so the continuation is safe — and applicable — with no library loaded.
1223
+ # `core_overlay/string_io.rbs` re-opens a class the `stringio` library declares yet is NOT listed
1224
+ # either: `RBS::EnvironmentLoader#load` adds that library itself whenever it loads core (rbs 3.7+),
1225
+ # and earlier releases declare `StringIO` in core, so gating would only drop its `Enumerable` mixin
1226
+ # where `StringIO` is present anyway.
1131
1227
  #
1132
1228
  # Keyed by `data/vendored_gem_sigs/` directory basename.
1133
1229
  LIBRARY_SUPPLEMENT_VENDORED_DIRS = {
@@ -1144,12 +1240,23 @@ module Rigor
1144
1240
  }.freeze
1145
1241
  private_constant :LIBRARY_SUPPLEMENT_CORE_OVERLAYS
1146
1242
 
1243
+ # `data/core_overlay/` files that fill a hole only one `rbs` release line has, keyed by file basename
1244
+ # to the `rbs` version requirement they load under. Each carries an `| ...` continuation of overloads a
1245
+ # later line declares upstream, so loading it there would add a second copy of them. The env-cache key
1246
+ # already carries `RBS::VERSION` ({Cache::RbsDescriptor.rbs_gem_entry}), so switching lines re-keys.
1247
+ # The requirement is a String, not a `Gem::Requirement`, so the constant stays `Ractor.shareable?`.
1248
+ RBS_LINE_CORE_OVERLAYS = {
1249
+ "hash_rbs3.rbs" => "< 4.0"
1250
+ }.freeze
1251
+ private_constant :RBS_LINE_CORE_OVERLAYS
1252
+
1147
1253
  # Adds the Rigor-shipped signature sources to `rbs_loader`: every `data/vendored_gem_sigs/<gem>/`
1148
- # directory, then the `data/core_overlay/` files — the overlay LAST so an upstream declaration
1149
- # always wins on conflict (these reopenings only fill genuine holes, e.g. `Numeric#to_f`/`to_i`/
1150
- # `to_r`, which upstream RBS declares on the concrete subclasses but not on the abstract `Numeric`
1151
- # that Rigor's arithmetic-chain widening produces). The overlay is added per-file, not
1152
- # per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` files must be gated individually.
1254
+ # directory, then the `data/core_overlay/` files, which fill genuine holes only (e.g. `Numeric#to_f`/
1255
+ # `to_i`/`to_r`, which upstream RBS declares on the concrete subclasses but not on the abstract `Numeric`
1256
+ # that Rigor's arithmetic-chain widening produces). Load order does not settle a conflict: a method
1257
+ # both sides declare directly raises `DuplicatedMethodDefinitionError` for the whole class. The
1258
+ # overlay is added per-file, not per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` and
1259
+ # `RBS_LINE_CORE_OVERLAYS` files must be gated individually.
1153
1260
  #
1154
1261
  # @param loaded_library_names — libraries that actually resolved on this loader.
1155
1262
  def add_bundled_signatures(rbs_loader, loaded_library_names)
@@ -1165,6 +1272,7 @@ module Rigor
1165
1272
  dir.children.sort.each do |file|
1166
1273
  next unless file.file? && file.extname == ".rbs"
1167
1274
  next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_CORE_OVERLAYS, file, loaded_library_names)
1275
+ next unless rbs_line_matches?(file)
1168
1276
 
1169
1277
  rbs_loader.add(path: file)
1170
1278
  end
@@ -1180,6 +1288,13 @@ module Rigor
1180
1288
  library.nil? || loaded_library_names.include?(library)
1181
1289
  end
1182
1290
 
1291
+ # @param path — a `data/core_overlay/` file.
1292
+ # @return true when `path` is not tied to an `rbs` release line, or the running `rbs` is on its line.
1293
+ def rbs_line_matches?(path)
1294
+ requirement = RBS_LINE_CORE_OVERLAYS[path.basename.to_s]
1295
+ requirement.nil? || Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(::RBS::VERSION))
1296
+ end
1297
+
1183
1298
  # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`), ADR-72. Unlike the unconditional
1184
1299
  # `core_overlay`, each gem's overlay is loaded ONLY when that gem is locked in the project's
1185
1300
  # Gemfile.lock but ships no RBS of its own — {Environment.for_project} decides eligibility and passes
@@ -1362,7 +1477,7 @@ module Rigor
1362
1477
  # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
1363
1478
  # `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
1364
1479
  # `:definition_build_deferred_count`, `:definition_build_deferred_first`,
1365
- # `:definition_build_summary_warned`, `:inline_member_standdowns`, `:internal_demand`,
1480
+ # `:definition_build_summary_warned`, `:member_consistency`, `:member_consistency_proven`, `:internal_demand`,
1366
1481
  # `:internal_demand_status`, `:builder`, `:reflection`,
1367
1482
  # `:instance_definitions_table`, `:singleton_definitions_table`.
1368
1483
  # Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
@@ -1457,11 +1572,94 @@ module Rigor
1457
1572
  end
1458
1573
  private :build_project_declared_classes
1459
1574
 
1575
+ # Issue #527 — the class / module names whose PRIMARY declaration lives in the `rbs` gem's own
1576
+ # `core/` or `stdlib/` tree, top-level prefix stripped.
1577
+ #
1578
+ # The distinction it draws is narrower than {#project_declared_classes}'s: not "who wrote this
1579
+ # signature" but "is this Ruby's own documented surface". A core / stdlib signature is the method
1580
+ # set every negative check rule already trusts for a direct receiver of the class, which is what
1581
+ # lets a Ruby-source SUBCLASS of one resolve its inherited calls there (`RbsDispatch`'s
1582
+ # `core_stdlib_ancestor_method`). A GEM's RBS is a different claim — routinely partial, which is
1583
+ # ADR-43's whole reason for declining blanket inherited resolution — so it is deliberately outside
1584
+ # this set even when the gem ships signatures and even when it is one of {DEFAULT_LIBRARIES}
1585
+ # (`prism`, `rbs` are gems whose signatures ship with the gem, not entries of rbs's `stdlib/`).
1586
+ #
1587
+ # Attribution is by buffer NAME, the same mechanism and the same limits as
1588
+ # {#project_declared_classes}: it survives the ADR-54 environment cache, and an old blob's
1589
+ # `<cached>` sentinel lands every class OUTSIDE the set — the feature switches off rather than
1590
+ # widening, which is the direction ADR-5 wants. Memoised per loader; one pass over `class_decls`.
1591
+ def core_stdlib_classes
1592
+ @state[:core_stdlib_classes] ||= build_core_stdlib_classes
1593
+ end
1594
+
1595
+ def build_core_stdlib_classes
1596
+ environment = env
1597
+ return Set.new.freeze if environment.nil? || CORE_STDLIB_ROOTS.empty?
1598
+
1599
+ names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1600
+ decl = self.class.primary_decl_for(entry)
1601
+ next if decl.nil?
1602
+
1603
+ path = self.class.declaration_buffer_name(decl)
1604
+ next if path.nil?
1605
+
1606
+ acc << rbs_name.to_s.delete_prefix("::") if CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }
1607
+ end
1608
+ names.freeze
1609
+ rescue ::RBS::BaseError
1610
+ Set.new.freeze
1611
+ end
1612
+ private :build_core_stdlib_classes
1613
+
1614
+ # True when `class_name` is declared by Ruby core or a stdlib library (see {#core_stdlib_classes}).
1615
+ def core_or_stdlib_class?(class_name)
1616
+ core_stdlib_classes.include?(class_name.to_s.delete_prefix("::"))
1617
+ end
1618
+
1460
1619
  # True when `class_name`'s RBS declaration is one the project wrote (see {#project_declared_classes}).
1461
1620
  def project_declared_class?(class_name)
1462
1621
  project_declared_classes.include?(class_name.to_s.delete_prefix("::"))
1463
1622
  end
1464
1623
 
1624
+ # Issue #1362 — the translated type of the RBS declaration of the global variable `name` (`:$stdout`, declared
1625
+ # `IO` by the core signatures), or nil when no loaded signature declares it. Translated without an alias
1626
+ # expander, as {#constant_type} translates a constant, so `$DEBUG: boolish` reads `Dynamic[top]` rather than
1627
+ # `top`.
1628
+ def global_type(name)
1629
+ global_declarations[name.to_sym]&.first
1630
+ end
1631
+
1632
+ # True when `name`'s RBS declaration lives in the `rbs` gem's own `core/` or `stdlib/` tree: a global the
1633
+ # interpreter or a default library sets. Attributed by buffer name, as {#core_or_stdlib_class?} is, with the
1634
+ # same limit: an environment blob without buffer names answers false for every global.
1635
+ def core_or_stdlib_global?(name)
1636
+ global_declarations[name.to_sym]&.last || false
1637
+ end
1638
+
1639
+ # `name => [type, core_or_stdlib]` for every global the environment declares; a declaration whose type
1640
+ # translates to `bot` is left out. Memoised per loader: the core signatures declare about fifty.
1641
+ def global_declarations
1642
+ @state[:global_declarations] ||= build_global_declarations
1643
+ end
1644
+ private :global_declarations
1645
+
1646
+ def build_global_declarations
1647
+ environment = env
1648
+ return {}.freeze if environment.nil?
1649
+
1650
+ table = environment.global_decls.each_with_object({}) do |(name, entry), acc|
1651
+ type = Inference::RbsTypeTranslator.translate(entry.decl.type)
1652
+ next if type.is_a?(Type::Bot)
1653
+
1654
+ path = self.class.declaration_buffer_name(entry.decl)
1655
+ acc[name.to_sym] = [type, !path.nil? && CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }].freeze
1656
+ end
1657
+ table.freeze
1658
+ rescue ::RBS::BaseError
1659
+ {}.freeze
1660
+ end
1661
+ private :build_global_declarations
1662
+
1465
1663
  # The total RBS-environment build failure captured this run, or nil when the env built. Unlike
1466
1664
  # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived so a
1467
1665
  # cache HIT reports it too — a total failure collapses the WHOLE env to nil. Project-vs-bundled
@@ -1495,9 +1693,10 @@ module Rigor
1495
1693
  # definition-build failure leaves no trace in the env at all — the env is fine; it is the BUILD over it
1496
1694
  # that raised — so the rescue is the only place that ever knows.
1497
1695
  #
1498
- # @return `[class_name, error_class_name,
1499
- # first_error_line, conflicting_buffer_names]`, one per class, in first-failure order. Empty for a
1500
- # healthy sig set, which is the common case.
1696
+ # @return `[class_name, error_class_name, member, conflicting_buffer_names,
1697
+ # unresolved_type_name_detail]`, one per class, in first-failure order (issue #997 added the fifth
1698
+ # slot; see {#unresolved_type_name_detail} — `nil` there for every failure kind but an unresolvable
1699
+ # type name). Empty for a healthy sig set, which is the common case.
1501
1700
  def definition_build_failures
1502
1701
  (@state[:definition_build_failures] || []).dup.freeze
1503
1702
  end
@@ -1524,10 +1723,11 @@ module Rigor
1524
1723
  end
1525
1724
  end
1526
1725
 
1527
- # Issue #824 / ADR-32 WD13 — the inline (rbs-inline) members that stood down because the project's own
1528
- # `signature_paths:` already declare the same `(class, method, kind)`. The `.rbs` wins per member; the
1529
- # rest of the file's annotations still bind, and the class keeps its method surface instead of failing
1530
- # its definition build.
1726
+ # Issue #1075 / ADR-112 WD5 — one {MemberConsistency::Record} per `(class, method, kind)` the project's
1727
+ # own `signature_paths:` and an inline (rbs-inline) source both declare, saying which side bound and
1728
+ # why, plus one per `rigor:v1:return:` / `rigor:v1:param:` refinement that exceeds its member's own
1729
+ # declared type. The rest of the file's annotations still bind either way, and the class keeps its
1730
+ # method surface instead of failing its definition build.
1531
1731
  #
1532
1732
  # The sibling of {#virtual_rbs_collision_quarantined} one granularity finer: that one names a whole
1533
1733
  # inline FILE dropped for a class-level duplicate, this one names a single member. Both are derived
@@ -1535,12 +1735,15 @@ module Rigor
1535
1735
  # derivation reads the loader's inputs rather than the built env, because a stripped member leaves no
1536
1736
  # trace in the env at all.
1537
1737
  #
1538
- # @return `[class_name, method_name, kind, signature_path,
1539
- # virtual_buffer_name]` records, sorted. Empty (and free) whenever the project contributes no inline
1540
- # RBS or none of it overlaps a signature file.
1541
- def inline_member_standdowns
1542
- @state[:inline_member_standdowns] ||=
1543
- self.class.inline_member_standdowns_for(signature_paths: @signature_paths, virtual_rbs: @virtual_rbs)
1738
+ # @return the records, sorted. Empty (and free) whenever the project contributes no inline RBS.
1739
+ #
1740
+ # @param proof — a {MemberConsistency::RbsProof} over this loader, without which no pair is reported
1741
+ # as a contradiction (only the severity of a row depends on it, never which declaration binds).
1742
+ def member_consistency(proof: nil)
1743
+ key = proof ? :member_consistency_proven : :member_consistency
1744
+ @state[key] ||= self.class.member_consistency_for(
1745
+ self.class.project_sig_files(@signature_paths), @virtual_rbs, proof: proof
1746
+ ).records
1544
1747
  end
1545
1748
 
1546
1749
  # The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
@@ -2025,11 +2228,21 @@ module Rigor
2025
2228
  # Building it digests every `.rbs` file under `signature_paths` + the vendored gem sigs, and the
2026
2229
  # result is identical across producers, so one build is memoised per loader (on `@state`, alongside
2027
2230
  # `:env` — the env itself is loader-lifetime-memoised, so this adds no new staleness class).
2231
+ #
2232
+ # Issue #1014 — nil when the engine's source cannot be digested ({Cache::EngineSource::Unavailable});
2233
+ # {Cache::RbsCacheProducer.fetch} then computes uncached. Memoised through `key?` rather than `||=` so
2234
+ # that answer is reached once: rebuilding it would re-digest the whole signature tree per producer to
2235
+ # arrive at the same nil.
2028
2236
  def rbs_cache_descriptor
2029
- @state[:rbs_cache_descriptor] ||= begin
2030
- require_relative "../cache/rbs_descriptor"
2031
- Cache::RbsDescriptor.build(self)
2032
- end
2237
+ return @state[:rbs_cache_descriptor] if @state.key?(:rbs_cache_descriptor)
2238
+
2239
+ require_relative "../cache/rbs_descriptor"
2240
+ @state[:rbs_cache_descriptor] =
2241
+ begin
2242
+ Cache::RbsDescriptor.build(self)
2243
+ rescue Cache::EngineSource::Unavailable
2244
+ nil
2245
+ end
2033
2246
  end
2034
2247
 
2035
2248
  # ADR-15 Phase 2b — return the loader's read-only query surface as a frozen, `Ractor.shareable?`
@@ -2397,7 +2610,60 @@ module Rigor
2397
2610
  return if details.key?(key)
2398
2611
 
2399
2612
  details[key] = [key, error.class.name.to_s, definition_build_member(error),
2400
- definition_build_conflict_buffers(error)].freeze
2613
+ definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
2614
+ end
2615
+
2616
+ # Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
2617
+ # error class, so the cached tuple gains a populated fifth slot only for the failure kind it
2618
+ # distinguishes — "one declaration names a type that does not exist" from "two declarations collide"
2619
+ # ({DiagnosticAggregator#definition_build_advice}, which folds this in).
2620
+ #
2621
+ # Computed once, at RAISE time, and frozen into the cached detail rather than re-derived when a warm
2622
+ # run replays the tuple from {IncrementalSnapshot} — there is no live `RBS::NoTypeFoundError` to
2623
+ # re-inspect on that path, only the plain strings this method already reduced it to.
2624
+ #
2625
+ # `[token, location, refinement_hint]`:
2626
+ # - `token` is the unresolved name exactly as `RBS::NoTypeFoundError#type_name` carries it — the ONLY
2627
+ # name that ever reaches RBS. A hyphenated payload (`finite-float`) is truncated to its first
2628
+ # segment by rbs-inline's own tolerant type parse (`RBS::Parser.parse_type(..., require_eof: false)`
2629
+ # stops at the first complete type and silently discards the rest), so `-float` never becomes a
2630
+ # token RBS itself can name or complain about.
2631
+ # - `location` is `error.location.to_s` verbatim (`"virtual:rbs-inline:<path>:line:col...line:col"`)
2632
+ # — the position the error already carries, per issue #997, named rather than re-derived. It is a
2633
+ # position in the SYNTHESIZED buffer for an inline annotation, not necessarily the `.rb` line the
2634
+ # author wrote: {Effects::InlineAnchor} maps a known `%a{...}` spelling back by ordinal, which does
2635
+ # not apply to an arbitrary broken type token, so no such mapping is attempted here.
2636
+ # - `refinement_hint` is the full compound name (`"finite-float"`) when the SAME synthesized buffer —
2637
+ # rbs-inline re-emits the author's original `# @rbs name: TYPE` comment verbatim above the member it
2638
+ # generates — carries `token` immediately followed by a hyphenated continuation that is ITSELF a
2639
+ # registered {Builtins::ImportedRefinements} name; `nil` otherwise. Exact-match only: a near-miss
2640
+ # compound name says nothing, so a coincidental `token-suffix` that is not a real refinement can
2641
+ # never manufacture a misleading "did you mean" (false positives outrank worst-case reading).
2642
+ def unresolved_type_name_detail(error)
2643
+ return nil unless error.is_a?(::RBS::NoTypeFoundError)
2644
+
2645
+ token = error.type_name.to_s.delete_prefix("::")
2646
+ [token, error.location&.to_s, refinement_hint_for(token, error)].freeze
2647
+ rescue ::RBS::BaseError, StandardError
2648
+ nil
2649
+ end
2650
+
2651
+ # The buffer rbs-inline synthesized still carries the author's own comment line verbatim (the
2652
+ # "annotation echo" {Effects::InlineAnchor} documents), so a hyphenated refinement name truncated out
2653
+ # of the parsed type ({#unresolved_type_name_detail}) is still readable a few characters further along
2654
+ # the SAME text the error's own location already points into.
2655
+ def refinement_hint_for(token, error)
2656
+ return nil if token.empty?
2657
+
2658
+ content = error.location&.buffer&.content
2659
+ return nil if content.nil?
2660
+
2661
+ candidate = content[/\b#{Regexp.escape(token)}-[A-Za-z0-9][\w-]*/]
2662
+ return nil if candidate.nil?
2663
+
2664
+ Builtins::ImportedRefinements.known?(candidate) ? candidate : nil
2665
+ rescue ::RBS::BaseError, StandardError
2666
+ nil
2401
2667
  end
2402
2668
 
2403
2669
  # Issue #696 — the REPORTING half: promote a remembered detail into {#definition_build_failures}