rubydex 0.3.0 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/diagnostic.c +75 -1
  8. data/ext/rubydex/diagnostic.h +2 -0
  9. data/ext/rubydex/graph.c +13 -45
  10. data/ext/rubydex/graph.h +6 -0
  11. data/ext/rubydex/query.c +398 -16
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +11 -4
  14. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  15. data/lib/rubydex/cli/command/console.rb +55 -0
  16. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  17. data/lib/rubydex/cli/command/lint.rb +202 -0
  18. data/lib/rubydex/cli/command/mcp.rb +30 -0
  19. data/lib/rubydex/cli/command/query.rb +70 -0
  20. data/lib/rubydex/cli/command/skill.rb +69 -0
  21. data/lib/rubydex/cli/command.rb +168 -0
  22. data/lib/rubydex/cli.rb +93 -0
  23. data/lib/rubydex/config.rb +59 -0
  24. data/lib/rubydex/diagnostic.rb +12 -3
  25. data/lib/rubydex/errors.rb +42 -1
  26. data/lib/rubydex/graph.rb +10 -3
  27. data/lib/rubydex/linter/custom_rule.rb +97 -0
  28. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  29. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  30. data/lib/rubydex/linter/rule_loader.rb +36 -0
  31. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  32. data/lib/rubydex/linter/runner.rb +56 -0
  33. data/lib/rubydex/linter.rb +19 -0
  34. data/lib/rubydex/location.rb +3 -0
  35. data/lib/rubydex/mcp_server.rb +1 -2
  36. data/lib/rubydex/related_information.rb +17 -0
  37. data/lib/rubydex/rule.rb +33 -0
  38. data/lib/rubydex/severity.rb +70 -0
  39. data/lib/rubydex/skill.rb +88 -0
  40. data/lib/rubydex/skill_registry.rb +62 -0
  41. data/lib/rubydex/version.rb +1 -1
  42. data/lib/rubydex.rb +6 -0
  43. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  44. data/rbi/rubydex.rbi +558 -17
  45. data/rust/Cargo.lock +2 -2
  46. data/rust/rubydex/Cargo.toml +1 -1
  47. data/rust/rubydex/benches/graph_memory.rs +3 -5
  48. data/rust/rubydex/src/config.rs +538 -157
  49. data/rust/rubydex/src/diagnostic.rs +66 -40
  50. data/rust/rubydex/src/errors.rs +0 -1
  51. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  52. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  53. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  54. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  55. data/rust/rubydex/src/lib.rs +1 -0
  56. data/rust/rubydex/src/listing.rs +26 -1
  57. data/rust/rubydex/src/main.rs +7 -4
  58. data/rust/rubydex/src/model/declaration.rs +302 -219
  59. data/rust/rubydex/src/model/graph.rs +27 -40
  60. data/rust/rubydex/src/model/name.rs +58 -17
  61. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  62. data/rust/rubydex/src/path_helpers.rs +77 -0
  63. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  64. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  65. data/rust/rubydex/src/query/cypher.rs +8 -11
  66. data/rust/rubydex/src/query.rs +123 -43
  67. data/rust/rubydex/src/resolution.rs +139 -187
  68. data/rust/rubydex/src/resolution_tests.rs +247 -19
  69. data/rust/rubydex/src/test_utils/context.rs +2 -1
  70. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  71. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  72. data/rust/rubydex/tests/cli.rs +4 -4
  73. data/rust/rubydex-sys/src/config_api.rs +205 -0
  74. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  75. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  76. data/rust/rubydex-sys/src/graph_api.rs +13 -194
  77. data/rust/rubydex-sys/src/lib.rs +2 -0
  78. data/rust/rubydex-sys/src/name_api.rs +2 -6
  79. data/rust/rubydex-sys/src/utils.rs +37 -0
  80. data/skills/send-private-method/SKILL.md +133 -0
  81. metadata +31 -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::{
@@ -51,11 +48,32 @@ enum SingletonAncestors {
51
48
  Deferred,
52
49
  }
53
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
+
54
74
  struct LinearizationContext {
55
75
  descendants: IdentityHashSet<DeclarationId>,
56
76
  seen_ids: IdentityHashSet<DeclarationId>,
57
- cyclic: bool,
58
- partial: bool,
59
77
  }
60
78
 
61
79
  impl LinearizationContext {
@@ -63,8 +81,6 @@ impl LinearizationContext {
63
81
  Self {
64
82
  descendants: IdentityHashSet::default(),
65
83
  seen_ids: IdentityHashSet::default(),
66
- cyclic: false,
67
- partial: false,
68
84
  }
69
85
  }
70
86
 
@@ -83,6 +99,8 @@ pub struct Resolver<'a> {
83
99
  unit_queue: VecDeque<Unit>,
84
100
  /// Whether we made any progress in the last pass of the resolution loop
85
101
  made_progress: bool,
102
+ /// The linearization context carried across recursion
103
+ context: LinearizationContext,
86
104
  }
87
105
 
88
106
  impl<'a> Resolver<'a> {
@@ -91,6 +109,7 @@ impl<'a> Resolver<'a> {
91
109
  graph,
92
110
  unit_queue: VecDeque::new(),
93
111
  made_progress: false,
112
+ context: LinearizationContext::new(),
94
113
  }
95
114
  }
96
115
 
@@ -229,12 +248,38 @@ impl<'a> Resolver<'a> {
229
248
  Outcome::Resolved(id) => {
230
249
  if needs_linearization {
231
250
  self.unit_queue.push_back(Unit::Ancestors(id));
251
+ self.relinearize_singleton_hierarchy(id);
232
252
  }
233
253
  self.made_progress = true;
234
254
  }
235
255
  }
236
256
  }
