rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -11,18 +11,29 @@ module Rigor
11
11
  ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
12
12
  private_constant :ANCESTOR_SCOPES_KEY
13
13
 
14
+ # The recording run's slot. See {.recorded_ancestor_constant_scopes}.
15
+ RECORDED_ANCESTOR_SCOPES_KEY = :__rigor_recorded_ancestor_constant_scopes__
16
+ private_constant :RECORDED_ANCESTOR_SCOPES_KEY
17
+
14
18
  module_function
15
19
 
16
20
  # #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
17
- # ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
18
- # transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
21
+ # ancestor order: the entries of `class_name`'s `Scope::ResolutionChain` — prepended modules, then
22
+ # included ones, then the superclass's chain likewise — each once. `class_name` itself is excluded:
23
+ # step 1 already covered it.
24
+ #
25
+ # Issue #1571 — this was a breadth-first walk of its own, which is not Ruby's order: `class C < Base;
26
+ # include A` with `A` including `M` reached `Base::X` (depth 1) before `M::X` (depth 2), while Ruby's
27
+ # ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
28
+ # project class also admits a module that holds only constants.
19
29
  #
20
- # Only PROJECT ancestors appear. `Scope#superclass_of` / `#includes_of` carry as-written names
21
- # from the discovery pre-pass, and an as-written name that resolves to no discovered class or
22
- # module is dropped — so a `class Foo < ActiveRecord::Base` contributes nothing and a constant
23
- # owned by an RBS-known ancestor still resolves only if the bare name reaches it at step 3. That
24
- # gap is deliberate for this slice: widening to the RBS ancestor graph is a separate question
25
- # with its own FP surface.
30
+ # Project ancestors appear, and (issue #1698) a module the class body `include`s or `prepend`s that
31
+ # only RBS or the class registry declares: `include AcLibraryRb` puts `AcLibraryRb` at its Ruby
32
+ # position, so `Segtree` inside the class reads `AcLibraryRb::Segtree` as Ruby does. Such an entry
33
+ # contributes its OWN constants only; the RBS ancestry behind it is not expanded, as for every external
34
+ # entry. An external SUPERCLASS (`class Foo < ActiveRecord::Base`) still contributes nothing, and a
35
+ # constant it owns resolves only if the bare name reaches it at step 3: widening to the superclass's
36
+ # RBS graph is a separate question with its own FP surface. See {.external_constant_owner}.
26
37
  #
27
38
  # Memoised per run because step 2 runs on every constant reference whose lexical candidates all
28
39
  # miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
@@ -30,11 +41,49 @@ module Rigor
30
41
  # back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
31
42
  # ADR-62 warm loop) cannot hit stale entries.
32
43
  def ancestor_constant_scopes(class_name, scope)
