rigortype 0.4.7 → 0.4.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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -80,6 +80,20 @@ module Rigor
80
80
  type.is_a?(Type::Constant) && type.value.is_a?(String)
81
81
  end
82
82
 
83
+ # A predicate refinement over String (`decimal-int-string`, `lowercase-string`) under a String mutator → its
84
+ # `String` base. Any in-place mutation can falsify the predicate (`s.clear` leaves a `decimal-int-string`
85
+ # empty, `s << "A"` a `lowercase-string` mixed-case), so none keeps it. `nil` for every other input.
86
+ def widen_refined(refined, method_name)
87
+ return nil unless refined?(refined)
88
+ return nil unless MUTATORS.include?(method_name)
89
+
90
+ refined.base
91
+ end
92
+
93
+ def refined?(type)
94
+ type.is_a?(Type::Refined) && type.base.is_a?(Type::Nominal) && type.base.class_name == "String"
95
+ end
96
+
83
97
  # Whether `method_name`, called with `arg_types` (the call's positional argument types, possibly none), can leave
84
98
  # a non-empty receiver empty, and so retract a `non-empty-string` refinement. Every mutator outside
85
99
  # {EMPTY_PRESERVING} can, except a {TRANSLATORS} call whose replacement is provably non-empty.
@@ -323,21 +323,29 @@ module Rigor
323
323
  # - `class_methods do ... end` blocks are NOT yet handled —
324
324
  # singleton-level emission is out of scope per the slice-3
325
325
  # floor framing.
326
+ #
327
+ # Issue #3005 — the `included do` blocks of every body of a module some body extends with Concern are read: a
328
+ # reopening that does not repeat the `extend` runs its block on a module that is already a concern (loaded
329
+ # first, its `included do` raises, so no running program has the includer without the calls). A module no
330
+ # body extends with Concern contributes nothing.
326
331
  CONCERN_NAME = "ActiveSupport::Concern"
327
332
 
328
333
  def build_concern_index(asts)
329
- index = {}
334
+ bodies = Hash.new { |hash, name| hash[name] = [] }
330
335
  asts.each_value do |ast|
331
336
  next if ast.nil?
332
337
 
333
338
  walk_module_decls(ast, []) do |module_name, body|
334
- next if module_name.nil? || body.nil?
335
- next unless concern_module_body?(body)
336
-
337
- deferred_calls = collect_included_do_calls(body)
338
- index[module_name] = deferred_calls.freeze if deferred_calls.any?
339
+ bodies[module_name] << body unless module_name.nil? || body.nil?
339
340
  end
340
341
  end
342
+ index = {}
343
+ bodies.each do |module_name, list|
344
+ next unless list.any? { |body| concern_module_body?(body) }
345
+
346
+ deferred_calls = list.flat_map { |body| collect_included_do_calls(body) }
347
+ index[module_name] = deferred_calls.freeze if deferred_calls.any?
348
+ end
341
349
  index.freeze
342
350
  end
343
351
 
@@ -359,18 +367,34 @@ module Rigor
359
367
  end
360
368
 
361
369
  # Recognises a module body that begins with (or contains at top level) an `extend ActiveSupport::Concern`
362
- # statement.
370
+ # statement. Issue #3005 — `self.extend` and a receiverless or `self.` `send` / `public_send` /
371
+ # `__send__(:extend, …)` are the same call.
363
372
  def concern_module_body?(body)
364
373
  return false unless body.respond_to?(:body)
365
374
 
366
375
  body.body.any? do |stmt|
367
- next false unless stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :extend
368
-
369
- args = stmt.arguments&.arguments || []
370
- args.any? { |arg| const_name_string(arg) == CONCERN_NAME }
376
+ args = concern_extend_arguments(stmt)
377
+ args&.any? { |arg| const_name_string(arg) == CONCERN_NAME }
371
378
  end
372
379
  end
373
380
 
