rubydex 0.2.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -1,7 +1,4 @@
1
- use std::{
2
- collections::{HashSet, VecDeque, hash_map::Entry},
3
- hash::BuildHasher,
4
- };
1
+ use std::collections::{HashSet, VecDeque, hash_map::Entry};
5
2
 
6
3
  use crate::diagnostic::{Diagnostic, Rule};
7
4
  use crate::model::{
@@ -14,8 +11,9 @@ use crate::model::{
14
11
  definitions::{Definition, Mixin, Receiver},
15
12
  graph::{Graph, Unit},
16
13
  identity_maps::{IdentityHashBuilder, IdentityHashMap, IdentityHashSet},
17
- ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId},
14
+ ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId, UriId},
18
15
  name::{Name, NameRef, ParentScope},
16
+ visibility::Visibility,
19
17
  };
20
18
 
21
19
  enum Outcome {
@@ -50,11 +48,32 @@ enum SingletonAncestors {
50
48
  Deferred,
51
49
  }
52
50
 
51
+ #[derive(Default)]
52
+ struct ChainState {
53
+ cyclic: bool,
54
+ partial: bool,
55
+ }
56
+
57
+ impl ChainState {
58
+ /// Fold a nested linearization's result into this frame's state, returning its ancestor list
59
+ fn fold(&mut self, ancestors: Ancestors) -> Vec<Ancestor> {
60
+ match ancestors {
61
+ Ancestors::Complete(ids) => ids,
62
+ Ancestors::Cyclic(ids) => {
63
+ self.cyclic = true;
64
+ ids
65
+ }
66
+ Ancestors::Partial(ids) => {
67
+ self.partial = true;
68
+ ids
69
+ }
70
+ }
71
+ }
72
+ }
73
+
53
74
  struct LinearizationContext {
54
75
  descendants: IdentityHashSet<DeclarationId>,
55
76
  seen_ids: IdentityHashSet<DeclarationId>,
56
- cyclic: bool,
57
- partial: bool,
58
77
  }
59
78
 
60
79
  impl LinearizationContext {
@@ -62,8 +81,6 @@ impl LinearizationContext {
62
81
  Self {
63
82
  descendants: IdentityHashSet::default(),
64
83
  seen_ids: IdentityHashSet::default(),
65
- cyclic: false,
66
- partial: false,
67
84
  }
68
85
  }
69
86
 
@@ -82,6 +99,8 @@ pub struct Resolver<'a> {
82
99
  unit_queue: VecDeque<Unit>,
83
100
  /// Whether we made any progress in the last pass of the resolution loop
84
101
  made_progress: bool,
102
+ /// The linearization context carried across recursion
103
+ context: LinearizationContext,
85
104
  }
86
105
 
87
106
  impl<'a> Resolver<'a> {
@@ -90,6 +109,7 @@ impl<'a> Resolver<'a> {
90
109
  graph,
91
110
  unit_queue: VecDeque::new(),
92
111
  made_progress: false,
112
+ context: LinearizationContext::new(),
93
113
  }
94
114
  }
95
115
 
@@ -228,12 +248,38 @@ impl<'a> Resolver<'a> {
228
248
  Outcome::Resolved(id) => {
229
249
  if needs_linearization {
230
250
  self.unit_queue.push_back(Unit::Ancestors(id));
251
+ self.relinearize_singleton_hierarchy(id);
231
252
  }
232
253
  self.made_progress = true;
233
254
  }
234
255
  }
235
256
  }
236
257
 
258
+ /// Clears and re-enqueues the ancestors of a declaration's singleton class and all of its higher-order singleton
259
+ /// classes. A later class or module definition can change their parent hierarchy.
260
+ fn relinearize_singleton_hierarchy(&mut self, declaration_id: DeclarationId) {
261
+ let Some(singleton_id) = self
262
+ .graph
263
+ .declarations()
264
+ .get(&declaration_id)
265
+ .and_then(|declaration| declaration.as_namespace())
266
+ .and_then(|namespace| namespace.singleton_class())
267
+ .copied()
268
+ else {
269
+ return;
270
+ };
271
+
272
+ self.graph
273
+ .declarations_mut()
274
+ .get_mut(&singleton_id)
275
+ .unwrap()
276
+ .as_namespace_mut()
277
+ .unwrap()
278
+ .clear_ancestors();
279
+ self.unit_queue.push_back(Unit::Ancestors(singleton_id));
280
+ self.relinearize_singleton_hierarchy(singleton_id);
281
+ }
282
+
237
283
  /// Handles a unit of work for resolving a constant reference