33
- # ADR-46: `superclass_of` / `includes_of` record a cross-file class dependency per consumer
34
- # file, and the memo is run-scoped rather than file-scoped — a hit would skip the recording and
35
- # under-record the edge for every later file. Recording runs are rare (incremental only), so
36
- # they simply bypass the memo rather than complicate its key.
37
- return compute_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
44
+ ancestor_constant_worlds(class_name, scope).first
45
+ end
46
+ private_class_method :ancestor_constant_scopes
47
+
48
+ # ADR-24 / #1570 — the first ancestor that owns a constant, asked as {.ancestor_constant_scopes} orders
49
+ # them and settled by `Scope::ResolutionChain#settle`: the block's first truthy answer where the chain
50
+ # stands, and otherwise the answer of the breadth-first order this rung used before the chain
51
+ # (`Scope::ResolutionChain::MasterOrder`) — where a skipped `include` would put a different owner first,
52
+ # which constant Ruby reads depends on the order the bodies ran.
53
+ def agreed_ancestor_hit(class_name, scope, &)
54
+ scopes, chain = ancestor_constant_worlds(class_name, scope)
55
+ owner, hit = first_ancestor_hit(scopes, &)
56
+ verdict = chain.settle(scope, owner, owner: owner) do |retro|
57
+ first_ancestor_hit(constant_scopes_of(retro, class_name, scope), &)&.first
58
+ end
59
+ return hit if verdict == :chain
60
+
61
+ first_ancestor_hit(master_constant_scopes(class_name, scope), &)&.last
62
+ end
63
+ private_class_method :agreed_ancestor_hit
64
+
65
+ def master_constant_scopes(class_name, scope)
66
+ Scope::ResolutionChain::MasterOrder.breadth_first(scope, class_name.to_s, :constants)
67
+ end
68
+ private_class_method :master_constant_scopes
69
+
70
+ def first_ancestor_hit(entries)
71
+ entries.each do |entry|
72
+ hit = yield entry
73
+ return [entry, hit] if hit
74
+ end
75
+ nil
76
+ end
77
+ private_class_method :first_ancestor_hit
78
+
79
+ # `[scopes, chain]` — {.ancestor_constant_scopes} and the chain they were read from, which settles the
80
+ # answer.
81
+ def ancestor_constant_worlds(class_name, scope)
82
+ # ADR-46: the answer depends on every class on the chain, and the memo is run-scoped rather than
83
+ # file-scoped — a hit would skip the recording and under-record the edge for every later file. A
84
+ # recording run reads the chain, which is memoised per discovery index, and files its edges on
85
+ # every call.
86
+ return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
38
87
 
39
88
  generation = scope.run_generation || scope.discovered_superclasses
40
89
  slot = Thread.current[ANCESTOR_SCOPES_KEY]
@@ -45,48 +94,164 @@ module Rigor
45
94
  bucket = slot[1]
46
95
  bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
47
96
  end
48
- private_class_method :ancestor_constant_scopes
97
+ private_class_method :ancestor_constant_worlds
98
+
99
+ # The recording run's memo. The answer is every project entry of the chain, so its edges are the
100
+ # root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
101
+ # hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
102
+ # both read from it, so the key is exact, and the slot never pins more than the index it serves.
103
+ def recorded_ancestor_constant_scopes(class_name, scope)
104
+ discovery = scope.discovery
105
+ slot = Thread.current[RECORDED_ANCESTOR_SCOPES_KEY]
106
+ unless slot && slot[0].equal?(discovery)
107
+ slot = [discovery, {}]
108
+ Thread.current[RECORDED_ANCESTOR_SCOPES_KEY] = slot
109
+ end
110
+ bucket = slot[1]
111
+ worlds = bucket[class_name]
112
+ return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
113
+
114
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
115
+ chain.record(scope)
116
+ record_external_owner_edges(chain)
117
+ worlds
118
+ end
119
+ private_class_method :recorded_ancestor_constant_scopes
49
120
 
121
+ # Reads the chain whole, so a recording run files every class on it (a no-op otherwise).
50
122
  def compute_ancestor_constant_scopes(class_name, scope)
51
- queue = [class_name]
52
- seen = { class_name => true }
123
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
124
+ chain.record(scope)
125
+ record_external_owner_edges(chain)
126
+ scopes = constant_scopes_of(chain, class_name, scope)
127
+ [scopes, chain].freeze
128
+ end
129
+ private_class_method :compute_ancestor_constant_scopes
130
+
131
+ # The chain's project entries in order, each once, without `class_name` itself, with each mixin entry
132
+ # the project does not declare standing as the module {.external_constant_owner} names.
133
+ def constant_scopes_of(chain, class_name, scope)
53
134
  out = []
54
- until queue.empty?
55
- current = queue.shift
56
- # Mixins first, then the superclass — Ruby's ancestor order.
57
- scope.includes_of(current).each do |raw|
58
- resolved = resolve_ancestor_name(current, raw, scope)
59
- next if resolved.nil? || seen[resolved]
60
-
61
- seen[resolved] = true
62
- out << resolved
63
- queue << resolved
64
- end
65
- raw_super = scope.superclass_of(current)
66
- next if raw_super.nil?
67
-
68
- resolved_super = resolve_ancestor_name(current, raw_super, scope)
69
- next if resolved_super.nil? || seen[resolved_super]
70
-
71
- seen[resolved_super] = true
72
- out << resolved_super
73
- queue << resolved_super
135
+ chain.entries.each do |entry|
136
+ name = entry.external? ? external_constant_owner(entry, scope) : entry.name
137
+ out << name unless name.nil? || name == class_name || out.include?(name)
74
138
  end