381
+ CONCERN_SEND_CALLS = %i[send public_send __send__].freeze
382
+ private_constant :CONCERN_SEND_CALLS
383
+
384
+ # The modules an `extend` statement on the module's own `self` names, or nil for any other statement.
385
+ def concern_extend_arguments(stmt)
386
+ return nil unless stmt.is_a?(Prism::CallNode)
387
+ return nil unless stmt.receiver.nil? || stmt.receiver.is_a?(Prism::SelfNode)
388
+
389
+ args = stmt.arguments&.arguments || []
390
+ return args if stmt.name == :extend
391
+ return nil unless CONCERN_SEND_CALLS.include?(stmt.name)
392
+
393
+ name = args.first
394
+ sent = name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
395
+ sent == "extend" ? args.drop(1) : nil
396
+ end
397
+
374
398
  def collect_included_do_calls(body)
375
399
  body.body.flat_map do |stmt|
376
400
  included_do = stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../run_scoped_slots"
4
- require_relative "memo_audit"
3
+ require_relative "../typing_state"
4
+ require_relative "../memo_audit"
5
5
 
6
6
  module Rigor
7
7
  module Inference
@@ -23,8 +23,14 @@ module Rigor
23
23
  # is bypassed while one is active, as the callee floor's other memos are.
24
24
  module ToplevelDefCensus
25
25
  OBJECT_OWNERS = %w[Object Kernel BasicObject].freeze
26
- RELIABLE_KEY = RunScopedSlots.register(:rigor_toplevel_def_reliable)
27
- private_constant :OBJECT_OWNERS, :RELIABLE_KEY
26
+ # ADR-129 WD1 — a run-scoped Memo, per discovery index and name: the answer is a function of both, and
27
+ # `MemoAudit` checks it on a hit.
28
+ RELIABLE = TypingState.memo(
29
+ :toplevel_def_reliable,
30
+ run_scoped: true, audit: "top-level def census",
31
+ reason: "keyed by discovery index, then name; the environment is the run's; audited by MemoAudit"
32
+ )
33
+ private_constant :OBJECT_OWNERS, :RELIABLE
28
34
 
29
35
  module_function
30
36
 
@@ -32,9 +38,11 @@ module Rigor
32
38
  def reliable?(scope, name)
33
39
  return compute?(scope, name) if Analysis::DependencyRecorder.active? || MemoAudit.suspended?
34
40
 
35
- table = (Thread.current[RELIABLE_KEY] ||= ObjectSpace::WeakKeyMap.new)
41
+ table = TypingState.current.weak_key_map(RELIABLE)
36
42
  by_name = (table[scope.discovery] ||= {})
37
- return by_name[name] = compute?(scope, name) unless by_name.key?(name)
43
+ unless by_name.key?(name)
44
+ return by_name[name] = MemoAudit.key_audit("top-level def census") { compute?(scope, name) }
45
+ end
38
46
 
39
47
  MemoAudit.check("top-level def census", by_name[name], name) { compute?(scope, name) }
40
48
  end
@@ -165,7 +165,7 @@ module Rigor
165
165
  arg.members.any? { |member| !member.is_a?(Type::Constant) && !gradual_top?(member) }
166
166
  end
167
167
 
168
- def gradual_top?(member) = member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Top)
168
+ def gradual_top?(member) = Type::Combinator.untyped?(member)
169
169
 
170
170
  # `Dynamic[top] | A | B` as `Dynamic[A | B]` for the class members; a literal member (a value-pinned key) and any
171
171
  # other element stay as they are.
@@ -57,7 +57,7 @@ module Rigor
57
57
  end
58
58
 
59
59
  def candidate?(entry_type, joined_type)
60
- entry_type.is_a?(Type::Dynamic) && entry_type.static_facet.is_a?(Type::Top) &&
60
+ Type::Combinator.untyped?(entry_type) &&
61
61
  joined_type.is_a?(Type::Union) && joined_type.members.any?(Type::Dynamic)
62
62
  end
63
63
 
@@ -5,7 +5,8 @@ require "prism"
5
5
  require_relative "void_origin"
6
6
  require_relative "../reflection"
7
7
  require_relative "../source/node_children"
8
- require_relative "../run_scoped_slots"
8
+ require_relative "../typing_state"
9
+ require_relative "../memo_audit"
9
10
 
10
11
  module Rigor
11
12
  module Inference
@@ -54,8 +55,8 @@ module Rigor
54
55
  # cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
55
56
  # is still mid-computation on the current chain (never yet memoised).
56
57
  class VoidTailSummary