237
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
+
238
283
  /// Handles a unit of work for resolving a constant reference
239
284
  fn handle_reference_unit(&mut self, unit_id: Unit, id: ConstantReferenceId) {
240
285
  let constant_ref = self.graph.constant_references().get(&id).unwrap();
@@ -257,7 +302,7 @@ impl<'a> Resolver<'a> {
257
302
 
258
303
  /// Handles a unit of work for linearizing ancestors of a declaration
259
304
  fn handle_ancestor_unit(&mut self, id: DeclarationId) {
260
- match self.ancestors_of(id) {
305
+ match self.linearize_ancestors(id) {
261
306
  Ancestors::Complete(_) | Ancestors::Cyclic(_) => {
262
307
  // We succeeded in some capacity this time
263
308
  self.made_progress = true;
@@ -928,7 +973,7 @@ impl<'a> Resolver<'a> {
928
973
  fn schedule_singleton_ancestors(&mut self, id: DeclarationId, mode: SingletonAncestors) {
929
974
  match mode {
930
975
  SingletonAncestors::Eager => {
931
- let _ = self.ancestors_of(id);
976
+ let _ = self.linearize_ancestors(id);
932
977
  }
933
978
  SingletonAncestors::Enqueue => {
934
979
  self.unit_queue.push_back(Unit::Ancestors(id));
@@ -945,35 +990,24 @@ impl<'a> Resolver<'a> {
945
990
  ///
946
991
  /// Can panic if there's inconsistent data in the graph
947
992
  #[must_use]
948
- fn ancestors_of(&mut self, declaration_id: DeclarationId) -> Ancestors {
949
- let mut context = LinearizationContext::new();
950
- self.linearize_ancestors(declaration_id, &mut context)
951
- }
952
-
953
- /// Linearizes the ancestors of a declaration, returning the list of ancestor declaration IDs
954
- ///
955
- /// # Panics
956
- ///
957
- /// Can panic if there's inconsistent data in the graph
958
- #[must_use]
959
- fn linearize_ancestors(&mut self, declaration_id: DeclarationId, context: &mut LinearizationContext) -> Ancestors {
993
+ fn linearize_ancestors(&mut self, declaration_id: DeclarationId) -> Ancestors {
960
994
  {
961
995
  let declaration = self.graph.declarations_mut().get_mut(&declaration_id).unwrap();
962
996
 
963
997
  // Add this declaration to the descendants so that we capture transitive descendant relationships
964
- context.descendants.insert(declaration_id);
998
+ self.context.descendants.insert(declaration_id);
965
999
 
966
1000
  // Return the cached ancestors if we already computed them. If they are partial ancestors, ignore the cache to try
967
1001
  // again
968
1002
  if declaration.as_namespace().unwrap().has_complete_ancestors() {
969
1003
  let cached = declaration.as_namespace().unwrap().clone_ancestors();
970
- self.propagate_descendants(&mut context.descendants, &cached);
1004
+ self.propagate_descendants(&cached);
971
1005
 
972
- context.finalize(declaration_id);
1006
+ self.context.finalize(declaration_id);
973
1007
  return cached;
974
1008
  }
975
1009
 
976
- if !context.seen_ids.insert(declaration_id) {
1010
+ if !self.context.seen_ids.insert(declaration_id) {
977
1011
  // If we find a cycle when linearizing ancestors, it's an error that the programmer must fix. However, we try to
978
1012
  // still approximate features by assuming that it must inherit from `Object` at some point (which is what most
979
1013
  // classes/modules inherit from). This is not 100% correct, but it allows us to provide a bit better IDE support
@@ -988,24 +1022,20 @@ impl<'a> Resolver<'a> {
988
1022
  .unwrap()
989
1023
  .set_ancestors(estimated_ancestors.clone());
990
1024
 
991
- context.finalize(declaration_id);
1025
+ self.context.finalize(declaration_id);
992
1026
  return estimated_ancestors;
993
1027
  }
994
1028
 
995
1029
  // Automatically track descendants as we recurse. This has to happen before checking the cache since we may have
996
1030
  // already linearized the parent's ancestors, but it's the first time we're discovering the descendant
997
- for descendant in &context.descendants {
998
- self.graph
999
- .declarations_mut()
1000
- .get_mut(&declaration_id)
1001
- .unwrap()
1002
- .as_namespace_mut()
1003
- .unwrap()
1004
- .add_descendant(*descendant);
1031
+ let namespace = declaration.as_namespace_mut().unwrap();
1032
+ for descendant in &self.context.descendants {
1033
+ namespace.add_descendant(*descendant);
1005
1034
  }
1006
1035
  }
1007
1036
 
1008
- 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);
1009
1039
  let declaration = self.graph.declarations().get(&declaration_id).unwrap();
1010
1040
  let mut mixins = Vec::new();
1011
1041
 
@@ -1019,9 +1049,9 @@ impl<'a> Resolver<'a> {
1019
1049
  attached_decl
1020
1050
  .definitions()
1021
1051
  .iter()
1022
- .filter_map(|definition_id| self.mixins_of(*definition_id))
1023
- .flatten()
1024
- .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(),
1025
1055
  );
1026
1056
  }
1027
1057
 
@@ -1029,12 +1059,10 @@ impl<'a> Resolver<'a> {
1029
1059
  let mut has_extends = false;
1030
1060
 
1031
1061
  for definition_id in declaration.definitions() {
1032
- if let Some(def_mixins) = self.mixins_of(*definition_id) {
1033
- for mixin in def_mixins {
1034
- match mixin {
1035
- Mixin::Prepend(_) | Mixin::Include(_) => mixins.push(mixin),
1036
- Mixin::Extend(_) => has_extends = true,
1037
- }
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,
1038
1066
  }
1039
1067
  }
1040
1068
  }
@@ -1045,7 +1073,7 @@ impl<'a> Resolver<'a> {
1045
1073
  }
1046
1074
 
1047
1075
  let (linearized_prepends, linearized_includes) =
1048
- self.linearize_mixins(context, mixins, parent_ancestors.as_ref());
1076
+ self.linearize_mixins(mixins, parent_ancestors.as_ref(), &mut state);
1049
1077
 
1050
1078
  // Build the final list
1051
1079
  let mut ancestors = Vec::new();
@@ -1056,9 +1084,9 @@ impl<'a> Resolver<'a> {
1056
1084
  ancestors.extend(parents);
1057
1085
  }
1058
1086
 
1059
- let result = if context.cyclic {
1087
+ let result = if state.cyclic {
1060
1088
  Ancestors::Cyclic(ancestors)
1061
- } else if context.partial {
1089
+ } else if state.partial {
1062
1090
  Ancestors::Partial(ancestors)
1063
1091
  } else {
1064
1092
  Ancestors::Complete(ancestors)
@@ -1072,50 +1100,32 @@ impl<'a> Resolver<'a> {
1072
1100
  .unwrap()
1073
1101
  .set_ancestors(result.clone());
1074
1102
 
1075
- context.finalize(declaration_id);
1103
+ self.context.finalize(declaration_id);
1076
1104
  result
1077
1105
  }
1078
1106
 
1079
1107
  fn linearize_parent_ancestors(
1080
1108
  &mut self,
1081
1109
  declaration_id: DeclarationId,
1082
- context: &mut LinearizationContext,
1110
+ state: &mut ChainState,
1083
1111
  ) -> Option<Vec<Ancestor>> {
1084
1112
  let declaration = self.graph.declarations().get(&declaration_id).unwrap();
1085
1113
 
1086
1114
  match declaration {
1087
1115
  Declaration::Namespace(Namespace::Class(_)) => {
1088
- Some(match self.linearize_superclass(declaration_id, context)? {
1089
- Ancestors::Complete(ids) => ids,
1090
- Ancestors::Cyclic(ids) => {
1091
- context.cyclic = true;
1092
- ids
1093
- }
1094
- Ancestors::Partial(ids) => {
1095
- context.partial = true;
1096
- ids
1097
- }
1098
- })
1116
+ let superclass = self.linearize_superclass(declaration_id, state)?;
1117
+ Some(state.fold(superclass))
1099
1118
  }
1100
1119
  Declaration::Namespace(Namespace::SingletonClass(_)) => {
1101
1120
  let owner_id = *declaration.owner_id();
1102
1121
 
1103
1122
  let (singleton_parent_id, partial_singleton) = self.singleton_parent_id(owner_id);
1104
1123
  if partial_singleton {
1105
- context.partial = true;
1124
+ state.partial = true;
1106
1125
  }
1107
1126
 
1108
- Some(match self.linearize_ancestors(singleton_parent_id, context) {
1109
- Ancestors::Complete(ids) => ids,
1110
- Ancestors::Cyclic(ids) => {
1111
- context.cyclic = true;
1112
- ids
1113
- }
1114
- Ancestors::Partial(ids) => {
1115
- context.partial = true;
1116
- ids
1117
- }
1118
- })
1127
+ let parent = self.linearize_ancestors(singleton_parent_id);
1128
+ Some(state.fold(parent))
1119
1129
  }
1120
1130
  _ => None,
1121
1131
  }
@@ -1125,9 +1135,9 @@ impl<'a> Resolver<'a> {
1125
1135
  /// modules are deduplicated against them
1126
1136
  fn linearize_mixins(
1127
1137
  &mut self,
1128
- context: &mut LinearizationContext,
1129
1138
  mixins: Vec<Mixin>,
1130
1139
  parent_ancestors: Option<&Vec<Ancestor>>,
1140
+ state: &mut ChainState,
1131
1141
  ) -> (VecDeque<Ancestor>, VecDeque<Ancestor>) {
1132
1142
  let mut linearized_prepends = VecDeque::new();
1133
1143
  let mut linearized_includes = VecDeque::new();
@@ -1150,17 +1160,8 @@ impl<'a> Resolver<'a> {
1150
1160
  continue;
1151
1161
  };
1152
1162
 
1153
- let ids = match self.linearize_ancestors(module_id, context) {
1154
- Ancestors::Complete(ids) => ids,
1155
- Ancestors::Cyclic(ids) => {
1156
- context.cyclic = true;
1157
- ids
1158
- }
1159
- Ancestors::Partial(ids) => {
1160
- context.partial = true;
1161
- ids
1162
- }
1163
- };
1163
+ let module_ancestors = self.linearize_ancestors(module_id);
1164
+ let ids = state.fold(module_ancestors);
1164
1165
 
1165
1166
  // Only reorder if there are new modules to add. If all modules being
1166
1167
  // prepended are already in the chain (e.g., `prepend A` when A is already
@@ -1177,7 +1178,7 @@ impl<'a> Resolver<'a> {
1177
1178
  NameRef::Unresolved(_) => {
1178
1179
  // We haven't been able to resolve this name yet, so we push it as a partial linearization to finish
1179
1180
  // later
1180
- context.partial = true;
1181
+ state.partial = true;
1181
1182
  linearized_prepends.push_front(Ancestor::Partial(*constant_reference.name_id()));
1182
1183
  }
1183
1184
  }
@@ -1189,17 +1190,8 @@ impl<'a> Resolver<'a> {
1189
1190
  continue;
1190
1191
  };
1191
1192
 
1192
- let mut ids = match self.linearize_ancestors(module_id, context) {
1193
- Ancestors::Complete(ids) => ids,
1194
- Ancestors::Cyclic(ids) => {
1195
- context.cyclic = true;
1196
- ids
1197
- }
1198
- Ancestors::Partial(ids) => {
1199
- context.partial = true;
1200
- ids
1201
- }
1202
- };
1193
+ let module_ancestors = self.linearize_ancestors(module_id);
1194
+ let mut ids = state.fold(module_ancestors);
1203
1195
 
1204
1196
  // Prepended module are deduped based only on other prepended modules
1205
1197
  ids.retain(|id| {
@@ -1217,7 +1209,7 @@ impl<'a> Resolver<'a> {
1217
1209
  NameRef::Unresolved(_) => {
1218
1210
  // We haven't been able to resolve this name yet, so we push it as a partial linearization to finish
1219
1211
  // later
1220
- context.partial = true;
1212
+ state.partial = true;
1221
1213
  linearized_includes.push_front(Ancestor::Partial(*constant_reference.name_id()));
1222
1214
  }
1223
1215
  }
@@ -1229,11 +1221,9 @@ impl<'a> Resolver<'a> {
1229
1221
  }
1230
1222
 
1231
1223
  /// Propagate descendants to all cached ancestors
1232
- fn propagate_descendants<S: BuildHasher>(
1233
- &mut self,
1234
- descendants: &mut HashSet<DeclarationId, S>,
1235
- cached: &Ancestors,
1236
- ) {
1224
+ fn propagate_descendants(&mut self, cached: &Ancestors) {
1225
+ let descendants = &self.context.descendants;
1226
+
1237
1227
  if !descendants.is_empty() {
1238
1228
  for ancestor in cached {
1239
1229
  if let Ancestor::Complete(ancestor_id) = ancestor {
@@ -1245,7 +1235,7 @@ impl<'a> Resolver<'a> {
1245
1235
  .as_namespace_mut()
1246
1236
  .unwrap();
1247
1237
 
1248
- for descendant in descendants.iter() {
1238
+ for descendant in descendants {
1249
1239
  namespace.add_descendant(*descendant);
1250
1240
  }
1251
1241
  }
@@ -1443,6 +1433,9 @@ impl<'a> Resolver<'a> {
1443
1433
  parent_owner_id,
1444
1434
  )))));
1445
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));
1446
1439
  }
1447
1440
 
1448
1441
  declaration_id
@@ -1735,8 +1728,13 @@ impl<'a> Resolver<'a> {
1735
1728
  if is_module || chain_incomplete {
1736
1729
  let object_outcome = self.search_ancestors(*OBJECT_ID, str_id);
1737
1730
 
1738
- if let Outcome::Resolved(decl_id) = object_outcome {
1739
- 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 => {}
1740
1738
  }
1741
1739
  }
1742
1740
 
@@ -1751,7 +1749,7 @@ impl<'a> Resolver<'a> {
1751
1749
 
1752
1750
  /// Search for a member in a declaration's ancestor chain.
1753
1751
  fn search_ancestors(&mut self, declaration_id: DeclarationId, str_id: StringId) -> Outcome {
1754
- match self.ancestors_of(declaration_id) {
1752
+ match self.linearize_ancestors(declaration_id) {
1755
1753
  Ancestors::Complete(ids) | Ancestors::Cyclic(ids) => ids
1756
1754
  .iter()
1757
1755
  .find_map(|ancestor_id| {
@@ -1829,9 +1827,15 @@ impl<'a> Resolver<'a> {
1829
1827
  Outcome::Unresolved
1830
1828
  }
1831
1829
 
1832
- /// Returns a complexity score for a given name, which is used to sort names for resolution. The complexity is based
1833
- /// on how many parent scopes are involved in a name's nesting. This is because simple names are always
1834
- /// 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:
1835
1839
  ///
1836
1840
  /// ```ruby
1837
1841
  /// module Foo
@@ -1841,9 +1845,8 @@ impl<'a> Resolver<'a> {
1841
1845
  /// end
1842
1846
  /// ```
1843
1847
  ///
1844
- /// These are all simple names because they don't require resolution logic to determine the final name of each step.
1845
- /// We only have to ensure that they are ordered by nesting level. Names with parent scopes require that their parts
1846
- /// 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:
1847
1850
  ///
1848
1851
  /// ```ruby
1849
1852
  /// module Foo
@@ -1853,51 +1856,9 @@ impl<'a> Resolver<'a> {
1853
1856
  /// end
1854
1857
  /// ```
1855
1858
  ///
1856
- /// In this case, we need `Bar` to have already been processed so that we can resolve the `Bar` reference inside of
1857
- /// the `Foo` nesting, which then unblocks the resolution of `Baz` and finally `Qux`. Notice how `Qux` is a simple
1858
- /// name, but it's nested under a complex name so we have to sort it last. This is why we consider the number of
1859
- /// parent scopes in the entire nesting, not just for the name itself
1860
- ///
1861
- /// Compute the depth of a name in the graph by recursively summing the depths of its
1862
- /// `parent_scope` and `nesting` chains. Results are memoized in `cache` (`NameId` → depth)
1863
- /// so each name is computed at most once across all calls.
1864
- ///
1865
- /// Depth represents the total complexity of a name's position in the namespace hierarchy.
1866
- /// For example, in `module Foo; module Bar; class Baz; end; end; end`, Foo has depth 1
1867
- /// (top-level), Bar has depth 2, and Baz has depth 3.
1868
- ///
1869
- /// # Panics
1870
- ///
1871
- /// Will panic if there is inconsistent data in the graph
1872
- fn name_depth(
1873
- name_id: NameId,
1874
- names: &IdentityHashMap<NameId, NameRef>,
1875
- cache: &mut IdentityHashMap<NameId, u32>,
1876
- ) -> u32 {
1877
- if let Some(&depth) = cache.get(&name_id) {
1878
- return depth;
1879
- }
1880
-
1881
- let name = names.get(&name_id).unwrap();
1882
-
1883
- let depth = if name.parent_scope().is_top_level() {
1884
- 1
1885
- } else {
1886
- let parent_depth = name.parent_scope().map_or(0, |id| Self::name_depth(*id, names, cache));
1887
-
1888
- let nesting_depth = name.nesting().map_or(0, |id| Self::name_depth(id, names, cache));
1889
-
1890
- parent_depth + nesting_depth + 1
1891
- };
1892
-
1893
- cache.insert(name_id, depth);
1894
- depth
1895
- }
1896
-
1897
- /// Drains `pending_work` and classifies items into the resolution queue.
1898
- /// Namespace definitions and constant references are sorted by name depth for deterministic
1899
- /// resolution order. Non-namespace definitions (methods, attrs, variables) are returned
1900
- /// 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.
1901
1862
  fn prepare_units(&mut self) -> Vec<DefinitionId> {
1902
1863
  let work = self.graph.take_pending_work();
1903
1864
  let estimated = work.len() / 2;
@@ -1907,16 +1868,13 @@ impl<'a> Resolver<'a> {
1907
1868
  let mut const_refs = Vec::new();
1908
1869
  let mut ancestors = vec![*BASIC_OBJECT_ID, *KERNEL_ID, *OBJECT_ID, *MODULE_ID, *CLASS_ID];
1909
1870
  let names = self.graph.names();
1910
- // Memoize name depths on demand for only the names referenced by the pending work. During incremental
1911
- // resolution this avoids walking every name in the graph just to sort the small subset of units below.
1912
- // A pending unit references at most one name, so `estimated` (bounded by the total number of names) is a
1913
- // reasonable capacity that avoids rehashing as depths accumulate.
1914
- let mut depths: IdentityHashMap<NameId, u32> =
1915
- IdentityHashMap::with_capacity_and_hasher(estimated.min(names.len()), IdentityHashBuilder);
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();
1916
1874
 
1917
1875
  // Precompute the lexicographic rank of every document URI. Definitions and references are sorted by
1918
- // (name depth, URI, offset) below; storing precomputed integer sort keys instead of looking up name
1919
- // depths and comparing URI strings on every comparison makes sorting substantially cheaper on large graphs.
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.
1920
1878
  let mut uris: Vec<(&str, UriId)> = self
1921
1879
  .graph
1922
1880
  .documents()
@@ -1950,23 +1908,23 @@ impl<'a> Resolver<'a> {
1950
1908
 
1951
1909
  match definition {
1952
1910
  Definition::Class(def) => {
1953
- let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1911
+ let depth = depth_of(def.name_id());
1954
1912
  definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1955
1913
  }
1956
1914
  Definition::Module(def) => {
1957
- let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1915
+ let depth = depth_of(def.name_id());
1958
1916
  definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1959
1917
  }
1960
1918
  Definition::Constant(def) => {
1961
- let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1919
+ let depth = depth_of(def.name_id());
1962
1920
  definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1963
1921
  }
1964
1922
  Definition::ConstantAlias(def) => {
1965
- let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1923
+ let depth = depth_of(def.name_id());
1966
1924
  definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1967
1925
  }
1968
1926
  Definition::SingletonClass(def) => {
1969
- let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1927
+ let depth = depth_of(def.name_id());
1970
1928
  definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1971
1929
  }
1972
1930
  // SelfReceiver methods create singleton classes, which need
@@ -1989,7 +1947,7 @@ impl<'a> Resolver<'a> {
1989
1947
  continue;
1990
1948
  };
1991
1949
  let uri_rank = *uri_ranks.get(&constant_ref.uri_id()).unwrap();
1992
- let depth = Self::name_depth(*constant_ref.name_id(), names, &mut depths);
1950
+ let depth = depth_of(constant_ref.name_id());
1993
1951
  const_refs.push((Unit::ConstantRef(id), (depth, uri_rank, constant_ref.offset())));
1994
1952
  }
1995
1953
  Unit::Ancestors(id) => {
@@ -2106,16 +2064,12 @@ impl<'a> Resolver<'a> {
2106
2064
  ))
2107
2065
  }
2108
2066
 
2109
- fn linearize_superclass(
2110
- &mut self,
2111
- declaration_id: DeclarationId,
2112
- context: &mut LinearizationContext,
2113
- ) -> Option<Ancestors> {
2067
+ fn linearize_superclass(&mut self, declaration_id: DeclarationId, state: &mut ChainState) -> Option<Ancestors> {
2114
2068
  let (superclass_id, unresolved_superclass) = self.get_superclass(declaration_id)?;
2115
- let mut result = self.linearize_ancestors(superclass_id, context);
2069
+ let mut result = self.linearize_ancestors(superclass_id);
2116
2070
 
2117
2071
  if let Some(name_id) = unresolved_superclass {
2118
- context.partial = true;
2072
+ state.partial = true;
2119
2073
 
2120
2074
  // Insert the unresolved superclass as a Partial ancestor at the front of the chain, so it
2121
2075
  // appears before the default Object ancestors
@@ -2130,14 +2084,12 @@ impl<'a> Resolver<'a> {
2130
2084
  Some(result)
2131
2085
  }
2132
2086
 
2133
- fn mixins_of(&self, definition_id: DefinitionId) -> Option<Vec<Mixin>> {
2134
- let definition = self.graph.definitions().get(&definition_id).unwrap();
2135
-
2136
- match definition {
2137
- Definition::Class(class) => Some(class.mixins().to_vec()),
2138
- Definition::SingletonClass(class) => Some(class.mixins().to_vec()),
2139
- Definition::Module(module) => Some(module.mixins().to_vec()),
2140
- _ => 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
+ _ => &[],
2141
2093
  }
2142
2094
  }
2143
2095
  }