75
139
  out.freeze
76
140
  end
77
- private_class_method :compute_ancestor_constant_scopes
141
+ private_class_method :constant_scopes_of
142
+
143
+ # Issue #1698 — the module an external `include` / `prepend` entry names, when the constant ladder may
144
+ # read its constants, or nil. Ruby resolved the spelling at the `include` lexically, so the first of the
145
+ # entry's candidates (most qualified first) that exists is the module. The answer is the first candidate
146
+ # RBS or the class registry knows, and nil when a nearer candidate is a constant the project binds
147
+ # ({.project_written_candidate?}): Ruby's module is then that constant, whose constants nothing here
148
+ # describes. The candidates are the entry's own, behind the namespace of the body that wrote the edge
149
+ # (`entry.owner`), which Ruby searches first and the chain's resolver does not consult. An ambiguous
150
+ # spelling has no candidates and answers nil. A superclass edge answers nil (see
151
+ # {.ancestor_constant_scopes}).
152
+ def external_constant_owner(entry, scope)
153
+ return nil if entry.superclass_edge || entry.candidates.empty?
154
+
155
+ env = scope.environment
156
+ external_owner_candidates(entry).each do |candidate|
157
+ return nil if project_written_candidate?(candidate, scope)
158
+ return candidate if env.class_known?(candidate)
159
+ end
160
+ nil
161
+ end
162
+ private_class_method :external_constant_owner
163
+
164
+ def external_owner_candidates(entry)
165
+ return entry.candidates if entry.owner.nil? || entry.raw.start_with?("::")
166
+
167
+ ["#{entry.owner}::#{entry.raw}", *entry.candidates]
168
+ end
169
+ private_class_method :external_owner_candidates
170
+
171
+ # ADR-46 — the name edges of {.external_constant_owner}'s guard, for every mixin entry it read: a file that
172
+ # starts writing or declaring a nearer spelling of the module's name (`User::AcLibraryRb = …` in a file the
173
+ # reader has no other edge to) moves the answer. Keyed on the spelling's last segment, as the ladder's own
174
+ # `constant:` and `class:` edges are. A memo hit replays them beside the chain's own edges.
175
+ def record_external_owner_edges(chain)
176
+ return unless Analysis::DependencyRecorder.active?
177
+
178
+ chain.entries.each do |entry|
179
+ next unless entry.external? && !entry.superclass_edge
180
+
181
+ Analysis::DependencyRecorder.read_last_segment(:constant, entry.raw)
182
+ Analysis::DependencyRecorder.read_last_segment(:class, entry.raw)
183
+ end
184
+ end
185
+ private_class_method :record_external_owner_edges
186
+
187
+ # Issue #1698 — whether the walk must stop at `owner` because a module its RBS ancestry holds may own the name
188
+ # (`candidate` answers for it). Ruby searches that ancestry right after `owner`, before any later entry of
189
+ # the chain and before the top level, and the chain does not expand it. Where no module of it owns the
190
+ # name, where they sit does not matter to this name and the walk goes on.
191
+ #
192
+ # `owner` is an RBS-only mixin (`external`), or a project entry RBS also declares: a project file that
193
+ # reopens an RBS module without restating its `include`s makes it a project entry whose chain lists only
194
+ # the edges the project writes, so its ancestry is read less what that chain lists. The ancestry comes from
195
+ # the declarations (`RbsLoader#declared_ancestry`), so a definition that fails to build still yields what
196
+ # it declares. Where it cannot be read whole, {.ancestry_unknown?} keeps the later RBS-only mixins out
197
+ # instead of stopping here.
198
+ def mixin_ancestry_stop?(owner, scope, external)
199
+ loader = rbs_loader_for(scope, nil)
200
+ return false if loader.nil? || (!external && !loader.class_known?(owner))
201
+
202
+ names, = loader.declared_ancestry(owner)
203
+ listed = nil
204
+ names.any? do |ancestor|
205
+ next false if IMPLICIT_ANCESTORS.include?(ancestor)
206
+
207
+ listed ||= external ? EMPTY_NAMES : project_chain_names(owner, scope)
208
+ !listed.include?(ancestor) && yield(ancestor)
209
+ end
210
+ end
211
+ private_class_method :mixin_ancestry_stop?
212
+
213
+ # Issue #1698 — whether an entry ahead of `entry` on `class_name`'s list has an RBS ancestry that cannot be
214
+ # read whole ({.mixin_ancestry_stop?}): a module it hides may own the name, so `entry`, an RBS-only mixin,
215
+ # MUST NOT answer. Only those entries are kept out; project entries, the superclass chain and the top level
216
+ # answer as they did before #1698.
217
+ def ancestry_unknown_before?(class_name, entry, scope)
218
+ loader = rbs_loader_for(scope, nil)
219
+ return true if loader.nil?
220
+
221
+ ancestor_constant_scopes(class_name, scope).each do |earlier|
222
+ break if earlier == entry
223
+ next unless loader.class_known?(earlier)
224
+
225
+ return true unless loader.declared_ancestry(earlier)[1]
226
+ end
227
+ false
228
+ end
229
+ private_class_method :ancestry_unknown_before?
230
+
231
+ # Ruby places these after every entry of a chain, so a constant they own is the top level's (step 4).
232
+ IMPLICIT_ANCESTORS = %w[Object Kernel BasicObject].freeze
233
+ private_constant :IMPLICIT_ANCESTORS
234
+
235
+ EMPTY_NAMES = [].freeze
236
+ private_constant :EMPTY_NAMES
237
+
238
+ # Every name `owner`'s own instance chain lists: its project entries and each external entry's candidates.
239
+ def project_chain_names(owner, scope)
240
+ Scope::ResolutionChain.for(scope, owner, :instance, :constants).entries.flat_map do |entry|
241
+ entry.external? ? entry.candidates : [entry.name]
242
+ end
243
+ end
244
+ private_class_method :project_chain_names
78
245
 