57
- # Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot on
58
- # `Thread.current` keyed by the run-generation token's identity, so a re-run in one process (LSP,
58
+ # Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot in
59
+ # the thread's {TypingState} keyed by the run-generation token's identity, so a re-run in one process (LSP,
59
60
  # ADR-62 warm loop) lands in a fresh bucket and can neither serve stale entries nor accumulate
60
61
  # them. Thread-local ⇒ each fork-pool worker (a separate process) and each Ractor-pool worker (its
61
62
  # own Ractor-local `Thread.current`) builds its own bucket; the bucket hash is created and mutated
@@ -63,8 +64,15 @@ module Rigor
63
64
  # graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
64
65
  # {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
65
66
  # return memo relies on.
66
- MEMO_KEY = RunScopedSlots.register(:__rigor_void_tail_summary_memo__)
67
- private_constant :MEMO_KEY
67
+ #
68
+ # ADR-129 WD1 — a run-scoped Memo keyed on the token's identity: the bucket holds summaries that are pure
69
+ # functions of the def node they key on within one run.
70
+ MEMO = TypingState.memo(
71
+ :void_tail_summary,
72
+ run_scoped: true, audit: ["void tail origin", "void tail walk"],
73
+ reason: "a bucket of per-def-node summaries, keyed on the run-generation token's identity"
74
+ )
75
+ private_constant :MEMO
68
76
 
69
77
  def initialize(scope)
70
78
  @scope = scope
@@ -82,7 +90,11 @@ module Rigor
82
90
  return nil if def_node.nil?
83
91
 
84
92
  memo = memo_bucket
85
- return memo[def_node] if memo.key?(def_node)
93
+ if memo.key?(def_node)
94
+ return MemoAudit.check("void tail origin", memo[def_node], def_node) do
95
+ walk(def_node, owner, kind, {}.compare_by_identity)
96
+ end
97
+ end
86
98
 
87
99
  walk(def_node, owner, kind, {}.compare_by_identity)
88
100
  end
@@ -98,10 +110,14 @@ module Rigor
98
110
  return nil if visited.key?(def_node)
99
111
 
100
112
  memo = memo_bucket
101
- return memo[def_node] if memo.key?(def_node)
113
+ if memo.key?(def_node)
114
+ return MemoAudit.check("void tail walk", memo[def_node], def_node) do
115
+ walk(def_node, owner, kind, {}.compare_by_identity)
116
+ end
117
+ end
102
118
 
103
119
  visited[def_node] = true
104
- memo[def_node] = compute(def_node, owner, kind, visited)
120
+ memo[def_node] = MemoAudit.key_audit("void tail walk") { compute(def_node, owner, kind, visited) }
105
121
  end
106
122
 
107
123
  def compute(def_node, owner, kind, visited)
@@ -206,11 +222,14 @@ module Rigor
206
222
  end
207
223
 
208
224
  def memo_bucket
225
+ return {}.compare_by_identity if MemoAudit.enabled? && MemoAudit.suspended?
226
+
209
227
  generation = scope.run_generation || scope.discovered_def_nodes
210
- slot = Thread.current[MEMO_KEY]
228
+ state = TypingState.current
229
+ slot = state[MEMO]
211
230
  unless slot && slot[0].equal?(generation)
212
231
  slot = [generation, {}.compare_by_identity]
213
- Thread.current[MEMO_KEY] = slot
232
+ state[MEMO] = slot
214
233
  end
215
234
  slot[1]