238
284
  fn handle_reference_unit(&mut self, unit_id: Unit, id: ConstantReferenceId) {
239
285
  let constant_ref = self.graph.constant_references().get(&id).unwrap();
@@ -256,7 +302,7 @@ impl<'a> Resolver<'a> {
256
302
 
257
303
  /// Handles a unit of work for linearizing ancestors of a declaration
258
304
  fn handle_ancestor_unit(&mut self, id: DeclarationId) {
259
- match self.ancestors_of(id) {
305
+ match self.linearize_ancestors(id) {
260
306
  Ancestors::Complete(_) | Ancestors::Cyclic(_) => {
261
307
  // We succeeded in some capacity this time
262
308
  self.made_progress = true;
@@ -635,7 +681,7 @@ impl<'a> Resolver<'a> {
635
681
  }
636
682
 
637
683
  /// Resolves retroactive method visibility changes (`private :foo`, `protected :foo`, `public :foo`,
638
- /// `private_class_method :foo`, `public_class_method :foo`).
684
+ /// `private_class_method :foo`, `public_class_method :foo`, `module_function :foo`).
639
685
  ///
640
686
  /// Runs as a second pass after all methods/attrs are declared, so `private :bar` works
641
687
  /// regardless of whether `def bar` appeared before or after it in source.
@@ -652,12 +698,14 @@ impl<'a> Resolver<'a> {
652
698
  let offset = method_visibility.offset().clone();
653
699
  let lexical_nesting_id = *method_visibility.lexical_nesting_id();
654
700
  let is_singleton = method_visibility.flags().is_singleton_method_visibility();
701
+ let is_module_function = *method_visibility.visibility() == Visibility::ModuleFunction;
702
+ let is_module_function_singleton = is_singleton && is_module_function;
655
703
 
656
704
  let Some(lexical_owner_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else {
657
705
  continue;
658
706
  };
659
707
 
660
- let owner_id = if is_singleton {
708
+ let namespace_id = if is_singleton && !is_module_function {
661
709
  let Some(singleton_id) =
662
710
  self.get_or_create_singleton_class(lexical_owner_id, SingletonAncestors::Eager)
663
711
  else {
@@ -668,12 +716,12 @@ impl<'a> Resolver<'a> {
668
716
  lexical_owner_id
669
717
  };
670
718
 
671
- let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&owner_id) else {
719
+ let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&namespace_id) else {
672
720
  continue;
673
721
  };
674
722
 
675
- let mut visibility_applied = false;
676
- let mut has_partial = false;
723
+ let mut visibility_definition_attached = false;
724
+ let mut ancestor_chain_is_incomplete = false;
677
725
 
678
726
  for ancestor in namespace.ancestors() {
679
727
  match ancestor {
@@ -687,40 +735,51 @@ impl<'a> Resolver<'a> {
687
735
  .is_some();
688
736
 
689
737
  if has_member {
690
- // Direct member: `create_declaration`'s fully qualified name dedup attaches
691
- // this visibility definition to the existing method declaration.
692
- // Inherited: a new child-owned declaration is created.
693
- self.create_declaration(str_id, id, owner_id, |name| {
694
- Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
695
- });
696
- visibility_applied = true;
738
+ if is_module_function_singleton {
739
+ let Some(singleton_id) =
740
+ self.get_or_create_singleton_class(lexical_owner_id, SingletonAncestors::Eager)
741
+ else {
742
+ break;
743
+ };
744
+
745
+ self.create_declaration(str_id, id, singleton_id, |name| {
746
+ Declaration::Method(Box::new(MethodDeclaration::new(name, singleton_id)))
747
+ });
748
+ } else {
749
+ self.create_declaration(str_id, id, namespace_id, |name| {
750
+ Declaration::Method(Box::new(MethodDeclaration::new(name, namespace_id)))
751
+ });
752
+ }
753
+ visibility_definition_attached = true;
697
754
  break;
698
755
  }
699
756
  }
700
- Ancestor::Partial(_) => has_partial = true,
757
+ Ancestor::Partial(_) => ancestor_chain_is_incomplete = true,
701
758
  }
702
759
  }
703
760
 
704
- if visibility_applied {
761
+ if visibility_definition_attached {
705
762
  continue;
706
763
  }
707
764
 
708
- if has_partial {
709
- // Method might exist on an unresolved ancestor — requeue for retry.
765
+ if ancestor_chain_is_incomplete {
710
766
  pending_work.push(Unit::Definition(id));
711
- } else {
712
- // Ancestors are fully resolved — method definitively doesn't exist.
713
- let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string();
714
- let owner_name = self.graph.declarations().get(&owner_id).unwrap().name().to_string();
715
- let diagnostic = Diagnostic::new(
716
- Rule::UndefinedMethodVisibilityTarget,
717
- uri_id,
718
- offset,
719
- format!("undefined method `{owner_name}#{method_name}` for visibility change"),
720
- );
721
-
722
- self.graph.add_document_diagnostic(uri_id, diagnostic);
767
+ continue;
768
+ }
769
+
770
+ if is_module_function_singleton {
771
+ continue;
723
772
  }
773
+
774
+ let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string();
775
+ let owner_name = self.graph.declarations().get(&namespace_id).unwrap().name().to_string();
776
+ let diagnostic = Diagnostic::new(
777
+ Rule::UndefinedMethodVisibilityTarget,
778
+ uri_id,
779
+ offset,
780
+ format!("undefined method `{owner_name}#{method_name}` for visibility change"),
781
+ );
782
+ self.graph.add_document_diagnostic(uri_id, diagnostic);
724
783
  }
725
784
 
726
785
  // Must extend work here so incremental resolution can resolve previously unresolved visibility operations
@@ -850,7 +909,7 @@ impl<'a> Resolver<'a> {
850
909
  /// Gets or creates a singleton class declaration for a given class/module declaration. For class `Foo`, this
851
910
  /// returns the declaration for `Foo::<Foo>`.
852
911
  ///
853
- /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to a `Class`
912
+ /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to an unknown
854
913
  /// namespace before creating the singleton. Returns `None` if the declaration is not a namespace and cannot be
855
914
  /// promoted (e.g., `FOO = 42`).
856
915
  /// `ancestors` controls how the singleton's ancestor chain is scheduled for linearization —
@@ -863,6 +922,10 @@ impl<'a> Resolver<'a> {
863
922
  ) -> Option<DeclarationId> {
864
923
  let attached_decl = self.graph.declarations().get(&attached_id).unwrap();
865
924
 
925
+ if let Some(singleton_id) = attached_decl.as_namespace().and_then(|d| d.singleton_class()) {
926
+ return Some(*singleton_id);
927
+ }
928
+
866
929
  // If the attached object is a constant alias, follow the alias chain to find the actual namespace
867
930
  if matches!(attached_decl, Declaration::ConstantAlias(_)) {
868
931
  return match self.resolve_to_namespace(attached_id) {
@@ -874,7 +937,7 @@ impl<'a> Resolver<'a> {
874
937
  if matches!(attached_decl, Declaration::Constant(_)) {
875
938
  if self.graph.all_definitions_promotable(attached_decl) {
876
939
  self.graph.promote_constant_to_namespace(attached_id, |name, owner_id| {
877
- Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
940
+ Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(name, owner_id))))
878
941
  });
879
942
 
880
943
  self.schedule_singleton_ancestors(attached_id, mode);
@@ -890,10 +953,6 @@ impl<'a> Resolver<'a> {
890
953
  .as_namespace_mut()
891
954
  .expect("constants are handled above; all other callers pass namespace declarations");
892
955
 
893
- if let Some(singleton_id) = namespace_decl.singleton_class() {
894
- return Some(*singleton_id);
895
- }
896
-
897
956
  let decl_id = DeclarationId::from(&fully_qualified_name);
898
957
  namespace_decl.set_singleton_class_id(decl_id);
899
958
 
@@ -914,7 +973,7 @@ impl<'a> Resolver<'a> {
914
973
  fn schedule_singleton_ancestors(&mut self, id: DeclarationId, mode: SingletonAncestors) {
915
974
  match mode {
916
975
  SingletonAncestors::Eager => {
917
- let _ = self.ancestors_of(id);
976
+ let _ = self.linearize_ancestors(id);
918
977
  }
919
978
  SingletonAncestors::Enqueue => {
920
979
  self.unit_queue.push_back(Unit::Ancestors(id));
@@ -931,35 +990,24 @@ impl<'a> Resolver<'a> {
931
990
  ///
932
991
  /// Can panic if there's inconsistent data in the graph
933
992
  #[must_use]
934
- fn ancestors_of(&mut self, declaration_id: DeclarationId) -> Ancestors {
935
- let mut context = LinearizationContext::new();
936
- self.linearize_ancestors(declaration_id, &mut context)
937
- }
938
-
939
- /// Linearizes the ancestors of a declaration, returning the list of ancestor declaration IDs
940
- ///
941
- /// # Panics
942
- ///
943
- /// Can panic if there's inconsistent data in the graph
944
- #[must_use]
945
- fn linearize_ancestors(&mut self, declaration_id: DeclarationId, context: &mut LinearizationContext) -> Ancestors {
993
+ fn linearize_ancestors(&mut self, declaration_id: DeclarationId) -> Ancestors {
946
994
  {
947
995
  let declaration = self.graph.declarations_mut().get_mut(&declaration_id).unwrap();
948
996
 
949
997
  // Add this declaration to the descendants so that we capture transitive descendant relationships
950
- context.descendants.insert(declaration_id);
998
+ self.context.descendants.insert(declaration_id);
951
999
 
952
1000
  // Return the cached ancestors if we already computed them. If they are partial ancestors, ignore the cache to try
953
1001
  // again
954
1002
  if declaration.as_namespace().unwrap().has_complete_ancestors() {
955
1003
  let cached = declaration.as_namespace().unwrap().clone_ancestors();
956
- self.propagate_descendants(&mut context.descendants, &cached);
1004
+ self.propagate_descendants(&cached);
957
1005
 
958
- context.finalize(declaration_id);
1006
+ self.context.finalize(declaration_id);
959
1007
  return cached;
960
1008
  }
961
1009
 
962
- if !context.seen_ids.insert(declaration_id) {
1010
+ if !self.context.seen_ids.insert(declaration_id) {
963
1011
  // If we find a cycle when linearizing ancestors, it's an error that the programmer must fix. However, we try to
964
1012
  // still approximate features by assuming that it must inherit from `Object` at some point (which is what most
965
1013
  // classes/modules inherit from). This is not 100% correct, but it allows us to provide a bit better IDE support
@@ -974,24 +1022,20 @@ impl<'a> Resolver<'a> {
974
1022
  .unwrap()
975
1023
  .set_ancestors(estimated_ancestors.clone());
976
1024
 
977
- context.finalize(declaration_id);
1025
+ self.context.finalize(declaration_id);
978
1026
  return estimated_ancestors;
979
1027
  }
980
1028
 
981
1029
  // Automatically track descendants as we recurse. This has to happen before checking the cache since we may have
982
1030
  // already linearized the parent's ancestors, but it's the first time we're discovering the descendant
983
- for descendant in &context.descendants {
984
- self.graph
985
- .declarations_mut()
986
- .get_mut(&declaration_id)
987
- .unwrap()
988
- .as_namespace_mut()
989
- .unwrap()
990
- .add_descendant(*descendant);
1031
+ let namespace = declaration.as_namespace_mut().unwrap();
1032
+ for descendant in &self.context.descendants {
1033
+ namespace.add_descendant(*descendant);
991
1034
  }
992
1035
  }
993
1036
 
994
- let parent_ancestors = self.linearize_parent_ancestors(declaration_id, context);
1037
+ let mut state = ChainState::default();
1038
+ let parent_ancestors = self.linearize_parent_ancestors(declaration_id, &mut state);
995
1039
  let declaration = self.graph.declarations().get(&declaration_id).unwrap();
996
1040
  let mut mixins = Vec::new();
997
1041
 
@@ -1005,9 +1049,9 @@ impl<'a> Resolver<'a> {
1005
1049
  attached_decl
1006
1050
  .definitions()
1007
1051
  .iter()
1008
- .filter_map(|definition_id| self.mixins_of(*definition_id))
1009
- .flatten()
1010
- .filter(|mixin| matches!(mixin, Mixin::Extend(_))),
1052
+ .flat_map(|definition_id| self.mixins_of(*definition_id))
1053
+ .filter(|mixin| matches!(mixin, Mixin::Extend(_)))
1054
+ .cloned(),
1011
1055
  );
1012
1056
  }
1013
1057
 
@@ -1015,12 +1059,10 @@ impl<'a> Resolver<'a> {
1015
1059
  let mut has_extends = false;
1016
1060
 
1017
1061
  for definition_id in declaration.definitions() {
1018
- if let Some(def_mixins) = self.mixins_of(*definition_id) {
1019
- for mixin in def_mixins {
1020
- match mixin {
1021
- Mixin::Prepend(_) | Mixin::Include(_) => mixins.push(mixin),
1022
- Mixin::Extend(_) => has_extends = true,
1023
- }
1062
+ for mixin in self.mixins_of(*definition_id) {
1063
+ match mixin {
1064
+ Mixin::Prepend(_) | Mixin::Include(_) => mixins.push(mixin.clone()),
1065
+ Mixin::Extend(_) => has_extends = true,
1024
1066
  }
1025
1067
  }
1026
1068
  }
@@ -1031,7 +1073,7 @@ impl<'a> Resolver<'a> {
1031
1073
  }
1032
1074
 
1033
1075
  let (linearized_prepends, linearized_includes) =
1034
- self.linearize_mixins(context, mixins, parent_ancestors.as_ref());
1076
+ self.linearize_mixins(mixins, parent_ancestors.as_ref(), &mut state);
1035
1077
 
1036
1078
  // Build the final list
1037
1079
  let mut ancestors = Vec::new();
@@ -1042,9 +1084,9 @@ impl<'a> Resolver<'a> {
1042
1084
  ancestors.extend(parents);
1043
1085
  }
1044
1086
 
1045
- let result = if context.cyclic {
1087
+ let result = if state.cyclic {
1046
1088
  Ancestors::Cyclic(ancestors)
1047
- } else if context.partial {
1089
+ } else if state.partial {
1048
1090
  Ancestors::Partial(ancestors)
1049
1091
  } else {
1050
1092
  Ancestors::Complete(ancestors)
@@ -1058,56 +1100,32 @@ impl<'a> Resolver<'a> {
1058
1100
  .unwrap()
1059
1101
  .set_ancestors(result.clone());
1060
1102
 
1061
- context.finalize(declaration_id);
1103
+ self.context.finalize(declaration_id);
1062
1104
  result
1063
1105
  }
1064
1106
 
1065
1107
  fn linearize_parent_ancestors(
1066
1108
  &mut self,
1067
1109
  declaration_id: DeclarationId,
1068
- context: &mut LinearizationContext,
1110
+ state: &mut ChainState,
1069
1111
  ) -> Option<Vec<Ancestor>> {
1070
- if declaration_id == *BASIC_OBJECT_ID {
1071
- return None;
1072
- }
1073
-
1074
1112
  let declaration = self.graph.declarations().get(&declaration_id).unwrap();
1075
1113
 
1076
1114
  match declaration {
1077
1115
  Declaration::Namespace(Namespace::Class(_)) => {
1078
- let definition_ids = declaration.definitions().to_vec();
1079
-
1080
- Some(match self.linearize_parent_class(&definition_ids, context) {
1081
- Ancestors::Complete(ids) => ids,
1082
- Ancestors::Cyclic(ids) => {
1083
- context.cyclic = true;
1084
- ids
1085
- }
1086
- Ancestors::Partial(ids) => {
1087
- context.partial = true;
1088
- ids
1089
- }
1090
- })
1116
+ let superclass = self.linearize_superclass(declaration_id, state)?;
1117
+ Some(state.fold(superclass))
1091
1118
  }
1092
1119
  Declaration::Namespace(Namespace::SingletonClass(_)) => {
1093
1120
  let owner_id = *declaration.owner_id();
1094
1121
 
1095
1122
  let (singleton_parent_id, partial_singleton) = self.singleton_parent_id(owner_id);
1096
1123
  if partial_singleton {
1097
- context.partial = true;
1124
+ state.partial = true;
1098
1125
  }
1099
1126
 
1100
- Some(match self.linearize_ancestors(singleton_parent_id, context) {
1101
- Ancestors::Complete(ids) => ids,
1102
- Ancestors::Cyclic(ids) => {
1103
- context.cyclic = true;
1104
- ids
1105
- }
1106
- Ancestors::Partial(ids) => {
1107
- context.partial = true;
1108
- ids
1109
- }
1110
- })
1127
+ let parent = self.linearize_ancestors(singleton_parent_id);
1128
+ Some(state.fold(parent))
1111
1129
  }
1112
1130
  _ => None,
1113
1131
  }
@@ -1117,9 +1135,9 @@ impl<'a> Resolver<'a> {
1117
1135
  /// modules are deduplicated against them
1118
1136
  fn linearize_mixins(
1119
1137
  &mut self,
1120
- context: &mut LinearizationContext,
1121
1138
  mixins: Vec<Mixin>,
1122
1139
  parent_ancestors: Option<&Vec<Ancestor>>,
1140
+ state: &mut ChainState,
1123
1141
  ) -> (VecDeque<Ancestor>, VecDeque<Ancestor>) {
1124
1142
  let mut linearized_prepends = VecDeque::new();
1125
1143
  let mut linearized_includes = VecDeque::new();
@@ -1142,17 +1160,8 @@ impl<'a> Resolver<'a> {
1142
1160
  continue;
1143
1161
  };
1144
1162
 
1145
- let ids = match self.linearize_ancestors(module_id, context) {
1146
- Ancestors::Complete(ids) => ids,
1147
- Ancestors::Cyclic(ids) => {
1148
- context.cyclic = true;
1149
- ids
1150
- }
1151
- Ancestors::Partial(ids) => {
1152
- context.partial = true;
1153
- ids
1154
- }
1155
- };
1163
+ let module_ancestors = self.linearize_ancestors(module_id);
1164
+ let ids = state.fold(module_ancestors);
1156
1165
 
1157
1166
  // Only reorder if there are new modules to add. If all modules being
1158
1167
  // prepended are already in the chain (e.g., `prepend A` when A is already
@@ -1169,7 +1178,7 @@ impl<'a> Resolver<'a> {
1169
1178
  NameRef::Unresolved(_) => {
1170
1179
  // We haven't been able to resolve this name yet, so we push it as a partial linearization to finish
1171
1180
  // later
1172
- context.partial = true;
1181
+ state.partial = true;
1173
1182
  linearized_prepends.push_front(Ancestor::Partial(*constant_reference.name_id()));
1174
1183
  }
1175
1184
  }
@@ -1181,17 +1190,8 @@ impl<'a> Resolver<'a> {
1181
1190
  continue;
1182
1191
  };
1183
1192
 
1184
- let mut ids = match self.linearize_ancestors(module_id, context) {
1185
- Ancestors::Complete(ids) => ids,
1186
- Ancestors::Cyclic(ids) => {
1187
- context.cyclic = true;
1188
- ids
1189
- }
1190
- Ancestors::Partial(ids) => {
1191
- context.partial = true;
1192
- ids
1193
- }
1194
- };
1193
+ let module_ancestors = self.linearize_ancestors(module_id);
1194
+ let mut ids = state.fold(module_ancestors);
1195
1195
 
1196
1196
  // Prepended module are deduped based only on other prepended modules
1197
1197
  ids.retain(|id| {
@@ -1209,7 +1209,7 @@ impl<'a> Resolver<'a> {
1209
1209
  NameRef::Unresolved(_) => {
1210
1210
  // We haven't been able to resolve this name yet, so we push it as a partial linearization to finish
1211
1211
  // later
1212
- context.partial = true;
1212
+ state.partial = true;
1213
1213
  linearized_includes.push_front(Ancestor::Partial(*constant_reference.name_id()));
1214
1214
  }
1215
1215
  }
@@ -1221,22 +1221,22 @@ impl<'a> Resolver<'a> {
1221
1221
  }
1222
1222
 
1223
1223
  /// Propagate descendants to all cached ancestors
1224
- fn propagate_descendants<S: BuildHasher>(
1225
- &mut self,
1226
- descendants: &mut HashSet<DeclarationId, S>,
1227
- cached: &Ancestors,
1228
- ) {
1224
+ fn propagate_descendants(&mut self, cached: &Ancestors) {
1225
+ let descendants = &self.context.descendants;
1226
+
1229
1227
  if !descendants.is_empty() {
1230
1228
  for ancestor in cached {
1231
1229
  if let Ancestor::Complete(ancestor_id) = ancestor {
1232
- for descendant in descendants.iter() {
1233
- self.graph
1234
- .declarations_mut()
1235
- .get_mut(ancestor_id)
1236
- .unwrap()
1237
- .as_namespace_mut()
1238
- .unwrap()
1239
- .add_descendant(*descendant);
1230
+ let namespace = self
1231
+ .graph
1232
+ .declarations_mut()
1233
+ .get_mut(ancestor_id)
1234
+ .unwrap()
1235
+ .as_namespace_mut()
1236
+ .unwrap();
1237
+
1238
+ for descendant in descendants {
1239
+ namespace.add_descendant(*descendant);
1240
1240
  }
1241
1241
  }
1242
1242
  }
@@ -1280,8 +1280,8 @@ impl<'a> Resolver<'a> {
1280
1280
  Outcome::Resolved(owner_id) => {
1281
1281
  let mut fully_qualified_name = self.graph.strings().get(&str_id).unwrap().to_string();
1282
1282
 
1283
- // If the owner is a promotable constant and something is being defined inside it, promote it to a
1284
- // module
1283
+ // If the owner is a promotable constant and something is being defined inside it, promote it to an
1284
+ // unknown namespace. A later explicit class or module definition selects the concrete kind.
1285
1285
  {
1286
1286
  let owner = self.graph.declarations().get(&owner_id).unwrap();
1287
1287
  let is_promotable_constant =
@@ -1289,7 +1289,7 @@ impl<'a> Resolver<'a> {
1289
1289
 
1290
1290
  if is_promotable_constant {
1291
1291
  self.graph.promote_constant_to_namespace(owner_id, |name, owner_id| {
1292
- Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
1292
+ Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(name, owner_id))))
1293
1293
  });
1294
1294
  self.unit_queue.push_back(Unit::Ancestors(owner_id));
1295
1295
  }
@@ -1433,6 +1433,9 @@ impl<'a> Resolver<'a> {
1433
1433
  parent_owner_id,
1434
1434
  )))));
1435
1435
  self.graph.add_member(&parent_owner_id, declaration_id, parent_str_id);
1436
+ // A namespace is the first entry of its own ancestor chain, and member lookups only walk that chain. Without
1437
+ // enqueueing this, the Todo keeps an empty `Partial` chain and none of its members can ever be found
1438
+ self.unit_queue.push_back(Unit::Ancestors(declaration_id));
1436
1439
  }
1437
1440
 
1438
1441
  declaration_id
@@ -1468,139 +1471,139 @@ impl<'a> Resolver<'a> {
1468
1471
  /// resolved
1469
1472
  #[allow(clippy::too_many_lines)]
1470
1473
  fn resolve_constant_internal(&mut self, name_id: NameId) -> Outcome {
1471
- let name_ref = self.graph.names().get(&name_id).unwrap().clone();
1474
+ let name = match self.graph.names().get(&name_id).unwrap() {
1475
+ NameRef::Resolved(resolved) => return Outcome::Resolved(*resolved.declaration_id()),
1476
+ NameRef::Unresolved(name) => name.as_ref().clone(),
1477
+ };
1472
1478
 
1473
- match name_ref {
1474
- NameRef::Unresolved(name) => {
1475
- match name.parent_scope() {
1476
- ParentScope::TopLevel => {
1477
- let result = self.search_ancestors(*OBJECT_ID, *name.str());
1479
+ match name.parent_scope() {
1480
+ ParentScope::TopLevel => {
1481
+ let result = self.search_ancestors(*OBJECT_ID, *name.str());
1478
1482
 
1479
- if let Outcome::Resolved(declaration_id) = result {
1480
- self.graph.record_resolved_name(name_id, declaration_id);
1481
- }
1483
+ if let Outcome::Resolved(declaration_id) = result {
1484
+ self.graph.record_resolved_name(name_id, declaration_id);
1485
+ }
1482
1486
 
1483
- result
1484
- }
1485
- ParentScope::Attached(parent_scope_id) => {
1486
- let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1487
- return Outcome::Retry {
1488
- partial_ancestors: false,
1489
- };
1487
+ result
1488
+ }
1489
+ ParentScope::Attached(parent_scope_id) => {
1490
+ let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1491
+ return Outcome::Retry {
1492
+ partial_ancestors: false,
1493
+ };
1494
+ };
1495
+
1496
+ let mut target_decl_id = *parent_scope.declaration_id();
1497
+ let target_decl = self.graph.declarations().get(&target_decl_id).unwrap();
1498
+
1499
+ // If the attached object is a constant alias, resolve it to the target namespace
1500
+ // (e.g., ALIAS.bar where ALIAS = Foo should create the singleton class on Foo, not ALIAS)
1501
+ if matches!(target_decl, Declaration::ConstantAlias(_)) {
1502
+ let resolved_ids = self.resolve_alias_chains(target_decl_id);
1503
+
1504
+ if resolved_ids
1505
+ .iter()
1506
+ .any(|id| matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_))))
1507
+ {
1508
+ return Outcome::Retry {
1509
+ partial_ancestors: false,
1490
1510
  };
1511
+ }
1491
1512
 
1492
- let mut target_decl_id = *parent_scope.declaration_id();
1493
- let target_decl = self.graph.declarations().get(&target_decl_id).unwrap();
1513
+ let Some(&namespace_id) = resolved_ids
1514
+ .iter()
1515
+ .find(|id| matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_))))
1516
+ else {
1517
+ return Outcome::Unresolved;
1518
+ };
1494
1519
 
1495
- // If the attached object is a constant alias, resolve it to the target namespace
1496
- // (e.g., ALIAS.bar where ALIAS = Foo should create the singleton class on Foo, not ALIAS)
1497
- if matches!(target_decl, Declaration::ConstantAlias(_)) {
1498
- let resolved_ids = self.resolve_alias_chains(target_decl_id);
1520
+ target_decl_id = namespace_id;
1521
+ }
1499
1522
 
1500
- if resolved_ids.iter().any(|id| {
1501
- matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_)))
1502
- }) {
1503
- return Outcome::Retry {
1504
- partial_ancestors: false,
1505
- };
1506
- }
1523
+ // If we found a singleton reference with a resolved attached object parent scope, we
1524
+ // automatically create the singleton class
1525
+ let Some(singleton_id) =
1526
+ self.get_or_create_singleton_class(target_decl_id, SingletonAncestors::Enqueue)
1527
+ else {
1528
+ return Outcome::Unresolved;
1529
+ };
1530
+ self.graph.record_resolved_name(name_id, singleton_id);
1531
+ // `get_or_create_singleton_class` already enqueued the singleton's ancestors on
1532
+ // creation, so there is nothing to hand back for re-enqueueing.
1533
+ Outcome::Resolved(singleton_id)
1534
+ }
1535
+ ParentScope::None => {
1536
+ // Otherwise, it's a simple constant read and we can resolve it directly
1537
+ let result = self.run_resolution(&name);
1507
1538
 
1508
- let Some(&namespace_id) = resolved_ids.iter().find(|id| {
1509
- matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_)))
1510
- }) else {
1511
- return Outcome::Unresolved;
1512
- };
1539
+ if let Outcome::Resolved(declaration_id) = result {
1540
+ self.graph.record_resolved_name(name_id, declaration_id);
1541
+ }
1513
1542
 
1514
- target_decl_id = namespace_id;
1515
- }
1543
+ result
1544
+ }
1545
+ ParentScope::Some(parent_scope_id) => {
1546
+ let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1547
+ return Outcome::Retry {
1548
+ partial_ancestors: false,
1549
+ };
1550
+ };
1516
1551
 
1517
- // If we found a singleton reference with a resolved attached object parent scope, we
1518
- // automatically create the singleton class
1519
- let Some(singleton_id) =
1520
- self.get_or_create_singleton_class(target_decl_id, SingletonAncestors::Enqueue)
1521
- else {
1522
- return Outcome::Unresolved;
1523
- };
1524
- self.graph.record_resolved_name(name_id, singleton_id);
1525
- // `get_or_create_singleton_class` already enqueued the singleton's ancestors on
1526
- // creation, so there is nothing to hand back for re-enqueueing.
1527
- Outcome::Resolved(singleton_id)
1528
- }
1529
- ParentScope::None => {
1530
- // Otherwise, it's a simple constant read and we can resolve it directly
1531
- let result = self.run_resolution(&name);
1552
+ // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
1553
+ // An alias can have multiple targets, so we try all of them in order.
1554
+ let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1532
1555
 
1533
- if let Outcome::Resolved(declaration_id) = result {
1534
- self.graph.record_resolved_name(name_id, declaration_id);
1535
- }
1556
+ // Search each resolved target for the constant. Return early if found.
1557
+ let mut missing_partial = false;
1558
+ let mut found_namespace = false;
1536
1559
 
1537
- result
1538
- }
1539
- ParentScope::Some(parent_scope_id) => {
1540
- let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1560
+ for &id in &resolved_ids {
1561
+ match self.graph.declarations().get(&id) {
1562
+ Some(Declaration::ConstantAlias(_)) => {
1563
+ // Alias not fully resolved yet
1541
1564
  return Outcome::Retry {
1542
1565
  partial_ancestors: false,
1543
1566
  };
1544
- };
1567
+ }
1568
+ Some(Declaration::Namespace(_)) => {
1569
+ found_namespace = true;
1545
1570
 
1546
- // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
1547
- // An alias can have multiple targets, so we try all of them in order.
1548
- let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1549
-
1550
- // Search each resolved target for the constant. Return early if found.
1551
- let mut missing_partial = false;
1552
- let mut found_namespace = false;
1553
-
1554
- for &id in &resolved_ids {
1555
- match self.graph.declarations().get(&id) {
1556
- Some(Declaration::ConstantAlias(_)) => {
1557
- // Alias not fully resolved yet
1558
- return Outcome::Retry {
1559
- partial_ancestors: false,
1560
- };
1571
+ match self.search_ancestors(id, *name.str()) {
1572
+ Outcome::Resolved(declaration_id) => {
1573
+ self.graph.record_resolved_name(name_id, declaration_id);
1574
+ return Outcome::Resolved(declaration_id);
1561
1575
  }
1562
- Some(Declaration::Namespace(_)) => {
1563
- found_namespace = true;
1564
-
1565
- match self.search_ancestors(id, *name.str()) {
1566
- Outcome::Resolved(declaration_id) => {
1567
- self.graph.record_resolved_name(name_id, declaration_id);
1568
- return Outcome::Resolved(declaration_id);
1569
- }
1570
- Outcome::Retry {
1571
- partial_ancestors: true,
1572
- } => {
1573
- missing_partial = true;
1574
- }
1575
- Outcome::Unresolved => {}
1576
- Outcome::Retry {
1577
- partial_ancestors: false,
1578
- } => {
1579
- unreachable!("search_ancestors never returns a non-partial Retry")
1580
- }
1581
- }
1576
+ Outcome::Retry {
1577
+ partial_ancestors: true,
1578
+ } => {
1579
+ missing_partial = true;
1582
1580
  }
1583
- _ => {
1584
- // Not a namespace (e.g., a constant) - skip
1581
+ Outcome::Unresolved => {}
1582
+ Outcome::Retry {
1583
+ partial_ancestors: false,
1584
+ } => {
1585
+ unreachable!("search_ancestors never returns a non-partial Retry")
1585
1586
  }
1586
1587
  }
1587
1588
  }
1588
-
1589
- // If no namespaces were found, this constant path can never resolve.
1590
- if !found_namespace {
1591
- return Outcome::Unresolved;
1592
- }
1593
-
1594
- // Member not found in any namespace yet - retry in case it's added later. `partial` records
1595
- // whether the miss was due to a still-partial ancestor chain, so the unit is re-checked once
1596
- // that chain completes.
1597
- Outcome::Retry {
1598
- partial_ancestors: missing_partial,
1589
+ _ => {
1590
+ // Not a namespace (e.g., a constant) - skip
1599
1591
  }
1600
1592
  }
1601
1593
  }
1594
+
1595
+ // If no namespaces were found, this constant path can never resolve.
1596
+ if !found_namespace {
1597
+ return Outcome::Unresolved;
1598
+ }
1599
+
1600
+ // Member not found in any namespace yet - retry in case it's added later. `partial` records
1601
+ // whether the miss was due to a still-partial ancestor chain, so the unit is re-checked once
1602
+ // that chain completes.
1603
+ Outcome::Retry {
1604
+ partial_ancestors: missing_partial,
1605
+ }
1602
1606
  }
1603
- NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id()),
1604
1607
  }
1605
1608
  }
1606
1609
 
@@ -1725,8 +1728,13 @@ impl<'a> Resolver<'a> {
1725
1728
  if is_module || chain_incomplete {
1726
1729
  let object_outcome = self.search_ancestors(*OBJECT_ID, str_id);
1727
1730
 
1728
- if let Outcome::Resolved(decl_id) = object_outcome {
1729
- return Outcome::Resolved(decl_id);
1731
+ match object_outcome {
1732
+ // Found the constant through Object's ancestors.
1733
+ Outcome::Resolved(decl_id) => return Outcome::Resolved(decl_id),
1734
+ // Object's ancestor chain is still partial, so we need to retry later.
1735
+ Outcome::Retry { .. } => return object_outcome,
1736
+ // Object's chain is complete, so we indeed can't resolve this constant.
1737
+ Outcome::Unresolved => {}
1730
1738
  }
1731
1739
  }
1732
1740
 
@@ -1741,7 +1749,7 @@ impl<'a> Resolver<'a> {
1741
1749
 
1742
1750
  /// Search for a member in a declaration's ancestor chain.
1743
1751
  fn search_ancestors(&mut self, declaration_id: DeclarationId, str_id: StringId) -> Outcome {
1744
- match self.ancestors_of(declaration_id) {
1752
+ match self.linearize_ancestors(declaration_id) {
1745
1753
  Ancestors::Complete(ids) | Ancestors::Cyclic(ids) => ids
1746
1754
  .iter()
1747
1755
  .find_map(|ancestor_id| {
@@ -1819,9 +1827,15 @@ impl<'a> Resolver<'a> {
1819
1827
  Outcome::Unresolved
1820
1828
  }
1821
1829
 
1822
- /// Returns a complexity score for a given name, which is used to sort names for resolution. The complexity is based
1823
- /// on how many parent scopes are involved in a name's nesting. This is because simple names are always
1824
- /// straightforward to resolve no matter how deep the nesting is. For example:
1830
+ /// Drains `pending_work` and classifies items into the resolution queue.
1831
+ ///
1832
+ /// Namespace definitions and constant references are sorted by their name's [depth](Name::depth) so that simpler
1833
+ /// names are processed first, with the URI rank and offset breaking ties to keep the order deterministic.
1834
+ /// Non-namespace definitions (methods, attrs, variables) are returned separately for
1835
+ /// `handle_remaining_definitions`.
1836
+ ///
1837
+ /// Ordering by depth matters because a name's parts may need to be resolved before the name itself can be. Simple
1838
+ /// names are always straightforward no matter how deeply they nest:
1825
1839
  ///
1826
1840
  /// ```ruby
1827
1841
  /// module Foo
@@ -1831,9 +1845,8 @@ impl<'a> Resolver<'a> {
1831
1845
  /// end
1832
1846
  /// ```
1833
1847
  ///
1834
- /// These are all simple names because they don't require resolution logic to determine the final name of each step.
1835
- /// We only have to ensure that they are ordered by nesting level. Names with parent scopes require that their parts
1836
- /// be resolved to determine what they refer to and so they must be sorted last.
1848
+ /// Here `Foo`, `Bar` and `Baz` only have to be ordered by nesting level. Names with parent scopes, however, require
1849
+ /// their parts to be resolved first, so they must sort last:
1837
1850
  ///
1838
1851
  /// ```ruby
1839
1852
  /// module Foo
@@ -1843,64 +1856,9 @@ impl<'a> Resolver<'a> {
1843
1856
  /// end
1844
1857
  /// ```
1845
1858
  ///
1846
- /// In this case, we need `Bar` to have already been processed so that we can resolve the `Bar` reference inside of
1847
- /// the `Foo` nesting, which then unblocks the resolution of `Baz` and finally `Qux`. Notice how `Qux` is a simple
1848
- /// name, but it's nested under a complex name so we have to sort it last. This is why we consider the number of
1849
- /// parent scopes in the entire nesting, not just for the name itself
1850
- ///
1851
- /// Compute the depth of a name in the graph by recursively summing the depths of its
1852
- /// `parent_scope` and `nesting` chains. Results are memoized in `cache` (`NameId` → depth)
1853
- /// so each name is computed at most once across all calls.
1854
- ///
1855
- /// Depth represents the total complexity of a name's position in the namespace hierarchy.
1856
- /// For example, in `module Foo; module Bar; class Baz; end; end; end`, Foo has depth 1
1857
- /// (top-level), Bar has depth 2, and Baz has depth 3.
1858
- ///
1859
- /// # Panics
1860
- ///
1861
- /// Will panic if there is inconsistent data in the graph
1862
- fn name_depth(
1863
- name_id: NameId,
1864
- names: &IdentityHashMap<NameId, NameRef>,
1865
- cache: &mut IdentityHashMap<NameId, u32>,
1866
- ) -> u32 {
1867
- if let Some(&depth) = cache.get(&name_id) {
1868
- return depth;
1869
- }
1870
-
1871
- let name = names.get(&name_id).unwrap();
1872
-
1873
- let depth = if name.parent_scope().is_top_level() {
1874
- 1
1875
- } else {
1876
- let parent_depth = name.parent_scope().map_or(0, |id| Self::name_depth(*id, names, cache));
1877
-
1878
- let nesting_depth = name.nesting().map_or(0, |id| Self::name_depth(id, names, cache));
1879
-
1880
- parent_depth + nesting_depth + 1
1881
- };
1882
-
1883
- cache.insert(name_id, depth);
1884
- depth
1885
- }
1886
-
1887
- /// Pre-compute name depths for all names into a `NameId → depth` map. Each name's depth is
1888
- /// computed once via memoized recursion, then used as an O(1) lookup key during sorting in
1889
- /// `prepare_units`.
1890
- pub(crate) fn compute_name_depths(names: &IdentityHashMap<NameId, NameRef>) -> IdentityHashMap<NameId, u32> {
1891
- let mut cache = IdentityHashMap::with_capacity_and_hasher(names.len(), IdentityHashBuilder);
1892
-
1893
- for &name_id in names.keys() {
1894
- Self::name_depth(name_id, names, &mut cache);
1895
- }
1896
-
1897
- cache
1898
- }
1899
-
1900
- /// Drains `pending_work` and classifies items into the resolution queue.
1901
- /// Namespace definitions and constant references are sorted by name depth for deterministic
1902
- /// resolution order. Non-namespace definitions (methods, attrs, variables) are returned
1903
- /// separately for `handle_remaining_definitions`.
1859
+ /// In this case `Bar` must already be processed so the `Bar` reference inside `Foo` resolves, which then unblocks
1860
+ /// `Baz` and finally `Qux`. Notice how `Qux` is a simple name nested under a complex one, so it too sorts late —
1861
+ /// which is why depth accounts for the parent scopes across the entire nesting, not just for the name itself.
1904
1862
  fn prepare_units(&mut self) -> Vec<DefinitionId> {
1905
1863
  let work = self.graph.take_pending_work();
1906
1864
  let estimated = work.len() / 2;
@@ -1910,7 +1868,25 @@ impl<'a> Resolver<'a> {
1910
1868
  let mut const_refs = Vec::new();
1911
1869
  let mut ancestors = vec![*BASIC_OBJECT_ID, *KERNEL_ID, *OBJECT_ID, *MODULE_ID, *CLASS_ID];
1912
1870
  let names = self.graph.names();
1913
- let depths = Self::compute_name_depths(names);
1871
+ // Every name captured its depth during indexing (see `Name::compute_depth`), so ordering the units below is a
1872
+ // direct lookup rather than a recursive walk of the nesting chain.
1873
+ let depth_of = |name_id: &NameId| names.get(name_id).unwrap().depth();
1874
+
1875
+ // Precompute the lexicographic rank of every document URI. Definitions and references are sorted by
1876
+ // (name depth, URI, offset) below; storing a precomputed integer rank instead of comparing URI strings on
1877
+ // every comparison makes sorting substantially cheaper on large graphs.
1878
+ let mut uris: Vec<(&str, UriId)> = self
1879
+ .graph
1880
+ .documents()
1881
+ .iter()
1882
+ .map(|(uri_id, document)| (document.uri(), *uri_id))
1883
+ .collect();
1884
+ uris.sort_unstable();
1885
+ let mut uri_ranks: IdentityHashMap<UriId, u32> =
1886
+ IdentityHashMap::with_capacity_and_hasher(uris.len(), IdentityHashBuilder);
1887
+ for (rank, (_, uri_id)) in uris.into_iter().enumerate() {
1888
+ uri_ranks.insert(uri_id, u32::try_from(rank).expect("more documents than u32::MAX"));
1889
+ }
1914
1890
 
1915
1891
  // Dedup: when multiple files are indexed before resolution runs, pending_work accumulates
1916
1892
  // and the same definition/reference ID can be enqueued more than once.
@@ -1928,23 +1904,28 @@ impl<'a> Resolver<'a> {
1928
1904
  let Some(definition) = self.graph.definitions().get(&id) else {
1929
1905
  continue;
1930
1906
  };
1931
- let uri = self.graph.documents().get(definition.uri_id()).unwrap().uri();
1907
+ let uri_rank = *uri_ranks.get(definition.uri_id()).unwrap();
1932
1908
 
1933
1909
  match definition {
1934
1910
  Definition::Class(def) => {
1935
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1911
+ let depth = depth_of(def.name_id());
1912
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1936
1913
  }
1937
1914
  Definition::Module(def) => {
1938
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1915
+ let depth = depth_of(def.name_id());
1916
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1939
1917
  }
1940
1918
  Definition::Constant(def) => {
1941
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1919
+ let depth = depth_of(def.name_id());
1920
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1942
1921
  }
1943
1922
  Definition::ConstantAlias(def) => {
1944
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1923
+ let depth = depth_of(def.name_id());
1924
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1945
1925
  }
1946
1926
  Definition::SingletonClass(def) => {
1947
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1927
+ let depth = depth_of(def.name_id());
1928
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1948
1929
  }
1949
1930
  // SelfReceiver methods create singleton classes, which need
1950
1931
  // ancestor linearization. Process them in the convergence loop
@@ -1965,11 +1946,9 @@ impl<'a> Resolver<'a> {
1965
1946
  let Some(constant_ref) = self.graph.constant_references().get(&id) else {
1966
1947
  continue;
1967
1948
  };
1968
- let uri = self.graph.documents().get(&constant_ref.uri_id()).unwrap().uri();
1969
- const_refs.push((
1970
- Unit::ConstantRef(id),
1971
- (*constant_ref.name_id(), uri, constant_ref.offset()),
1972
- ));
1949
+ let uri_rank = *uri_ranks.get(&constant_ref.uri_id()).unwrap();
1950
+ let depth = depth_of(constant_ref.name_id());
1951
+ const_refs.push((Unit::ConstantRef(id), (depth, uri_rank, constant_ref.offset())));
1973
1952
  }
1974
1953
  Unit::Ancestors(id) => {
1975
1954
  if !seen_ancestors.insert(id) {
@@ -1984,14 +1963,10 @@ impl<'a> Resolver<'a> {
1984
1963
  }
1985
1964
 
1986
1965
  // Sort namespaces based on their name complexity so that simpler names are always first
1987
- // When the depth is the same, sort by URI and offset to maintain determinism
1988
- definitions.sort_unstable_by(|(_, (name_a, uri_a, offset_a)), (_, (name_b, uri_b, offset_b))| {
1989
- (depths.get(name_a).unwrap(), uri_a, offset_a).cmp(&(depths.get(name_b).unwrap(), uri_b, offset_b))
1990
- });
1966
+ // When the depth is the same, sort by URI rank and offset to maintain determinism
1967
+ definitions.sort_unstable_by_key(|(_, key)| *key);
1991
1968
 
1992
- const_refs.sort_unstable_by(|(_, (name_a, uri_a, offset_a)), (_, (name_b, uri_b, offset_b))| {
1993
- (depths.get(name_a).unwrap(), uri_a, offset_a).cmp(&(depths.get(name_b).unwrap(), uri_b, offset_b))
1994
- });
1969
+ const_refs.sort_unstable_by_key(|(_, key)| *key);
1995
1970
 
1996
1971
  others.sort_unstable_by_key(|(_, key)| *key);
1997
1972
 
@@ -2011,15 +1986,11 @@ impl<'a> Resolver<'a> {
2011
1986
  /// - Class: parent is the singleton class of the original parent class
2012
1987
  /// - Singleton class: recurse as many times as necessary to wrap the original attached object's parent class
2013
1988
  fn singleton_parent_id(&mut self, attached_id: DeclarationId) -> (DeclarationId, bool) {
2014
- // Base case: if we reached `BasicObject`, then the parent is `Class`
2015
- if attached_id == *BASIC_OBJECT_ID {
2016
- return (*CLASS_ID, false);
2017
- }
2018
-
2019
1989
  let decl = self.graph.declarations().get(&attached_id).unwrap();
2020
1990
 
2021
1991
  match decl {
2022
- Declaration::Namespace(Namespace::Module(_)) => (*MODULE_ID, false),
1992
+ // Todo may later become a class or module. Until then, use Module as the singleton-parent fallback.
1993
+ Declaration::Namespace(Namespace::Module(_) | Namespace::Todo(_)) => (*MODULE_ID, false),
2023
1994
  Declaration::Namespace(Namespace::SingletonClass(_)) => {
2024
1995
  // For singleton classes, we keep recursively wrapping parents until we can reach the original attached
2025
1996
  // object
@@ -2033,14 +2004,15 @@ impl<'a> Resolver<'a> {
2033
2004
  )
2034
2005
  }
2035
2006
  Declaration::Namespace(Namespace::Class(_)) => {
2036
- // For classes (the regular case), we need to return the singleton class of its parent
2037
- let definition_ids = decl.definitions().to_vec();
2038
-
2039
- let (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids);
2007
+ // For classes (the regular case), we need to return the singleton class of its superclass
2008
+ let Some((superclass_id, unresolved_superclass)) = self.get_superclass(attached_id) else {
2009
+ // BasicObject has no superclass, but its singleton class inherits from Class
2010
+ return (*CLASS_ID, false);
2011
+ };
2040
2012
  (
2041
- self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred)
2042
- .expect("parent class should always be a namespace"),
2043
- unresolved_parent.is_some(),
2013
+ self.get_or_create_singleton_class(superclass_id, SingletonAncestors::Deferred)
2014
+ .expect("superclass should always be a namespace"),
2015
+ unresolved_superclass.is_some(),
2044
2016
  )
2045
2017
  }
2046
2018
  _ => {
@@ -2051,11 +2023,18 @@ impl<'a> Resolver<'a> {
2051
2023
  }
2052
2024
  }
2053
2025
 
2054
- fn get_parent_class(&self, definition_ids: &[DefinitionId]) -> (DeclarationId, Option<NameId>) {
2055
- let mut explicit_parents = Vec::new();
2056
- let mut unresolved_parent = None;
2026
+ /// Returns the selected superclass and any unresolved explicit superclass. The top-level `BasicObject` declaration
2027
+ /// is Ruby's root class and is the only class without a superclass.
2028
+ fn get_superclass(&self, declaration_id: DeclarationId) -> Option<(DeclarationId, Option<NameId>)> {
2029
+ if declaration_id == *BASIC_OBJECT_ID {
2030
+ return None;
2031
+ }
2057
2032
 
2058
- for definition_id in definition_ids {
2033
+ let declaration = self.graph.declarations().get(&declaration_id).unwrap();
2034
+ let mut explicit_superclasses = Vec::new();
2035
+ let mut unresolved_superclass = None;
2036
+
2037
+ for definition_id in declaration.definitions() {
2059
2038
  let definition = self.graph.definitions().get(definition_id).unwrap();
2060
2039
 
2061
2040
  if let Definition::Class(class) = definition
@@ -2066,57 +2045,51 @@ impl<'a> Resolver<'a> {
2066
2045
 
2067
2046
  match name {
2068
2047
  NameRef::Resolved(resolved) => {
2069
- if let Some(parent_id) = self.resolve_to_namespace(*resolved.declaration_id()) {
2070
- explicit_parents.push(parent_id);
2048
+ if let Some(superclass_id) = self.resolve_to_namespace(*resolved.declaration_id()) {
2049
+ explicit_superclasses.push(superclass_id);
2071
2050
  }
2072
2051
  }
2073
2052
  NameRef::Unresolved(_) => {
2074
- unresolved_parent = Some(*constant_reference.name_id());
2053
+ unresolved_superclass = Some(*constant_reference.name_id());
2075
2054
  }
2076
2055
  }
2077
2056
  }
2078
2057
  }
2079
2058
 
2080
- // If there's more than one parent class that isn't `Object` and they are different, then there's a superclass
2059
+ // If there's more than one superclass that isn't `Object` and they are different, then there's a superclass
2081
2060
  // mismatch error. TODO: We should add a diagnostic here
2082
- (
2083
- explicit_parents.first().copied().unwrap_or(*OBJECT_ID),
2084
- unresolved_parent,
2085
- )
2061
+ Some((
2062
+ explicit_superclasses.first().copied().unwrap_or(*OBJECT_ID),
2063
+ unresolved_superclass,
2064
+ ))
2086
2065
  }
2087
2066
 
2088
- fn linearize_parent_class(
2089
- &mut self,
2090
- definition_ids: &[DefinitionId],
2091
- context: &mut LinearizationContext,
2092
- ) -> Ancestors {
2093
- let (picked_parent, unresolved_parent) = self.get_parent_class(definition_ids);
2094
- let mut result = self.linearize_ancestors(picked_parent, context);
2067
+ fn linearize_superclass(&mut self, declaration_id: DeclarationId, state: &mut ChainState) -> Option<Ancestors> {
2068
+ let (superclass_id, unresolved_superclass) = self.get_superclass(declaration_id)?;
2069
+ let mut result = self.linearize_ancestors(superclass_id);
2095
2070
 
2096
- if let Some(name_id) = unresolved_parent {
2097
- context.partial = true;
2071
+ if let Some(name_id) = unresolved_superclass {
2072
+ state.partial = true;
2098
2073
 
2099
- // Insert the unresolved parent as a Partial ancestor at the front of the chain, so it
2074
+ // Insert the unresolved superclass as a Partial ancestor at the front of the chain, so it
2100
2075
  // appears before the default Object ancestors
2101
2076
  let ancestors = match &mut result {
2102
2077
  Ancestors::Complete(ids) | Ancestors::Cyclic(ids) | Ancestors::Partial(ids) => ids,
2103
2078
  };
2104
2079
  ancestors.insert(0, Ancestor::Partial(name_id));
2105
2080
 
2106
- result.to_partial()
2107
- } else {
2108
- result
2081
+ result = result.to_partial();
2109
2082
  }
2110
- }
2111
2083
 
2112
- fn mixins_of(&self, definition_id: DefinitionId) -> Option<Vec<Mixin>> {
2113
- let definition = self.graph.definitions().get(&definition_id).unwrap();
2084
+ Some(result)
2085
+ }
2114
2086
 
2115
- match definition {
2116
- Definition::Class(class) => Some(class.mixins().to_vec()),
2117
- Definition::SingletonClass(class) => Some(class.mixins().to_vec()),
2118
- Definition::Module(module) => Some(module.mixins().to_vec()),
2119
- _ => None,
2087
+ fn mixins_of(&self, definition_id: DefinitionId) -> &[Mixin] {
2088
+ match self.graph.definitions().get(&definition_id).unwrap() {
2089
+ Definition::Class(class) => class.mixins(),
2090
+ Definition::SingletonClass(class) => class.mixins(),
2091
+ Definition::Module(module) => module.mixins(),
2092
+ _ => &[],
2120
2093
  }
2121
2094
  }
2122
2095
  }