79
- # Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the nesting in
80
- # force where the subclass's header is written — `Scope#ancestor_name_candidates`, the single
81
- # owner of that order, which `Scope#enqueue_ancestors` reads for method lookup and the
82
- # override-visibility rule reads for its own walk. Returns nil when no candidate names a
83
- # discovered project class or module.
84
- def resolve_ancestor_name(subclass_qualified, raw, scope)
85
- scope.ancestor_name_candidates(subclass_qualified, raw)
86
- .find { |candidate| known_project_namespace?(candidate, scope) }
246
+ # Whether the project binds `candidate` itself: a namespace under the `:constants` flavor's tables, or a
247
+ # constant write the census records (#1290), typed or not.
248
+ def project_written_candidate?(candidate, scope)
249
+ known_project_namespace?(candidate, scope) || scope.shadowing_constant_names(candidate).include?(candidate)
87
250
  end
88
- private_class_method :resolve_ancestor_name
251
+ private_class_method :project_written_candidate?
89
252
 
253
+ # Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
254
+ # resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
90
255
  def known_project_namespace?(name, scope)
91
256
  scope.discovered_superclasses.key?(name) ||
92
257
  scope.discovered_includes.key?(name) ||
@@ -81,7 +81,10 @@ module Rigor
81
81
 
82
82
  prefix = caller_derived ? enclosing_class_path(scope) : nil
83
83
  unless prefix.nil? || prefix.empty?
84
- hit = first_namespace_hit(bounded_ancestor_scopes(prefix, scope), head, scope)
84
+ hit = bounded_agreed_hit(prefix, scope) do |entry|
85
+ candidate = "#{entry}::#{head}"
86
+ candidate if namespace_known?(candidate, scope)
87
+ end
85
88
  return hit if hit
86
89
  end
87
90
 