216
235
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../environment/member_consistency"
4
+
5
+ module Rigor
6
+ module Lens
7
+ class Claims
8
+ # The deliberately small proof fragment of lens-claims.md. Unknown is not a failed subtype check.
9
+ class Comparison
10
+ def initialize(loader, relative_conflict:)
11
+ @loader = loader
12
+ @proof = Environment::MemberConsistency::RbsProof.new(loader)
13
+ @relative_conflict = relative_conflict
14
+ end
15
+
16
+ def call(sub, sup)
17
+ included = sub.map do |arm|
18
+ match = sup.filter_map do |other|
19
+ (rule = inclusion(arm, other)) && evidence(rule, arm, other.display)
20
+ end.first
21
+ match
22
+ end
23
+ return ["compatible", "proven_inclusion", included] if included.none?(nil)
24
+
25
+ sub.each do |arm|
26
+ rules = sup.map { |other| disjoint(arm, other) }
27
+ next if rules.any?(nil)
28
+
29
+ return ["conflict", "proven_disjoint_arm", [evidence(rules.first, arm, sup.map(&:display).join(" | "))]]
30
+ end
31
+ ["unknown", "unproven", []]
32
+ end
33
+
34
+ private
35
+
36
+ def evidence(rule, arm, against)
37
+ { "rule" => rule, "arm" => arm.display, "against" => against }
38
+ end
39
+
40
+ def inclusion(sub, sup)
41
+ return "equal" if sub.identity == sup.identity
42
+ return nil unless sup.kind == :class && sup.arguments.empty?
43
+ return nil unless @loader.class_type_param_names(sup.value).empty?
44
+
45
+ name = sub.kind == :literal ? sub.value.class.name : sub.value
46
+ unreliable = Environment::MemberConsistency::RbsProof::UNRELIABLE_ANCESTRY
47
+ return nil if unreliable.include?(name) || unreliable.include?(sup.value)
48
+ return nil unless @loader.ancestor_names_for(name).include?(sup.value)
49
+
50
+ sub.kind == :literal ? "literal_class_ancestor" : "class_ancestor"
51
+ end
52
+
53
+ def disjoint(left, right)
54
+ return nil if !@relative_conflict && (left.relative || right.relative)
55
+ return "distinct_literals" if left.kind == :literal && right.kind == :literal && !left.value.eql?(right.value)
56
+
57
+ disjoint_classes(left, right)
58
+ end
59
+
60
+ def disjoint_classes(left, right)
61
+ left_name = left.kind == :literal ? left.value.class.name : left.value
62
+ right_name = right.kind == :literal ? right.value.class.name : right.value
63
+ left_ancestors = @proof.class_ancestors(left_name)
64
+ right_ancestors = @proof.class_ancestors(right_name)
65
+ return nil if left_ancestors.nil? || right_ancestors.nil?
66
+
67
+ if left.kind == :literal
68
+ "literal_outside_class" unless left_ancestors.include?(right_name)
69
+ elsif right.kind == :literal
70
+ "literal_outside_class" unless right_ancestors.include?(left_name)
71
+ elsif !left_ancestors.include?(right_name) && !right_ancestors.include?(left_name)
72
+ "unrelated_classes"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Rigor
6
+ module Lens
7
+ class Claims
8
+ class IntegrityError < StandardError; end
9
+
10
+ # Validates transport and source identity before the incremental session is started.
11
+ class Request
12
+ attr_reader :claims, :digest, :paths
13
+
14
+ def self.read(path, root:, arguments:)
15
+ bytes = File.binread(path).force_encoding(Encoding::UTF_8)
16
+ raise IntegrityError, "claims request is not UTF-8" unless bytes.valid_encoding?
17
+
18
+ new(JSON.parse(bytes), digest: Lens.digest(bytes), root: root, arguments: arguments)
19
+ rescue SystemCallError, JSON::ParserError => e
20
+ raise IntegrityError, "cannot read claims request: #{e.message}"
21
+ end
22
+
23
+ def initialize(data, digest:, root:, arguments:)
24
+ @digest = digest
25
+ @root = root
26
+ @arguments = arguments.map { |argument| File.expand_path(argument, root) }
27
+ validate_header(data)
28
+ @claims = data.fetch("claims")
29
+ @files = data.fetch("files")
30
+ @paths = {}
31
+ @readable_paths = Set.new
32
+ @preflight = true
33
+ @files.each { |file| validate_file(file) }
34
+ ids = []
35
+ @claims.each do |claim|
36
+ validate_claim(claim)
37
+ ids << claim.fetch("id")
38
+ end
39
+ require_value(ids.uniq.size == ids.size, "duplicate claim id")
40
+ verify_sources!
41
+ @preflight = false
42
+ end
43
+
44
+ def verify_sources!(targets = [])
45
+ @files.each do |file|
46
+ absolute = @paths.fetch(file.fetch("path"))
47
+ bytes = readable(absolute)
48
+ target = targets.find { |entry| entry.absolute == absolute }
49
+ require_value(bytes || (!@readable_paths.include?(absolute) && !target&.reader),
50
+ "source became unreadable during analysis: #{file['path']}")
51
+ @readable_paths << absolute if @preflight && bytes
52
+ next unless bytes
53
+
54
+ require_value(Lens.digest(bytes) == file.fetch("digest"), "stale claims digest: #{file['path']}")
55
+ if target&.reader
56
+ require_value(target.reader.digest == file.fetch("digest"),
57
+ "source changed during analysis: #{file['path']}")
58
+ end
59
+ verify_spans(file, bytes.bytesize)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def verify_spans(file, size)
66
+ @claims.select { |claim| claim.fetch("file") == file.fetch("path") }.each do |claim|
67
+ %w[declaration tag].each do |field|
68
+ require_value(claim.fetch(field).fetch("span").fetch("end_offset") <= size,
69
+ "#{field} span outside #{file['path']}")
70
+ end
71
+ end
72
+ end
73
+
74
+ def validate_header(data)
75
+ require_value(data.is_a?(Hash), "claims request must be an object")
76
+ require_value(data["schema"] == "lens-claims-request/v0", "unsupported claims schema")
77
+ require_value(data["revision"].is_a?(Integer) && data["revision"] == 1, "unsupported claims revision")
78
+ require_value(data["grammar"] == "rbs", "unsupported claims grammar")
79
+ %w[files claims].each { |field| require_value(data[field].is_a?(Array), "#{field} must be an array") }
80
+ end
81
+
82
+ def validate_file(file)
83
+ strings(file, %w[path digest])
84
+ path = file.fetch("path")
85
+ absolute = File.expand_path(path, @root)
86
+ relative = Pathname(absolute).relative_path_from(Pathname(@root)).to_s
87
+ require_value(path == relative, "file path must be root-relative: #{path}")
88
+ require_value(@arguments.include?(absolute), "claims file is not a target: #{path}")
89
+ require_value(!@paths.key?(path), "duplicate claims file: #{path}")
90
+ require_value(file.fetch("digest").match?(/\Axxh3-64:[0-9a-f]{16}\z/), "invalid claims file digest")
91
+ @paths[path] = absolute
92
+ end
93
+
94
+ def validate_claim(claim)
95
+ strings(claim, %w[id file claim])
96
+ require_value(@paths.key?(claim.fetch("file")), "claim file is missing from files")
97
+ declaration = claim["declaration"]
98
+ strings(declaration, %w[kind owner scope name])
99
+ tag = claim["tag"]
100
+ strings(tag, %w[name])
101
+ require_value(tag.key?("subject") && (tag["subject"].nil? || tag["subject"].is_a?(String)),
102
+ "tag subject must be a string or null")
103
+ [declaration, tag].each { |entry| validate_span(entry["span"]) }
104
+ end
105
+
106
+ def validate_span(span)
107
+ require_value(span.is_a?(Hash), "span must be an object")
108
+ start = span["start_offset"]
109
+ finish = span["end_offset"]
110
+ require_value(start.is_a?(Integer) && finish.is_a?(Integer) && start >= 0 && finish >= start,
111
+ "invalid byte span")
112
+ end
113
+
114
+ def strings(object, fields)
115
+ require_value(object.is_a?(Hash), "claims entry must be an object")
116
+ fields.each { |field| require_value(object[field].is_a?(String), "#{field} must be a string") }
117
+ end
118
+
119
+ def require_value(condition, message)
120
+ raise IntegrityError, message unless condition
121
+ end
122
+
123
+ def readable(path)
124
+ File.binread(path) if File.file?(path)
125
+ rescue SystemCallError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+ require_relative "../../rbs_extended"
5
+ require_relative "../../scope/resolution_chain"
6
+
7
+ module Rigor
8
+ module Lens
9
+ class Claims
10
+ # Normalizes RBS ASTs without erasing unsupported forms or using display text as identity.
11
+ class TypeReader
12
+ Atom = Data.define(:kind, :value, :arguments, :relative) do
13
+ def identity
14
+ [kind, value, arguments.map { |arms| arms.to_set(&:identity) }]
15
+ end
16
+
17
+ def display
18
+ return value.inspect if kind == :literal
19
+
20
+ suffix = arguments.empty? ? "" : "[#{arguments.map { |arms| arms.map(&:display).join(' | ') }.join(', ')}]"
21
+ "::#{value}#{suffix}"
22
+ end
23
+ end
24
+
25
+ attr_reader :reasons
26
+
27
+ def self.parse(text)
28
+ # An empty, blank or comment-only text parses to nil rather than raising.
29
+ type = ::RBS::Parser.parse_type(text, require_eof: true)
30
+ type ? [type, nil] : [nil, "syntax_error"]
31
+ rescue ::RBS::ParsingError
32
+ extended = RbsExtended.parse_return_type_override("rigor:v1:return: #{text}")
33
+ [nil, extended ? "unsupported_grammar_feature" : "syntax_error"]
34
+ end
35
+
36
+ # `owner` is the member's owner; an RBS declaration of the member sits in its namespace, which a builder
37
+ # block's lexical nesting does not name.
38
+ def initialize(scope:, nesting:, singleton_nesting: false, owner: nil)
39
+ @scope = scope
40
+ @loader = scope.environment.rbs_loader
41
+ @resolver = @loader.type_name_resolver
42
+ @nesting = nesting
43
+ @singleton_nesting = singleton_nesting
44
+ @context = rbs_context(Array(nesting))
45
+ @owner_context = owner && owner != "Object" ? rbs_context(owner_namespaces(owner)) : nil
46
+ @reasons = Set.new
47
+ end
48
+
49
+ def read(type, claim: false, aliases: Set.new)
50
+ case type
51
+ when ::RBS::Types::Bases::Any
52
+ @reasons << "dynamic_type"
53
+ []
54
+ when ::RBS::Types::Bases::Bool then [literal(true), literal(false)]
55
+ when ::RBS::Types::Bases::Nil then [literal(nil)]
56
+ when ::RBS::Types::Literal then [literal(type.literal)]
57
+ when ::RBS::Types::Union then type.types.flat_map { |part| read(part, claim: claim, aliases: aliases) }
58
+ when ::RBS::Types::Optional then read(type.type, claim: claim, aliases: aliases) + [literal(nil)]
59
+ when ::RBS::Types::Alias then read_alias(type, claim, aliases)
60
+ when ::RBS::Types::ClassInstance then read_class(type, claim, aliases)
61
+ else
62
+ @reasons << "unsupported_type"
63
+ type.each_type { |part| read(part, claim: claim, aliases: aliases) }
64
+ []
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def rbs_context(names)
71
+ names.reduce(nil) do |outer, name|
72
+ type_name = ::RBS::TypeName.parse("::#{name}")
73
+ @loader.class_known?(name) ? [outer, type_name] : outer
74
+ end
75
+ end
76
+
77
+ def owner_namespaces(owner)
78
+ segments = owner.split("::")
79
+ segments.each_index.map { |index| segments[0..index].join("::") }
80
+ end
81
+
82
+ def literal(value, relative: false)
83
+ Atom.new(kind: :literal, value: value, arguments: [], relative: relative)
84
+ end
85
+
86
+ def resolve(name, claim)
87
+ resolved = @resolver&.resolve(name, context: claim ? @context : nil)
88
+ if resolved.nil?
89
+ @reasons << "unresolved_name"
90
+ elsif claim && !name.absolute? && name.class?
91
+ compare_ruby_name(name, resolved)
92
+ end
93
+ resolved
94
+ end
95
+
96
+ def compare_ruby_name(name, resolved)
97
+ # An unlocated alias body has no known cref. A singleton class's anonymous cref is not a
98
+ # named namespace either; its constant table and ancestry cannot be read as the owner's.
99
+ if @nesting.nil? || @singleton_nesting
100
+ @reasons << "ambiguous_name"
101
+ return
102
+ end
103
+
104
+ ruby = ruby_name(name.to_s)
105
+ @reasons << "ambiguous_name" if ruby && ruby != resolved.to_s.delete_prefix("::")
106
+ end
107
+
108
+ def read_class(type, claim, aliases)
109
+ name = resolve(type.name, claim)
110
+ arguments = type.args.map { |arg| read(arg, claim: claim, aliases: aliases) }
111
+ return [] unless name
112
+
113
+ key = name.to_s.delete_prefix("::")
114
+ if !@loader.class_known?(key) || arguments.size != @loader.class_type_param_names(key).size
115
+ @reasons << "unsupported_type"
116
+ end
117
+ singles = { "TrueClass" => true, "FalseClass" => false, "NilClass" => nil }
118
+ if arguments.empty? && singles.key?(key)
119
+ return [literal(singles.fetch(key), relative: claim && !type.name.absolute?)]
120
+ end
121
+
122
+ [Atom.new(kind: :class, value: key, arguments: arguments, relative: claim && !type.name.absolute?)]
123
+ end
124
+
125
+ def read_alias(type, claim, aliases)
126
+ name = resolve(type.name, claim)
127
+ type.args.each { |arg| read(arg, claim: claim, aliases: aliases) }
128
+ return [] unless name
129
+
130
+ relative = claim && !type.name.absolute?
131
+ return [] if relative && !alias_name_settled?(type.name, name)
132
+
133
+ entry = @loader.type_alias_declaration(name)
134
+ if entry.nil? || !entry.type_params.empty? || !type.args.empty? || aliases.include?(name)
135
+ @reasons << "unsupported_type"
136
+ return []
137
+ end
138
+ # The resolved declaration binds names in the alias's namespace, not the claim's lexical scope. Arms
139
+ # reached through a relatively written name stay relative, so the conflict guard still applies.
140
+ arms = read(entry.type, aliases: aliases | [name])
141
+ relative ? arms.map { |arm| arm.with(relative: true) } : arms
142
+ end
143
+
144
+ # A type alias has no Ruby meaning, so the only reading to cross-check is the RBS one: the claim's lexical
145
+ # context and the member's RBS namespace must resolve a relative alias name alike. An unlocated body or a
146
+ # singleton-class body has no context to read it in.
147
+ def alias_name_settled?(written, resolved)
148
+ if @nesting.nil? || @singleton_nesting
149
+ @reasons << "ambiguous_name"
150
+ return false
151
+ end
152
+ return true if @owner_context.nil? || @owner_context == @context
153
+
154
+ declared = @resolver&.resolve(written, context: @owner_context)
155
+ return true if declared == resolved
156
+
157
+ @reasons << "ambiguous_name"
158
+ false
159
+ end
160
+
161
+ # Project constants only: a missing entry falls back to RBS, which also knows libraries and gems.
162
+ # Resolve the first segment before the rest, so a Ruby-only namespace cannot fall through to core.
163
+ def ruby_name(written)
164
+ head, *tail = written.split("::")
165
+ found = project_constant_in(@nesting.reverse, head)
166
+ found ||= ancestor_constant(head)
167
+ found = head if found.nil? && (@nesting.empty? || @loader.rbs_module?(@nesting.last)) && project_name?(head)
168
+ found && ([found] + tail).join("::")
169
+ end
170
+
171
+ def project_constant_in(bases, name)
172
+ bases.each do |base|
173
+ candidate = base == "Object" ? name : "#{base}::#{name}"
174
+ return candidate if project_name?(candidate)
175
+ end
176
+ nil
177
+ end
178
+
179
+ def project_name?(name)
180
+ @scope.declared_class?(name) || @scope.bound_constant_names(name).include?(name)
181
+ end
182
+
183
+ def ancestor_constant(name)
184
+ chain = Scope::ResolutionChain.for(@scope, @nesting.last || "Object", :instance, :constants)
185
+ found = chain_constant(chain, name)
186
+ settled = chain.settle(@scope, found) { |retro| chain_constant(retro, name) }
187
+ if chain.truncated? || chain.wildcard_mixin? || settled != :chain || opaque_ancestry?(chain)
188
+ @reasons << "ambiguous_name"
189
+ return nil
190
+ end
191
+ found
192
+ end
193
+
194
+ # A superclass the tables cannot name (`Class.new(Base)`, `< pick`, a builder class's `Struct`/`Data`) or an
195
+ # external ancestor no RBS declares (`< SomeAlias`): the constants it holds are not on the chain.
196
+ def opaque_ancestry?(chain)
197
+ return true if chain.opaque_tail?(@scope)
198
+
199
+ chain.entries.any? { |entry| entry.external? && entry.candidates.none? { |name| @loader.class_known?(name) } }
200
+ end
201
+
202
+ def chain_constant(chain, name)
203
+ chain.search(@scope) do |entry|
204
+ bases = entry.external? ? external_ancestors(entry) : [entry.name]
205
+ project_constant_in(bases, name)
206
+ end
207
+ end
208
+
209
+ def external_ancestors(entry)
210
+ name = entry.candidates.find { |candidate| @loader.class_known?(candidate) }
211
+ name ? @loader.ancestor_names_for(name) : []
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end