@@ -106,28 +109,51 @@ module Rigor
106
109
  candidate = "#{owner}::#{segment}"
107
110
  return candidate if yield(candidate)
108
111
 
109
- bounded_ancestor_scopes(owner, scope).each do |ancestor|
112
+ bounded_agreed_hit(owner, scope) do |ancestor|
110
113
  inherited = "#{ancestor}::#{segment}"
111
- return inherited if yield(inherited)
114
+ inherited if yield(inherited)
112
115
  end
113
- nil
114
116
  end
115
117
  private_class_method :constant_in_namespace
116
118
 
119
+ # {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
120
+ # `Scope::ResolutionChain#settle` lets the chain stand, and otherwise the breadth-first order's.
121
+ #
122
+ # Issue #1698 — the path walk reads the project entries only: an RBS-only mixin's own ancestry is not on the
123
+ # chain, so the rung that reads such a mixin also stops behind it ({.mixin_ancestry_stop?}), and this
124
+ # walk keeps the answer it gave before.
125
+ def bounded_agreed_hit(class_name, scope, &)
126
+ scopes, chain = ancestor_constant_worlds(class_name, scope)
127
+ owner, hit = first_ancestor_hit(bounded(project_scopes(scopes, scope)), &)
128
+ verdict = chain.settle(scope, owner, owner: owner) do |retro|
129
+ first_ancestor_hit(bounded(project_scopes(constant_scopes_of(retro, class_name, scope), scope)), &)&.first
130
+ end
131
+ return hit if verdict == :chain
132
+
133
+ first_ancestor_hit(bounded(master_constant_scopes(class_name, scope)), &)&.last
134
+ end
135
+ private_class_method :bounded_agreed_hit
136
+
137
+ def project_scopes(scopes, scope)
138
+ return scopes if scopes.all? { |name| known_project_namespace?(name, scope) }
139
+
140
+ scopes.select { |name| known_project_namespace?(name, scope) }
141
+ end
142
+ private_class_method :project_scopes
143
+
117
144
  # How many of {.ancestor_constant_scopes}' entries one segment may consult, under
118
145
  # `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
119
146
  # graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
120
147
  # itself is the memoised one step 2 builds, so this bounds the per-segment consultation the walk
121
148
  # adds and not the graph. Reported through the shared counter, so a truncated walk is visible in a
122
149
  # budget trace instead of looking like a plain miss.
123
- def bounded_ancestor_scopes(class_name, scope)
124
- scopes = ancestor_constant_scopes(class_name, scope)
150
+ def bounded(scopes)
125
151
  return scopes if scopes.size <= Scope::ANCESTOR_WALK_LIMIT
126
152
 
127
153
  Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
128
154
  scopes.first(Scope::ANCESTOR_WALK_LIMIT)
129
155
  end
130
- private_class_method :bounded_ancestor_scopes
156
+ private_class_method :bounded
131
157
 
132
158
  # Whether `name` can OWN a constant. `class_known?` covers the RBS and registry classes and modules;
133
159
  # the project tables cover a namespace the project declares with no RBS for it.
@@ -50,6 +50,11 @@ module Rigor
50
50
  WRITTEN_CANDIDATE = Object.new.freeze
51
51
  private_constant :WRITTEN_CANDIDATE
52
52
 
53
+ # Issue #1698 — what the constant ladder's ancestor rung answers where an entry's RBS ancestry may
54
+ # own the name ({.mixin_ancestry_stop?}). Never a type: {.lexical_constant_type} replaces it.
55
+ MIXIN_ANCESTRY_STOP = Object.new.freeze
56
+ private_constant :MIXIN_ANCESTRY_STOP
57
+
53
58
  module_function
54
59
 
55
60
  def class_known?(class_name, scope: Scope.empty)
@@ -197,10 +202,19 @@ module Rigor
197
202
  # `P::A < P::Base` is `P::Base::B`, and step 3's `A::B` is a real but different class.
198
203
  #
199
204
  # Step 3 — the bare name (top level). It takes no `shadows`: nothing sits below it for a stop to replace.
200
- first_constant_hit(lexical_nesting_chain(scope), name, scope, shadows) ||
201
- ancestor_constant_type(name, scope, enclosing_class_path(scope), shadows) ||
202
- constant_path_type(name, scope, shadows: shadows) ||
203
- constant_type_at(name, scope)
205
+ #
206
+ # Issue #1698 — step 2 answers {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's own ancestry may own the name
207
+ # ({.mixin_ancestry_stop?}). Ruby reads that ancestry before any later ancestor or the top level, and
208
+ # the chain does not list it, so a lower rung's answer is replaced by `Dynamic[top]`, as #1290's stop is.
209
+ lexical = first_constant_hit(lexical_nesting_chain(scope), name, scope, shadows)
210
+ return lexical if lexical
211
+
212
+ ancestor = ancestor_constant_type(name, scope, enclosing_class_path(scope), shadows)
213
+ stopped = ancestor.equal?(MIXIN_ANCESTRY_STOP)
214
+ return ancestor if ancestor && !stopped
215
+
216
+ below = constant_path_type(name, scope, shadows: shadows) || constant_type_at(name, scope)
217
+ stopped && below ? Type::Combinator.untyped : below
204
218
  end
205
219
  private_class_method :lexical_constant_type
206
220
 
@@ -216,11 +230,28 @@ module Rigor
216
230
  end
217
231
  private_class_method :first_constant_hit
218
232
 
219
- # Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of.
233
+ # Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of, and
234
+ # {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's ancestry may own the name.
235
+ #
236
+ # Issue #1698 — an entry that stands for an RBS-only mixin ({.external_constant_owner}) is skipped where `self`
237
+ # is a class object: a `class << self` body and a `def` in it run under the singleton class's cref, whose
238
+ # ancestors are not the class's ([#1305](https://github.com/rigortype/rigor/issues/1305)), and the scope does
239
+ # not tell such a body from a `def self.m` or the class body. Skipping keeps the answer this rung gave before
240
+ # #1698 there.
220
241
  def ancestor_constant_type(name, scope, prefix, shadows)
221
242
  return nil if prefix.nil? || prefix.empty?
222
243
 
223
- first_constant_hit(ancestor_constant_scopes(prefix, scope), name, scope, shadows)
244
+ class_object_self = scope.self_type.is_a?(Type::Singleton)
245
+ agreed_ancestor_hit(prefix, scope) do |entry|
246
+ external = !known_project_namespace?(entry, scope)
247
+ next nil if external && (class_object_self || ancestry_unknown_before?(prefix, entry, scope))
248
+
249
+ candidate = ->(owner) { constant_type_at("#{owner}::#{name}", scope, shadows) }
250
+ hit = candidate.call(entry)
251
+ next hit if hit
252
+
253
+ MIXIN_ANCESTRY_STOP if mixin_ancestry_stop?(entry, scope, external, &candidate)
254
+ end
224
255
  end
225
256
  private_class_method :ancestor_constant_type
226
257
 
@@ -264,8 +295,9 @@ module Rigor
264
295
  prefix = enclosing_class_path(scope)
265
296
  return nil if prefix.nil? || prefix.empty?
266
297
 
267
- first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
268
- ancestor_constant_type(name, scope, prefix, shadows)
298
+ hit = first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
299
+ ancestor_constant_type(name, scope, prefix, shadows)
300
+ hit.equal?(MIXIN_ANCESTRY_STOP) ? nil : hit
269
301
  end
270
302
  private_class_method :caller_derived_constant_type
271
303
 
@@ -322,8 +354,7 @@ module Rigor
322
354
  # ({Analysis::Incremental.changed_constant_publications}). Gated by the caller on the recorder being
323
355
  # active, so an ordinary run pays one integer read.
324
356
  def record_constant_reference(name)
325
- segment = name.delete_prefix("::").split("::").last
326
- Analysis::DependencyRecorder.read_name(:constant, segment) if segment
357
+ Analysis::DependencyRecorder.read_last_segment(:constant, name)
327
358
  end
328
359
  private_class_method :record_constant_reference
329
360