rubydex 0.1.0.beta12-aarch64-linux

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +1,3754 @@
1
+ use std::collections::HashSet;
2
+ use std::collections::hash_map::Entry;
3
+ use std::path::PathBuf;
4
+
5
+ use crate::diagnostic::Diagnostic;
6
+ use crate::indexing::local_graph::LocalGraph;
7
+ use crate::model::built_in::{OBJECT_ID, add_built_in_data};
8
+ use crate::model::declaration::{Ancestor, Declaration, Namespace};
9
+ use crate::model::definitions::{Definition, Receiver};
10
+ use crate::model::document::Document;
11
+ use crate::model::encoding::Encoding;
12
+ use crate::model::identity_maps::{IdentityHashMap, IdentityHashSet};
13
+ use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId};
14
+ use crate::model::name::{Name, NameRef, ParentScope, ResolvedName};
15
+ use crate::model::references::{ConstantReference, MethodRef};
16
+ use crate::model::string_ref::StringRef;
17
+ use crate::stats;
18
+
19
+ /// An entity whose validity depends on a particular `NameId`.
20
+ /// Used as the value type in the `name_dependents` reverse index.
21
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22
+ pub enum NameDependent {
23
+ Definition(DefinitionId),
24
+ Reference(ConstantReferenceId),
25
+ /// This name's `parent_scope` is the key name — structural dependency.
26
+ ChildName(NameId),
27
+ /// This name's `nesting` is the key name — reference-only dependency.
28
+ NestedName(NameId),
29
+ }
30
+
31
+ /// Items processed by the unified invalidation worklist.
32
+ enum InvalidationItem {
33
+ /// Ancestor chain is stale, or declaration has become empty and needs removal.
34
+ Declaration(DeclarationId),
35
+ /// Structural dependency broken — unresolve the name and cascade to all dependents.
36
+ Name(NameId),
37
+ /// Ancestor context changed — unresolve references under this name but keep the name resolved.
38
+ References(NameId),
39
+ }
40
+
41
+ /// A work item produced by graph mutations (update/delete) that needs resolution.
42
+ #[derive(Debug)]
43
+ pub enum Unit {
44
+ /// A definition that defines a constant and might require resolution
45
+ Definition(DefinitionId),
46
+ /// A constant reference that needs to be resolved
47
+ ConstantRef(ConstantReferenceId),
48
+ /// A declaration whose ancestors need re-linearization
49
+ Ancestors(DeclarationId),
50
+ }
51
+
52
+ // The `Graph` is the global representation of the entire Ruby codebase. It contains all declarations and their
53
+ // relationships
54
+ #[derive(Default, Debug)]
55
+ pub struct Graph {
56
+ // Map of declaration nodes
57
+ declarations: IdentityHashMap<DeclarationId, Declaration>,
58
+ // Map of document nodes
59
+ documents: IdentityHashMap<UriId, Document>,
60
+ // Map of definition nodes
61
+ definitions: IdentityHashMap<DefinitionId, Definition>,
62
+
63
+ // Map of unqualified names
64
+ strings: IdentityHashMap<StringId, StringRef>,
65
+ // Map of names
66
+ names: IdentityHashMap<NameId, NameRef>,
67
+ // Map of constant references
68
+ constant_references: IdentityHashMap<ConstantReferenceId, ConstantReference>,
69
+ // Map of method references that still need to be resolved
70
+ method_references: IdentityHashMap<MethodReferenceId, MethodRef>,
71
+
72
+ /// The position encoding used for LSP line/column locations. Not related to the actual encoding of the file
73
+ position_encoding: Encoding,
74
+
75
+ /// Reverse index: for each `NameId`, which definitions, references, and child/nested names depend on it.
76
+ /// Used during invalidation to efficiently find affected entities without scanning the full graph.
77
+ name_dependents: IdentityHashMap<NameId, Vec<NameDependent>>,
78
+
79
+ /// Accumulated work items from update/delete operations.
80
+ /// Drained by `take_pending_work()` before resolution.
81
+ pending_work: Vec<Unit>,
82
+
83
+ /// Paths to exclude from file discovery during indexing.
84
+ excluded_paths: HashSet<PathBuf>,
85
+ }
86
+
87
+ impl Graph {
88
+ #[must_use]
89
+ pub fn new() -> Self {
90
+ let mut graph = Self {
91
+ declarations: IdentityHashMap::default(),
92
+ definitions: IdentityHashMap::default(),
93
+ documents: IdentityHashMap::default(),
94
+ strings: IdentityHashMap::default(),
95
+ names: IdentityHashMap::default(),
96
+ constant_references: IdentityHashMap::default(),
97
+ method_references: IdentityHashMap::default(),
98
+ position_encoding: Encoding::default(),
99
+ name_dependents: IdentityHashMap::default(),
100
+ pending_work: Vec::default(),
101
+ excluded_paths: HashSet::new(),
102
+ };
103
+
104
+ add_built_in_data(&mut graph);
105
+ graph
106
+ }
107
+
108
+ // Returns an immutable reference to the declarations map
109
+ #[must_use]
110
+ pub fn declarations(&self) -> &IdentityHashMap<DeclarationId, Declaration> {
111
+ &self.declarations
112
+ }
113
+
114
+ /// Returns a mutable reference to the declarations map
115
+ #[must_use]
116
+ pub fn declarations_mut(&mut self) -> &mut IdentityHashMap<DeclarationId, Declaration> {
117
+ &mut self.declarations
118
+ }
119
+
120
+ /// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
121
+ /// directory traversal.
122
+ pub fn exclude_paths(&mut self, paths: Vec<PathBuf>) {
123
+ self.excluded_paths.extend(paths);
124
+ }
125
+
126
+ /// Returns the set of paths excluded from file discovery.
127
+ #[must_use]
128
+ pub fn excluded_paths(&self) -> &HashSet<PathBuf> {
129
+ &self.excluded_paths
130
+ }
131
+
132
+ /// # Panics
133
+ ///
134
+ /// Will panic if the `definition_id` is not registered in the graph
135
+ pub fn add_declaration<F>(
136
+ &mut self,
137
+ definition_id: DefinitionId,
138
+ fully_qualified_name: String,
139
+ constructor: F,
140
+ ) -> DeclarationId
141
+ where
142
+ F: FnOnce(String) -> Declaration,
143
+ {
144
+ let declaration_id = DeclarationId::from(&fully_qualified_name);
145
+
146
+ let is_namespace_definition = matches!(
147
+ self.definitions.get(&definition_id),
148
+ Some(Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_))
149
+ );
150
+
151
+ let should_promote = is_namespace_definition
152
+ && self
153
+ .declarations
154
+ .get(&declaration_id)
155
+ .is_some_and(|existing| match existing {
156
+ Declaration::Constant(_) => self.all_definitions_promotable(existing),
157
+ Declaration::Namespace(Namespace::Todo(_)) => true,
158
+ _ => false,
159
+ });
160
+
161
+ match self.declarations.entry(declaration_id) {
162
+ Entry::Occupied(mut occupied_entry) => {
163
+ debug_assert!(
164
+ occupied_entry.get().name() == fully_qualified_name,
165
+ "DeclarationId collision in global graph"
166
+ );
167
+
168
+ if should_promote {
169
+ let mut new_declaration = constructor(fully_qualified_name);
170
+ let removed_declaration = occupied_entry.remove();
171
+ new_declaration.as_namespace_mut().unwrap().extend(removed_declaration);
172
+ new_declaration.add_definition(definition_id);
173
+ self.declarations.insert(declaration_id, new_declaration);
174
+ } else {
175
+ occupied_entry.get_mut().add_definition(definition_id);
176
+ }
177
+ }
178
+ Entry::Vacant(vacant_entry) => {
179
+ let mut declaration = constructor(fully_qualified_name);
180
+ declaration.add_definition(definition_id);
181
+ vacant_entry.insert(declaration);
182
+ }
183
+ }
184
+
185
+ declaration_id
186
+ }
187
+
188
+ /// Checks if all constant definitions for a declaration have the PROMOTABLE flag set.
189
+ /// Used to determine whether a constant can be promoted to a namespace.
190
+ #[must_use]
191
+ pub fn all_definitions_promotable(&self, declaration: &Declaration) -> bool {
192
+ declaration
193
+ .definitions()
194
+ .iter()
195
+ .all(|def_id| match self.definitions.get(def_id) {
196
+ Some(Definition::Constant(c)) => c.flags().is_promotable(),
197
+ _ => true,
198
+ })
199
+ }
200
+
201
+ /// Promotes a `Declaration::Constant` to a namespace using the provided constructor. Transfers all definitions,
202
+ /// references, and diagnostics from the old declaration.
203
+ ///
204
+ /// # Panics
205
+ ///
206
+ /// Will panic if the declaration ID doesn't exist
207
+ pub fn promote_constant_to_namespace<F>(&mut self, declaration_id: DeclarationId, constructor: F)
208
+ where
209
+ F: FnOnce(String, DeclarationId) -> Declaration,
210
+ {
211
+ let old_decl = self.declarations.remove(&declaration_id).unwrap();
212
+ let name = old_decl.name().to_string();
213
+ let owner_id = *old_decl.owner_id();
214
+
215
+ let mut new_decl = constructor(name, owner_id);
216
+ new_decl.as_namespace_mut().unwrap().extend(old_decl);
217
+
218
+ self.declarations.insert(declaration_id, new_decl);
219
+ }
220
+
221
+ #[must_use]
222
+ pub fn is_namespace(&self, declaration_id: &DeclarationId) -> bool {
223
+ self.declarations
224
+ .get(declaration_id)
225
+ .is_some_and(|decl| decl.as_namespace().is_some())
226
+ }
227
+
228
+ // Returns an immutable reference to the definitions map
229
+ #[must_use]
230
+ pub fn definitions(&self) -> &IdentityHashMap<DefinitionId, Definition> {
231
+ &self.definitions
232
+ }
233
+
234
+ /// Returns the ID of the unqualified name of a definition
235
+ ///
236
+ /// # Panics
237
+ ///
238
+ /// This will panic if there's inconsistent data in the graph
239
+ #[must_use]
240
+ pub fn definition_string_id(&self, definition: &Definition) -> StringId {
241
+ let id = match definition {
242
+ Definition::Class(it) => {
243
+ let name = self.names.get(it.name_id()).unwrap();
244
+ name.str()
245
+ }
246
+ Definition::SingletonClass(it) => {
247
+ let name = self.names.get(it.name_id()).unwrap();
248
+ name.str()
249
+ }
250
+ Definition::Module(it) => {
251
+ let name = self.names.get(it.name_id()).unwrap();
252
+ name.str()
253
+ }
254
+ Definition::Constant(it) => {
255
+ let name = self.names.get(it.name_id()).unwrap();
256
+ name.str()
257
+ }
258
+ Definition::ConstantAlias(it) => {
259
+ let name = self.names.get(it.name_id()).unwrap();
260
+ name.str()
261
+ }
262
+ Definition::ConstantVisibility(it) => {
263
+ let name = self.names.get(it.name_id()).unwrap();
264
+ name.str()
265
+ }
266
+ Definition::MethodVisibility(it) => it.str_id(),
267
+ Definition::GlobalVariable(it) => it.str_id(),
268
+ Definition::InstanceVariable(it) => it.str_id(),
269
+ Definition::ClassVariable(it) => it.str_id(),
270
+ Definition::AttrAccessor(it) => it.str_id(),
271
+ Definition::AttrReader(it) => it.str_id(),
272
+ Definition::AttrWriter(it) => it.str_id(),
273
+ Definition::Method(it) => it.str_id(),
274
+ Definition::MethodAlias(it) => it.new_name_str_id(),
275
+ Definition::GlobalVariableAlias(it) => it.new_name_str_id(),
276
+ };
277
+
278
+ *id
279
+ }
280
+
281
+ // Returns an immutable reference to the strings map
282
+ #[must_use]
283
+ pub fn strings(&self) -> &IdentityHashMap<StringId, StringRef> {
284
+ &self.strings
285
+ }
286
+
287
+ // Returns an immutable reference to the URI pool map
288
+ #[must_use]
289
+ pub fn documents(&self) -> &IdentityHashMap<UriId, Document> {
290
+ &self.documents
291
+ }
292
+
293
+ /// # Panics
294
+ ///
295
+ /// Panics if the definition is not found
296
+ #[must_use]
297
+ pub fn definition_id_to_declaration_id(&self, definition_id: DefinitionId) -> Option<&DeclarationId> {
298
+ self.definition_to_declaration_id(self.definitions.get(&definition_id).unwrap())
299
+ }
300
+
301
+ /// # Panics
302
+ ///
303
+ /// Panics if the definition is not found
304
+ #[must_use]
305
+ pub fn definition_to_declaration_id(&self, definition: &Definition) -> Option<&DeclarationId> {
306
+ let (nesting_name_id, member_str_id) = match definition {
307
+ Definition::Class(it) => {
308
+ return self.name_id_to_declaration_id(*it.name_id());
309
+ }
310
+ Definition::SingletonClass(it) => {
311
+ return self.name_id_to_declaration_id(*it.name_id());
312
+ }
313
+ Definition::Module(it) => {
314
+ return self.name_id_to_declaration_id(*it.name_id());
315
+ }
316
+ Definition::Constant(it) => {
317
+ return self.name_id_to_declaration_id(*it.name_id());
318
+ }
319
+ Definition::ConstantAlias(it) => {
320
+ return self.name_id_to_declaration_id(*it.name_id());
321
+ }
322
+ Definition::ConstantVisibility(it) => {
323
+ return self.name_id_to_declaration_id(*it.name_id());
324
+ }
325
+ Definition::MethodVisibility(it) => (
326
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
327
+ it.str_id(),
328
+ ),
329
+ Definition::GlobalVariable(it) => (
330
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
331
+ it.str_id(),
332
+ ),
333
+ Definition::GlobalVariableAlias(it) => (
334
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
335
+ it.new_name_str_id(),
336
+ ),
337
+ Definition::InstanceVariable(it) => (
338
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
339
+ it.str_id(),
340
+ ),
341
+ Definition::ClassVariable(it) => (
342
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
343
+ it.str_id(),
344
+ ),
345
+ Definition::AttrAccessor(it) => (
346
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
347
+ it.str_id(),
348
+ ),
349
+ Definition::AttrReader(it) => (
350
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
351
+ it.str_id(),
352
+ ),
353
+ Definition::AttrWriter(it) => (
354
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
355
+ it.str_id(),
356
+ ),
357
+ Definition::Method(it) => {
358
+ if let Some(Receiver::SelfReceiver(def_id)) = it.receiver() {
359
+ return self.find_self_receiver_declaration(*def_id, *it.str_id());
360
+ }
361
+ (
362
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
363
+ it.str_id(),
364
+ )
365
+ }
366
+ Definition::MethodAlias(it) => {
367
+ if let Some(Receiver::SelfReceiver(def_id)) = it.receiver() {
368
+ return self.find_self_receiver_declaration(*def_id, *it.new_name_str_id());
369
+ }
370
+ (
371
+ self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
372
+ it.new_name_str_id(),
373
+ )
374
+ }
375
+ };
376
+
377
+ let nesting_declaration_id = match nesting_name_id {
378
+ Some(name_id) => self.name_id_to_declaration_id(*name_id),
379
+ None => Some(&*OBJECT_ID),
380
+ }?;
381
+
382
+ self.declarations
383
+ .get(nesting_declaration_id)?
384
+ .as_namespace()?
385
+ .member(member_str_id)
386
+ }
387
+
388
+ /// Finds the closest namespace name ID to connect a definition to its declaration
389
+ fn find_enclosing_namespace_name_id(&self, starting_id: Option<&DefinitionId>) -> Option<&NameId> {
390
+ let mut current = starting_id;
391
+
392
+ while let Some(id) = current {
393
+ let def = self.definitions.get(id).unwrap();
394
+
395
+ if let Some(name_id) = def.name_id() {
396
+ return Some(name_id);
397
+ }
398
+
399
+ current = def.lexical_nesting_id().as_ref();
400
+ }
401
+
402
+ None
403
+ }
404
+
405
+ /// Looks up the declaration for a `SelfReceiver` method/alias through the singleton class.
406
+ fn find_self_receiver_declaration(&self, def_id: DefinitionId, member_str_id: StringId) -> Option<&DeclarationId> {
407
+ let owner_decl_id = self.definition_id_to_declaration_id(def_id)?;
408
+ let singleton_id = self
409
+ .declarations
410
+ .get(owner_decl_id)
411
+ .unwrap()
412
+ .as_namespace()
413
+ .unwrap()
414
+ .singleton_class()?;
415
+ self.declarations
416
+ .get(singleton_id)
417
+ .unwrap()
418
+ .as_namespace()
419
+ .unwrap()
420
+ .member(&member_str_id)
421
+ }
422
+
423
+ #[must_use]
424
+ pub fn name_id_to_declaration_id(&self, name_id: NameId) -> Option<&DeclarationId> {
425
+ let name = self.names.get(&name_id);
426
+
427
+ match name {
428
+ Some(NameRef::Resolved(resolved)) => Some(resolved.declaration_id()),
429
+ Some(NameRef::Unresolved(_)) | None => None,
430
+ }
431
+ }
432
+
433
+ // Returns an immutable reference to the constant references map
434
+ #[must_use]
435
+ pub fn constant_references(&self) -> &IdentityHashMap<ConstantReferenceId, ConstantReference> {
436
+ &self.constant_references
437
+ }
438
+
439
+ // Returns an immutable reference to the method references map
440
+ #[must_use]
441
+ pub fn method_references(&self) -> &IdentityHashMap<MethodReferenceId, MethodRef> {
442
+ &self.method_references
443
+ }
444
+
445
+ #[must_use]
446
+ pub fn all_diagnostics(&self) -> Vec<&Diagnostic> {
447
+ let document_diagnostics = self.documents.values().flat_map(Document::diagnostics);
448
+ let declaration_diagnostics = self.declarations.values().flat_map(Declaration::diagnostics);
449
+
450
+ document_diagnostics.chain(declaration_diagnostics).collect()
451
+ }
452
+
453
+ /// Interns a string in the graph unless already interned. This method is only used to back the
454
+ /// `Graph#resolve_constant` Ruby API because every string must be interned in the graph to properly resolve.
455
+ pub fn intern_string(&mut self, string: String) -> StringId {
456
+ let string_id = StringId::from(&string);
457
+ match self.strings.entry(string_id) {
458
+ Entry::Occupied(mut entry) => {
459
+ entry.get_mut().increment_ref_count(1);
460
+ }
461
+ Entry::Vacant(entry) => {
462
+ entry.insert(StringRef::new(string));
463
+ }
464
+ }
465
+ string_id
466
+ }
467
+
468
+ /// Registers a name in the graph unless already registered. In regular indexing, this only happens in the local
469
+ /// graph. This method is only used to back the `Graph#resolve_constant` Ruby API because every name must be
470
+ /// registered in the graph to properly resolve
471
+ pub fn add_name(&mut self, name: Name) -> NameId {
472
+ let name_id = name.id();
473
+
474
+ match self.names.entry(name_id) {
475
+ Entry::Occupied(mut entry) => {
476
+ entry.get_mut().increment_ref_count(1);
477
+ }
478
+ Entry::Vacant(entry) => {
479
+ entry.insert(NameRef::Unresolved(Box::new(name)));
480
+ }
481
+ }
482
+
483
+ name_id
484
+ }
485
+
486
+ /// Searches for the initial attached object for an arbitrarily nested singleton class.
487
+ /// Walks up the owner chain until finding a non-singleton namespace.
488
+ ///
489
+ /// # Example
490
+ /// For `Foo::<Foo>::<<Foo>>`, returns `Foo`
491
+ ///
492
+ /// # Panics
493
+ ///
494
+ /// Panics if we attached a singleton class to something that isn't a namespace
495
+ #[must_use]
496
+ pub fn attached_object<'a>(&'a self, maybe_singleton: &'a Namespace) -> &'a Namespace {
497
+ let mut attached_object = maybe_singleton;
498
+
499
+ while matches!(attached_object, Namespace::SingletonClass(_)) {
500
+ attached_object = self
501
+ .declarations
502
+ .get(attached_object.owner_id())
503
+ .unwrap()
504
+ .as_namespace()
505
+ .unwrap();
506
+ }
507
+
508
+ attached_object
509
+ }
510
+
511
+ #[must_use]
512
+ pub fn get(&self, name: &str) -> Option<Vec<&Definition>> {
513
+ let declaration_id = DeclarationId::from(name);
514
+ let declaration = self.declarations.get(&declaration_id)?;
515
+
516
+ Some(
517
+ declaration
518
+ .definitions()
519
+ .iter()
520
+ .filter_map(|id| self.definitions.get(id))
521
+ .collect(),
522
+ )
523
+ }
524
+
525
+ /// Returns all target declaration IDs for a constant alias.
526
+ ///
527
+ /// A constant alias can have multiple definitions (e.g., conditional assignment in different files),
528
+ /// each potentially pointing to a different target. This method collects all resolved targets.
529
+ ///
530
+ /// Returns `None` if the declaration doesn't exist or is not a constant alias.
531
+ /// Returns `Some(vec![])` if no targets have been resolved yet.
532
+ #[must_use]
533
+ pub fn alias_targets(&self, declaration_id: &DeclarationId) -> Option<Vec<DeclarationId>> {
534
+ let declaration = self.declarations.get(declaration_id)?;
535
+
536
+ let Declaration::ConstantAlias(_) = declaration else {
537
+ return None;
538
+ };
539
+
540
+ let mut targets = Vec::new();
541
+ for definition_id in declaration.definitions() {
542
+ let Some(Definition::ConstantAlias(alias_def)) = self.definitions.get(definition_id) else {
543
+ continue;
544
+ };
545
+
546
+ let target_name_id = alias_def.target_name_id();
547
+ let Some(name_ref) = self.names.get(target_name_id) else {
548
+ continue;
549
+ };
550
+
551
+ if let NameRef::Resolved(resolved) = name_ref {
552
+ let target_id = *resolved.declaration_id();
553
+ if !targets.contains(&target_id) {
554
+ targets.push(target_id);
555
+ }
556
+ }
557
+ }
558
+
559
+ Some(targets)
560
+ }
561
+
562
+ /// Resolves a constant alias chain to the final non-alias declaration.
563
+ ///
564
+ /// Returns `None` if the declaration is not a constant alias, the chain is circular, or the chain leads to an
565
+ /// unresolved name.
566
+ #[must_use]
567
+ pub fn resolve_alias(&self, declaration_id: &DeclarationId) -> Option<DeclarationId> {
568
+ let mut seen = IdentityHashSet::default();
569
+ let mut current_id = *declaration_id;
570
+
571
+ loop {
572
+ if !seen.insert(current_id) {
573
+ return None;
574
+ }
575
+
576
+ if let Some(targets) = self.alias_targets(&current_id)
577
+ && let Some(&first_target) = targets.first()
578
+ {
579
+ if matches!(
580
+ self.declarations.get(&first_target),
581
+ Some(Declaration::ConstantAlias(_))
582
+ ) {
583
+ current_id = first_target;
584
+ continue;
585
+ }
586
+
587
+ return Some(first_target);
588
+ }
589
+
590
+ return None;
591
+ }
592
+ }
593
+
594
+ #[must_use]
595
+ pub fn names(&self) -> &IdentityHashMap<NameId, NameRef> {
596
+ &self.names
597
+ }
598
+
599
+ #[must_use]
600
+ pub fn name_dependents(&self) -> &IdentityHashMap<NameId, Vec<NameDependent>> {
601
+ &self.name_dependents
602
+ }
603
+
604
+ /// Drains the accumulated work items, returning them for use by the resolver.
605
+ pub fn take_pending_work(&mut self) -> Vec<Unit> {
606
+ std::mem::take(&mut self.pending_work)
607
+ }
608
+
609
+ fn push_work(&mut self, unit: Unit) {
610
+ self.pending_work.push(unit);
611
+ }
612
+
613
+ pub(crate) fn extend_work(&mut self, units: impl IntoIterator<Item = Unit>) {
614
+ self.pending_work.extend(units);
615
+ }
616
+
617
+ /// Converts a `Resolved` `NameRef` back to `Unresolved`, preserving the original `Name` data.
618
+ /// Returns the `DeclarationId` it was previously resolved to, if any.
619
+ fn unresolve_name(&mut self, name_id: NameId) -> Option<DeclarationId> {
620
+ let name_ref = self.names.get(&name_id)?;
621
+
622
+ match name_ref {
623
+ NameRef::Resolved(resolved) => {
624
+ let declaration_id = *resolved.declaration_id();
625
+ let name = resolved.name().clone();
626
+ self.names.insert(name_id, NameRef::Unresolved(Box::new(name)));
627
+ Some(declaration_id)
628
+ }
629
+ NameRef::Unresolved(_) => None,
630
+ }
631
+ }
632
+
633
+ /// Unresolves a constant reference: removes it from the target declaration's reference set
634
+ /// and unresolves its underlying name.
635
+ fn unresolve_reference(&mut self, reference_id: ConstantReferenceId) -> Option<DeclarationId> {
636
+ let constant_ref = self.constant_references.get(&reference_id)?;
637
+ let name_id = *constant_ref.name_id();
638
+
639
+ if let Some(old_decl_id) = self.unresolve_name(name_id) {
640
+ self.declarations
641
+ .get_mut(&old_decl_id)
642
+ .expect("Tried to unresolve reference for declaration that doesn't exist in the graph")
643
+ .remove_constant_reference(&reference_id);
644
+
645
+ Some(old_decl_id)
646
+ } else {
647
+ None
648
+ }
649
+ }
650
+
651
+ /// Removes a name from the graph and cleans up its name-to-name edges from parent names.
652
+ fn remove_name(&mut self, name_id: NameId) {
653
+ if let Some(name_ref) = self.names.get(&name_id) {
654
+ let parent_scope = name_ref.parent_scope().as_ref().copied();
655
+ let nesting = name_ref.nesting().as_ref().copied();
656
+
657
+ if let Some(ps_id) = parent_scope {
658
+ self.remove_name_dependent(ps_id, NameDependent::ChildName(name_id));
659
+ }
660
+ if let Some(nesting_id) = nesting {
661
+ self.remove_name_dependent(nesting_id, NameDependent::NestedName(name_id));
662
+ }
663
+ }
664
+ self.name_dependents.remove(&name_id);
665
+ self.names.remove(&name_id);
666
+ }
667
+
668
+ /// Removes a specific dependent from the `name_dependents` entry for `name_id`,
669
+ /// cleaning up the entry if no dependents remain.
670
+ fn remove_name_dependent(&mut self, name_id: NameId, dependent: NameDependent) {
671
+ if let Some(deps) = self.name_dependents.get_mut(&name_id) {
672
+ deps.retain(|d| *d != dependent);
673
+ if deps.is_empty() {
674
+ self.name_dependents.remove(&name_id);
675
+ }
676
+ }
677
+ }
678
+
679
+ /// Decrements the ref count for a name and removes it if the count reaches zero.
680
+ ///
681
+ /// This does not recursively untrack `parent_scope` or `nesting` names.
682
+ pub fn untrack_name(&mut self, name_id: NameId) {
683
+ if let Some(name_ref) = self.names.get_mut(&name_id) {
684
+ let string_id = *name_ref.str();
685
+ if !name_ref.decrement_ref_count() {
686
+ self.remove_name(name_id);
687
+ }
688
+ self.untrack_string(string_id);
689
+ }
690
+ }
691
+
692
+ fn untrack_string(&mut self, string_id: StringId) {
693
+ if let Some(string_ref) = self.strings.get_mut(&string_id)
694
+ && !string_ref.decrement_ref_count()
695
+ {
696
+ self.strings.remove(&string_id);
697
+ }
698
+ }
699
+
700
+ fn untrack_definition_strings(&mut self, definition: &Definition) {
701
+ match definition {
702
+ Definition::Class(_)
703
+ | Definition::SingletonClass(_)
704
+ | Definition::Module(_)
705
+ | Definition::Constant(_)
706
+ | Definition::ConstantAlias(_)
707
+ | Definition::ConstantVisibility(_) => {}
708
+ Definition::MethodVisibility(d) => self.untrack_string(*d.str_id()),
709
+ Definition::Method(d) => self.untrack_string(*d.str_id()),
710
+ Definition::AttrAccessor(d) => self.untrack_string(*d.str_id()),
711
+ Definition::AttrReader(d) => self.untrack_string(*d.str_id()),
712
+ Definition::AttrWriter(d) => self.untrack_string(*d.str_id()),
713
+ Definition::GlobalVariable(d) => self.untrack_string(*d.str_id()),
714
+ Definition::InstanceVariable(d) => self.untrack_string(*d.str_id()),
715
+ Definition::ClassVariable(d) => self.untrack_string(*d.str_id()),
716
+ Definition::MethodAlias(d) => {
717
+ self.untrack_string(*d.new_name_str_id());
718
+ self.untrack_string(*d.old_name_str_id());
719
+ }
720
+ Definition::GlobalVariableAlias(d) => {
721
+ self.untrack_string(*d.new_name_str_id());
722
+ self.untrack_string(*d.old_name_str_id());
723
+ }
724
+ }
725
+ }
726
+
727
+ /// Decrements the ref count for a name and removes it if the count reaches zero.
728
+ ///
729
+ /// This recursively untracks `parent_scope` and `nesting` names.
730
+ pub fn untrack_name_recursive(&mut self, name_id: NameId) {
731
+ let Some(name_ref) = self.names.get(&name_id) else {
732
+ return;
733
+ };
734
+
735
+ let parent_scope = name_ref.parent_scope();
736
+ let nesting = *name_ref.nesting();
737
+
738
+ if let ParentScope::Some(parent_scope_id) = parent_scope {
739
+ self.untrack_name_recursive(*parent_scope_id);
740
+ }
741
+
742
+ if let Some(nesting_id) = nesting {
743
+ self.untrack_name_recursive(nesting_id);
744
+ }
745
+
746
+ self.untrack_name(name_id);
747
+ }
748
+
749
+ /// Register a member relationship from a declaration to another declaration through its unqualified name id. For example, in
750
+ ///
751
+ /// ```ruby
752
+ /// module Foo
753
+ /// class Bar; end
754
+ /// def baz; end
755
+ /// end
756
+ /// ```
757
+ ///
758
+ /// `Foo` has two members:
759
+ /// ```ruby
760
+ /// {
761
+ /// NameId(Bar) => DeclarationId(Bar)
762
+ /// NameId(baz) => DeclarationId(baz)
763
+ /// }
764
+ /// ```
765
+ ///
766
+ /// # Panics
767
+ ///
768
+ /// Will panic if the declaration ID passed doesn't belong to a namespace declaration
769
+ pub fn add_member(
770
+ &mut self,
771
+ owner_id: &DeclarationId,
772
+ member_declaration_id: DeclarationId,
773
+ member_str_id: StringId,
774
+ ) {
775
+ if let Some(declaration) = self.declarations.get_mut(owner_id) {
776
+ match declaration {
777
+ Declaration::Namespace(Namespace::Class(it)) => it.add_member(member_str_id, member_declaration_id),
778
+ Declaration::Namespace(Namespace::Module(it)) => it.add_member(member_str_id, member_declaration_id),
779
+ Declaration::Namespace(Namespace::SingletonClass(it)) => {
780
+ it.add_member(member_str_id, member_declaration_id);
781
+ }
782
+ Declaration::Namespace(Namespace::Todo(it)) => it.add_member(member_str_id, member_declaration_id),
783
+ Declaration::Constant(_) => {
784
+ // TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
785
+ }
786
+ _ => panic!("Tried to add member to a declaration that isn't a namespace"),
787
+ }
788
+ }
789
+ }
790
+
791
+ /// # Panics
792
+ ///
793
+ /// This function will panic when trying to record a resolve name for a name ID that does not exist
794
+ pub fn record_resolved_name(&mut self, name_id: NameId, declaration_id: DeclarationId) {
795
+ match self.names.entry(name_id) {
796
+ Entry::Occupied(entry) => match entry.get() {
797
+ NameRef::Unresolved(_) => {
798
+ if let NameRef::Unresolved(unresolved) = entry.remove() {
799
+ let resolved_name = NameRef::Resolved(Box::new(ResolvedName::new(*unresolved, declaration_id)));
800
+ self.names.insert(name_id, resolved_name);
801
+ }
802
+ }
803
+ NameRef::Resolved(_) => {
804
+ // TODO: consider if this is a valid scenario with the resolution phase design. Either collect
805
+ // metrics here or panic if it's never supposed to occur
806
+ }
807
+ },
808
+ Entry::Vacant(_) => panic!("Trying to record resolved name for a name ID that does not exist"),
809
+ }
810
+ }
811
+
812
+ /// # Panics
813
+ ///
814
+ /// Will panic if invoked for a non existing declaration
815
+ pub fn record_resolved_reference(&mut self, reference_id: ConstantReferenceId, declaration_id: DeclarationId) {
816
+ self.declarations
817
+ .get_mut(&declaration_id)
818
+ .expect("Tried to record a constant reference for a declaration that doesn't exist")
819
+ .add_constant_reference(reference_id);
820
+ }
821
+
822
+ /// Handles the deletion of a document identified by `uri`.
823
+ /// Returns the `UriId` of the removed document, or `None` if it didn't exist.
824
+ ///
825
+ /// Runs incremental invalidation to cascade changes through the graph and
826
+ /// accumulates pending work items for the resolver to process.
827
+ pub fn delete_document(&mut self, uri: &str) -> Option<UriId> {
828
+ let uri_id = UriId::from(uri);
829
+ let document = self.documents.remove(&uri_id)?;
830
+ self.invalidate(Some(&document), None);
831
+ self.remove_document_data(&document);
832
+ Some(uri_id)
833
+ }
834
+
835
+ /// Merges everything in `other` into this Graph. This method is meant to merge all graph representations from
836
+ /// different threads, but not meant to handle updates to the existing global representation
837
+ pub fn extend(&mut self, local_graph: LocalGraph) {
838
+ let (uri_id, document, definitions, strings, names, constant_references, method_references, name_dependents) =
839
+ local_graph.into_parts();
840
+
841
+ if self.documents.insert(uri_id, document).is_some() {
842
+ debug_assert!(false, "UriId collision in global graph");
843
+ }
844
+
845
+ for (string_id, string_ref) in strings {
846
+ match self.strings.entry(string_id) {
847
+ Entry::Occupied(mut entry) => {
848
+ debug_assert!(*string_ref == **entry.get(), "StringId collision in global graph");
849
+ entry.get_mut().increment_ref_count(string_ref.ref_count());
850
+ }
851
+ Entry::Vacant(entry) => {
852
+ entry.insert(string_ref);
853
+ }
854
+ }
855
+ }
856
+
857
+ for (name_id, name_ref) in names {
858
+ match self.names.entry(name_id) {
859
+ Entry::Occupied(mut entry) => {
860
+ debug_assert!(*entry.get() == name_ref, "NameId collision in global graph");
861
+ entry.get_mut().increment_ref_count(name_ref.ref_count());
862
+ }
863
+ Entry::Vacant(entry) => {
864
+ entry.insert(name_ref);
865
+ }
866
+ }
867
+ }
868
+
869
+ for (definition_id, definition) in definitions {
870
+ if self.definitions.insert(definition_id, definition).is_some() {
871
+ debug_assert!(false, "DefinitionId collision in global graph");
872
+ }
873
+
874
+ self.push_work(Unit::Definition(definition_id));
875
+ }
876
+
877
+ for (constant_ref_id, constant_ref) in constant_references {
878
+ self.push_work(Unit::ConstantRef(constant_ref_id));
879
+
880
+ if self.constant_references.insert(constant_ref_id, constant_ref).is_some() {
881
+ debug_assert!(false, "Constant ReferenceId collision in global graph");
882
+ }
883
+ }
884
+
885
+ for (method_ref_id, method_ref) in method_references {
886
+ if self.method_references.insert(method_ref_id, method_ref).is_some() {
887
+ debug_assert!(false, "Method ReferenceId collision in global graph");
888
+ }
889
+ }
890
+
891
+ for (name_id, deps) in name_dependents {
892
+ let global_deps = self.name_dependents.entry(name_id).or_default();
893
+ for dep in deps {
894
+ if !global_deps.contains(&dep) {
895
+ global_deps.push(dep);
896
+ }
897
+ }
898
+ }
899
+ }
900
+
901
+ /// Updates the global representation with the information contained in `other`, handling deletions, insertions and
902
+ /// updates to existing entries.
903
+ ///
904
+ /// Runs incremental invalidation to cascade changes through the graph and
905
+ /// accumulates pending work items for the resolver to process.
906
+ ///
907
+ /// The three steps must run in this order:
908
+ /// 1. `invalidate` -- reads resolved names and declaration state to determine what to invalidate
909
+ /// 2. `remove_document_data` -- removes old refs/defs/names/strings from maps
910
+ /// 3. `extend` -- merges the new `LocalGraph` into the now-clean graph
911
+ pub fn consume_document_changes(&mut self, other: LocalGraph) {
912
+ let uri_id = other.uri_id();
913
+ let old_document = self.documents.remove(&uri_id);
914
+
915
+ // Skip invalidation during boot indexing (no documents have been resolved yet)
916
+ // or when the document is brand new (no old data to invalidate against).
917
+ if old_document.is_some() || !self.documents.is_empty() {
918
+ self.invalidate(old_document.as_ref(), Some(&other));
919
+ if let Some(doc) = &old_document {
920
+ self.remove_document_data(doc);
921
+ }
922
+ }
923
+
924
+ self.extend(other);
925
+ }
926
+
927
+ /// Identifies declarations affected by old/new documents and feeds them into `invalidate_graph`.
928
+ ///
929
+ /// Does NOT mutate declarations or remove raw data — definition detachment is deferred to
930
+ /// `invalidate_declaration`, and raw data cleanup to `remove_document_data`.
931
+ fn invalidate(&mut self, old_document: Option<&Document>, new_local_graph: Option<&LocalGraph>) {
932
+ let capacity = old_document.map_or(0, |d| d.definitions().len())
933
+ + new_local_graph.map_or(0, |lg| lg.definitions().len() + lg.constant_references().len());
934
+ let mut items: Vec<InvalidationItem> = Vec::with_capacity(capacity);
935
+ let mut pending_detachments: IdentityHashMap<DeclarationId, Vec<DefinitionId>> = IdentityHashMap::default();
936
+
937
+ // Identify declarations affected by removed definitions
938
+ if let Some(document) = old_document {
939
+ for def_id in document.definitions() {
940
+ if let Some(declaration_id) = self.definition_id_to_declaration_id(*def_id).copied() {
941
+ pending_detachments.entry(declaration_id).or_default().push(*def_id);
942
+ }
943
+ }
944
+ for decl_id in pending_detachments.keys() {
945
+ items.push(InvalidationItem::Declaration(*decl_id));
946
+ }
947
+ }
948
+
949
+ // Declarations touched by the new local graph
950
+ if let Some(lg) = new_local_graph {
951
+ for def in lg.definitions().values() {
952
+ if let Some(name_id) = def.name_id()
953
+ && let Some(NameRef::Resolved(resolved)) = self.names.get(name_id)
954
+ {
955
+ items.push(InvalidationItem::Declaration(*resolved.declaration_id()));
956
+ }
957
+ }
958
+
959
+ // Constant references include `include`/`prepend`/`extend` targets.
960
+ // A new mixin changes the nesting declaration's ancestor chain, so we
961
+ // invalidate the nesting declaration.
962
+ // We can optimize this later by checking where the constant reference is used.
963
+ for const_ref in lg.constant_references().values() {
964
+ // The name may not exist in the global graph yet — it's in the local graph
965
+ // which hasn't been extended yet. Only act on names already known globally.
966
+ if let Some(name_ref) = self.names.get(const_ref.name_id())
967
+ && let Some(nesting_id) = name_ref.nesting()
968
+ && let Some(NameRef::Resolved(resolved)) = self.names.get(nesting_id)
969
+ {
970
+ items.push(InvalidationItem::Declaration(*resolved.declaration_id()));
971
+ }
972
+ }
973
+ }
974
+
975
+ if !items.is_empty() {
976
+ self.invalidate_graph(items, pending_detachments);
977
+ }
978
+ }
979
+
980
+ /// Removes raw document data (refs, defs, names, strings) from maps.
981
+ /// Does not touch declarations or perform invalidation -- that is handled by `invalidate`.
982
+ fn remove_document_data(&mut self, document: &Document) {
983
+ for ref_id in document.method_references() {
984
+ if let Some(method_ref) = self.method_references.remove(ref_id) {
985
+ self.untrack_string(*method_ref.str());
986
+ }
987
+ }
988
+
989
+ for ref_id in document.constant_references() {
990
+ if let Some(constant_ref) = self.constant_references.remove(ref_id) {
991
+ // Detach from target declaration. References unresolved during invalidation
992
+ // were already detached; this catches the rest.
993
+ if let NameRef::Resolved(resolved) = self.names.get(constant_ref.name_id()).unwrap()
994
+ && let Some(declaration) = self.declarations.get_mut(resolved.declaration_id())
995
+ {
996
+ declaration.remove_constant_reference(ref_id);
997
+ }
998
+
999
+ self.remove_name_dependent(*constant_ref.name_id(), NameDependent::Reference(*ref_id));
1000
+ self.untrack_name(*constant_ref.name_id());
1001
+ }
1002
+ }
1003
+
1004
+ // Detach removed definitions from their declarations.
1005
+ // Most definitions were already detached by invalidate_declaration via
1006
+ // pending_detachments. Definitions not handled by pending_detachments are
1007
+ // those where definition_to_declaration_id returns None, for example:
1008
+ // - methods inside `class << self` when <Foo> was unresolved by a prior deletion
1009
+ // - instance variables in class body (owned by singleton, but lookup resolves to class)
1010
+ // - definitions whose enclosing namespace name chain is broken
1011
+ // Detach those by scanning declarations for the remainder.
1012
+ let missed_def_ids: Vec<DefinitionId> = document
1013
+ .definitions()
1014
+ .iter()
1015
+ .copied()
1016
+ .filter(|def_id| self.definition_id_to_declaration_id(*def_id).is_none())
1017
+ .collect();
1018
+
1019
+ if !missed_def_ids.is_empty() {
1020
+ for declaration in self.declarations.values_mut() {
1021
+ for def_id in &missed_def_ids {
1022
+ declaration.remove_definition(def_id);
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ for def_id in document.definitions() {
1028
+ let definition = self.definitions.remove(def_id).unwrap();
1029
+
1030
+ if let Some(name_id) = definition.name_id() {
1031
+ self.remove_name_dependent(*name_id, NameDependent::Definition(*def_id));
1032
+ self.untrack_name(*name_id);
1033
+ }
1034
+ self.untrack_definition_strings(&definition);
1035
+ }
1036
+ }
1037
+
1038
+ /// Unified invalidation worklist. Processes declaration and name items in a single loop,
1039
+ /// where processing one item can push new items back onto the queue.
1040
+ fn invalidate_graph(
1041
+ &mut self,
1042
+ items: Vec<InvalidationItem>,
1043
+ mut pending_detachments: IdentityHashMap<DeclarationId, Vec<DefinitionId>>,
1044
+ ) {
1045
+ let mut queue = items;
1046
+ let mut visited_declarations = IdentityHashSet::<DeclarationId>::default();
1047
+
1048
+ while let Some(item) = queue.pop() {
1049
+ match item {
1050
+ InvalidationItem::Declaration(decl_id) => {
1051
+ let detach = pending_detachments.remove(&decl_id).unwrap_or_default();
1052
+ self.invalidate_declaration(decl_id, &detach, &mut queue, &mut visited_declarations);
1053
+ }
1054
+ InvalidationItem::Name(name_id) => {
1055
+ self.unresolve_dependent_name(name_id, &mut queue);
1056
+ }
1057
+ InvalidationItem::References(name_id) => {
1058
+ self.unresolve_dependent_references(name_id, &mut queue);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ /// Processes a declaration in the invalidation worklist.
1065
+ ///
1066
+ /// Detaches any pending definitions first, then either:
1067
+ ///
1068
+ /// - **Remove**: no definitions remain or owner was already removed (orphaned).
1069
+ /// Removes the declaration, unresolves its names, and cascades to members,
1070
+ /// singleton class, and descendants.
1071
+ ///
1072
+ /// When an orphaned declaration still has definitions, those are re-queued for
1073
+ /// re-resolution. For example, given `class Foo::Bar`, if `Foo` is changed from
1074
+ /// `module Foo` to `Foo = Baz`, we can still recreate `Baz::Bar` from the
1075
+ /// existing definitions of it.
1076
+ ///
1077
+ /// - **Update**: declaration survives but its ancestor chain may have changed
1078
+ /// (e.g. mixin added/removed, superclass changed, or an ancestor was removed).
1079
+ /// Clears ancestors and descendants, then re-queues ancestor resolution.
1080
+ /// Also enters this path when a new definition targets an existing declaration
1081
+ /// without changing ancestors (e.g. adding a method in a new file). In that case
1082
+ /// the ancestor re-resolution is redundant — a future optimization could skip it
1083
+ /// by tracking why the declaration was seeded.
1084
+ fn invalidate_declaration(
1085
+ &mut self,
1086
+ decl_id: DeclarationId,
1087
+ detach_def_ids: &[DefinitionId],
1088
+ queue: &mut Vec<InvalidationItem>,
1089
+ visited_declarations: &mut IdentityHashSet<DeclarationId>,
1090
+ ) {
1091
+ // Collect names before detaching — after detachment, definitions() may be empty
1092
+ let seed_names = self.names_for_declaration(decl_id);
1093
+
1094
+ // Detach pending definitions before deciding the mode
1095
+ if let Some(decl) = self.declarations.get_mut(&decl_id) {
1096
+ for def_id in detach_def_ids {
1097
+ decl.remove_definition(def_id);
1098
+ }
1099
+ if !detach_def_ids.is_empty() {
1100
+ decl.clear_diagnostics();
1101
+ }
1102
+ }
1103
+
1104
+ let Some(decl) = self.declarations.get(&decl_id) else {
1105
+ return;
1106
+ };
1107
+ let should_remove = decl.has_no_definitions() || !self.declarations.contains_key(decl.owner_id());
1108
+
1109
+ if should_remove {
1110
+ // Queue members + singleton for removal
1111
+ if let Some(ns) = decl.as_namespace() {
1112
+ if let Some(singleton_id) = ns.singleton_class() {
1113
+ queue.push(InvalidationItem::Declaration(*singleton_id));
1114
+ }
1115
+ for member_decl_id in ns.members().values() {
1116
+ queue.push(InvalidationItem::Declaration(*member_decl_id));
1117
+ }
1118
+ for descendant_id in ns.descendants() {
1119
+ queue.push(InvalidationItem::Declaration(*descendant_id));
1120
+ }
1121
+ }
1122
+
1123
+ // Unresolve names and cascade. Reference dependents from surviving
1124
+ // files must be re-queued — their resolution path through this
1125
+ // declaration is broken and needs to be retried after re-add.
1126
+ for name_id in seed_names {
1127
+ self.unresolve_name(name_id);
1128
+ self.queue_structural_cascade(name_id, queue);
1129
+
1130
+ if let Some(deps) = self.name_dependents.get(&name_id) {
1131
+ for dep in deps {
1132
+ if let NameDependent::Reference(ref_id) = dep {
1133
+ self.pending_work.push(Unit::ConstantRef(*ref_id));
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ // Clean up owner membership and queue remaining definitions for re-resolution
1140
+ if let Some(decl) = self.declarations.get(&decl_id) {
1141
+ let def_ids: Vec<DefinitionId> = decl.definitions().to_vec();
1142
+ let unqualified_str_id = StringId::from(&decl.unqualified_name());
1143
+ let owner_id = *decl.owner_id();
1144
+
1145
+ for def_id in def_ids {
1146
+ self.push_work(Unit::Definition(def_id));
1147
+ }
1148
+
1149
+ if let Some(owner) = self.declarations.get_mut(&owner_id)
1150
+ && let Some(ns) = owner.as_namespace_mut()
1151
+ {
1152
+ ns.remove_member(&unqualified_str_id);
1153
+ }
1154
+ }
1155
+
1156
+ self.declarations.remove(&decl_id);
1157
+ } else {
1158
+ // Update: the declaration still has definitions so it stays in the graph,
1159
+ // but its ancestor chain may have changed (e.g. a mixin was added/removed).
1160
+ // Clear ancestors and descendants, then re-queue ancestor resolution.
1161
+ if !visited_declarations.insert(decl_id) {
1162
+ return;
1163
+ }
1164
+
1165
+ let Some(namespace) = self.declarations.get_mut(&decl_id).and_then(|d| d.as_namespace_mut()) else {
1166
+ return;
1167
+ };
1168
+
1169
+ // Remove self from each ancestor's descendant set
1170
+ for ancestor in &namespace.clone_ancestors() {
1171
+ if let Ancestor::Complete(ancestor_id) = ancestor
1172
+ && let Some(anc_decl) = self.declarations.get_mut(ancestor_id)
1173
+ && let Some(ns) = anc_decl.as_namespace_mut()
1174
+ {
1175
+ ns.remove_descendant(&decl_id);
1176
+ }
1177
+ }
1178
+
1179
+ let namespace = self.declarations.get_mut(&decl_id).unwrap().as_namespace_mut().unwrap();
1180
+
1181
+ namespace.for_each_descendant(|descendant_id| {
1182
+ queue.push(InvalidationItem::Declaration(*descendant_id));
1183
+ });
1184
+
1185
+ namespace.clear_ancestors();
1186
+ namespace.clear_descendants();
1187
+
1188
+ self.push_work(Unit::Ancestors(decl_id));
1189
+
1190
+ for seed_name_id in seed_names {
1191
+ self.queue_ancestor_triggered_invalidation(seed_name_id, queue);
1192
+ }
1193
+ }
1194
+ }
1195
+
1196
+ /// The name's structural dependency is broken (its nesting or parent scope was removed).
1197
+ /// Unresolves the name and cascades to all dependents — both references and definitions.
1198
+ fn unresolve_dependent_name(&mut self, name_id: NameId, queue: &mut Vec<InvalidationItem>) {
1199
+ let dependents: Vec<NameDependent> = self.name_dependents.get(&name_id).cloned().unwrap_or_default();
1200
+ self.queue_structural_cascade(name_id, queue);
1201
+
1202
+ if let Some(old_decl_id) = self.unresolve_name(name_id) {
1203
+ for dep in &dependents {
1204
+ match dep {
1205
+ NameDependent::Reference(ref_id) => {
1206
+ if let Some(decl) = self.declarations.get_mut(&old_decl_id) {
1207
+ decl.remove_constant_reference(ref_id);
1208
+ }
1209
+ self.push_work(Unit::ConstantRef(*ref_id));
1210
+ }
1211
+ NameDependent::Definition(def_id) => {
1212
+ self.push_work(Unit::Definition(*def_id));
1213
+
1214
+ if let Some(decl) = self.declarations.get_mut(&old_decl_id) {
1215
+ decl.remove_definition(def_id);
1216
+ }
1217
+
1218
+ if self
1219
+ .declarations
1220
+ .get(&old_decl_id)
1221
+ .is_some_and(Declaration::has_no_definitions)
1222
+ {
1223
+ queue.push(InvalidationItem::Declaration(old_decl_id));
1224
+ }
1225
+ }
1226
+ NameDependent::ChildName(_) | NameDependent::NestedName(_) => {}
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ /// Ancestor context changed but the name itself is still valid.
1233
+ /// Unresolves constant references under this name without unresolving the name itself.
1234
+ fn unresolve_dependent_references(&mut self, name_id: NameId, queue: &mut Vec<InvalidationItem>) {
1235
+ let dependents: Vec<NameDependent> = self.name_dependents.get(&name_id).cloned().unwrap_or_default();
1236
+ self.queue_ancestor_triggered_invalidation(name_id, queue);
1237
+
1238
+ let is_resolved = matches!(self.names.get(&name_id), Some(NameRef::Resolved(_)));
1239
+
1240
+ for dep in &dependents {
1241
+ if let NameDependent::Reference(ref_id) = dep {
1242
+ if is_resolved {
1243
+ self.unresolve_reference(*ref_id);
1244
+ }
1245
+ self.push_work(Unit::ConstantRef(*ref_id));
1246
+ }
1247
+ }
1248
+ }
1249
+
1250
+ /// Structural cascade: all dependent names must be unresolved regardless of edge type.
1251
+ /// Both `ChildName` and `NestedName` dependents get `UnresolveName`.
1252
+ fn queue_structural_cascade(&self, name_id: NameId, queue: &mut Vec<InvalidationItem>) {
1253
+ if let Some(deps) = self.name_dependents.get(&name_id) {
1254
+ for dep in deps {
1255
+ match dep {
1256
+ NameDependent::ChildName(id) | NameDependent::NestedName(id) => {
1257
+ queue.push(InvalidationItem::Name(*id));
1258
+ }
1259
+ NameDependent::Reference(_) | NameDependent::Definition(_) => {}
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ /// Ancestor context changed: `ChildName` dependents need full unresolve (structural),
1266
+ /// `NestedName` dependents only need reference re-evaluation.
1267
+ fn queue_ancestor_triggered_invalidation(&self, name_id: NameId, queue: &mut Vec<InvalidationItem>) {
1268
+ if let Some(deps) = self.name_dependents.get(&name_id) {
1269
+ for dep in deps {
1270
+ match dep {
1271
+ NameDependent::ChildName(id) => {
1272
+ queue.push(InvalidationItem::Name(*id));
1273
+ }
1274
+ NameDependent::NestedName(id) => {
1275
+ queue.push(InvalidationItem::References(*id));
1276
+ }
1277
+ NameDependent::Reference(_) | NameDependent::Definition(_) => {}
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ /// Collects all `NameId`s that resolved to the given declaration, by inspecting its
1284
+ /// definitions and references.
1285
+ fn names_for_declaration(&self, decl_id: DeclarationId) -> IdentityHashSet<NameId> {
1286
+ let Some(decl) = self.declarations.get(&decl_id) else {
1287
+ return IdentityHashSet::default();
1288
+ };
1289
+
1290
+ let mut names = IdentityHashSet::default();
1291
+
1292
+ for def_id in decl.definitions() {
1293
+ if let Some(name_id) = self.definitions.get(def_id).and_then(|d| d.name_id())
1294
+ && matches!(self.names.get(name_id), Some(NameRef::Resolved(_)))
1295
+ {
1296
+ names.insert(*name_id);
1297
+ }
1298
+ }
1299
+
1300
+ for ref_id in decl.constant_references().into_iter().flatten() {
1301
+ if let Some(constant_ref) = self.constant_references.get(ref_id) {
1302
+ let name_id = *constant_ref.name_id();
1303
+ if matches!(self.names.get(&name_id), Some(NameRef::Resolved(_))) {
1304
+ names.insert(name_id);
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ names
1310
+ }
1311
+
1312
+ /// Sets the encoding that should be used for transforming byte offsets into LSP code unit line/column positions
1313
+ pub fn set_encoding(&mut self, encoding: Encoding) {
1314
+ self.position_encoding = encoding;
1315
+ }
1316
+
1317
+ #[must_use]
1318
+ pub fn encoding(&self) -> &Encoding {
1319
+ &self.position_encoding
1320
+ }
1321
+
1322
+ #[allow(clippy::cast_precision_loss)]
1323
+ pub fn print_query_statistics(&self) {
1324
+ use std::collections::{HashMap, HashSet};
1325
+
1326
+ let mut declarations_with_docs = 0;
1327
+ let mut total_doc_size = 0;
1328
+ let mut multi_definition_count = 0;
1329
+ let mut declarations_types: HashMap<&str, usize> = HashMap::new();
1330
+ let mut linked_definition_types: HashMap<&str, usize> = HashMap::new();
1331
+ let mut linked_definition_ids: HashSet<&DefinitionId> = HashSet::new();
1332
+
1333
+ for declaration in self.declarations.values() {
1334
+ // Check documentation
1335
+ if let Some(definitions) = self.get(declaration.name()) {
1336
+ let has_docs = definitions.iter().any(|def| !def.comments().is_empty());
1337
+ if has_docs {
1338
+ declarations_with_docs += 1;
1339
+ let doc_size: usize = definitions
1340
+ .iter()
1341
+ .map(|def| def.comments().iter().map(|c| c.string().len()).sum::<usize>())
1342
+ .sum();
1343
+ total_doc_size += doc_size;
1344
+ }
1345
+ }
1346
+
1347
+ *declarations_types.entry(declaration.kind()).or_insert(0) += 1;
1348
+
1349
+ // Count definitions by type
1350
+ let definition_count = declaration.definitions().len();
1351
+ if definition_count > 1 {
1352
+ multi_definition_count += 1;
1353
+ }
1354
+
1355
+ for def_id in declaration.definitions() {
1356
+ linked_definition_ids.insert(def_id);
1357
+ if let Some(def) = self.definitions().get(def_id) {
1358
+ *linked_definition_types.entry(def.kind()).or_insert(0) += 1;
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ // Count ALL definitions by type (including unlinked)
1364
+ let mut all_definition_types: HashMap<&str, usize> = HashMap::new();
1365
+ for def in self.definitions.values() {
1366
+ *all_definition_types.entry(def.kind()).or_insert(0) += 1;
1367
+ }
1368
+
1369
+ println!();
1370
+ println!("Query statistics");
1371
+ let total_declarations = self.declarations.len();
1372
+ println!(" Total declarations: {total_declarations}");
1373
+ println!(
1374
+ " With documentation: {} ({:.1}%)",
1375
+ declarations_with_docs,
1376
+ stats::percentage(declarations_with_docs, total_declarations)
1377
+ );
1378
+ println!(
1379
+ " Without documentation: {} ({:.1}%)",
1380
+ total_declarations - declarations_with_docs,
1381
+ stats::percentage(total_declarations - declarations_with_docs, total_declarations)
1382
+ );
1383
+ println!(" Total documentation size: {total_doc_size} bytes");
1384
+ println!(
1385
+ " Multi-definition names: {} ({:.1}%)",
1386
+ multi_definition_count,
1387
+ stats::percentage(multi_definition_count, total_declarations)
1388
+ );
1389
+
1390
+ println!();
1391
+ println!("Declaration breakdown:");
1392
+ let mut types: Vec<_> = declarations_types.iter().collect();
1393
+ types.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
1394
+ for (kind, count) in types {
1395
+ println!(" {kind:20} {count:6}");
1396
+ }
1397
+
1398
+ // Combined definition breakdown: total, linked, orphan
1399
+ println!();
1400
+ println!("Definition breakdown:");
1401
+ println!(" {:20} {:>8} {:>8} {:>8}", "Type", "Total", "Linked", "Orphan");
1402
+ println!(" {:20} {:>8} {:>8} {:>8}", "----", "-----", "------", "------");
1403
+
1404
+ let mut definition_types: Vec<_> = all_definition_types.iter().collect();
1405
+ definition_types.sort_by_key(|(_, total)| std::cmp::Reverse(**total));
1406
+
1407
+ for (kind, total) in definition_types {
1408
+ let linked = linked_definition_types.get(kind).unwrap_or(&0);
1409
+ let orphan = total.saturating_sub(*linked);
1410
+ println!(" {kind:20} {total:>8} {linked:>8} {orphan:>8}");
1411
+ }
1412
+
1413
+ // Definition linkage summary
1414
+ let total_definitions = self.definitions.len();
1415
+ let linked_count = linked_definition_ids.len();
1416
+ let unlinked_count = total_definitions - linked_count;
1417
+ println!(" {:20} {:>8} {:>8} {:>8}", "----", "-----", "------", "------");
1418
+ println!(
1419
+ " {:20} {:>8} {:>8} {:>8}",
1420
+ "TOTAL", total_definitions, linked_count, unlinked_count
1421
+ );
1422
+ println!(
1423
+ " Orphan rate: {:.1}%",
1424
+ stats::percentage(unlinked_count, total_definitions)
1425
+ );
1426
+ }
1427
+ }
1428
+
1429
+ #[cfg(test)]
1430
+ mod tests {
1431
+ use super::*;
1432
+ use crate::model::comment::Comment;
1433
+ use crate::model::declaration::Ancestors;
1434
+ use crate::test_utils::GraphTest;
1435
+ use crate::{
1436
+ assert_declaration_does_not_exist, assert_dependents, assert_descendants, assert_members_eq,
1437
+ assert_no_diagnostics, assert_no_members,
1438
+ };
1439
+
1440
+ #[test]
1441
+ fn deleting_a_uri() {
1442
+ let mut context = GraphTest::new();
1443
+
1444
+ context.index_uri("file:///foo.rb", "module Foo; end");
1445
+ context.delete_uri("file:///foo.rb");
1446
+ context.resolve();
1447
+
1448
+ assert!(!context.graph().documents.contains_key(&UriId::from("file:///foo.rb")));
1449
+ assert_declaration_does_not_exist!(context, "Foo");
1450
+ assert!(
1451
+ context
1452
+ .graph()
1453
+ .declarations()
1454
+ .get(&DeclarationId::from("Foo"))
1455
+ .is_none()
1456
+ );
1457
+ }
1458
+
1459
+ #[test]
1460
+ fn deleting_file_triggers_name_dependent_cleanup() {
1461
+ let mut context = GraphTest::new();
1462
+
1463
+ context.index_uri(
1464
+ "file:///foo.rb",
1465
+ "
1466
+ module Foo
1467
+ CONST
1468
+ end
1469
+ ",
1470
+ );
1471
+ context.index_uri(
1472
+ "file:///bar.rb",
1473
+ "
1474
+ module Foo
1475
+ class Bar; end
1476
+ end
1477
+ ",
1478
+ );
1479
+ context.resolve();
1480
+
1481
+ assert_dependents!(
1482
+ &context,
1483
+ "Foo",
1484
+ [
1485
+ Definition("Foo"),
1486
+ Definition("Foo"),
1487
+ NestedName("Bar"),
1488
+ NestedName("CONST"),
1489
+ ]
1490
+ );
1491
+
1492
+ // Deleting bar.rb removes Bar's name (and its NestedName edge from Foo)
1493
+ // and one Definition dependent (bar.rb's `module Foo` definition).
1494
+ context.delete_uri("file:///bar.rb");
1495
+ assert_dependents!(&context, "Foo", [Definition("Foo"), NestedName("CONST")]);
1496
+
1497
+ // Deleting foo.rb cleans up everything
1498
+ context.delete_uri("file:///foo.rb");
1499
+ let foo_ids = context
1500
+ .graph()
1501
+ .names()
1502
+ .iter()
1503
+ .filter(|(_, n)| *n.str() == StringId::from("Foo"))
1504
+ .count();
1505
+ assert_eq!(foo_ids, 0, "Foo name should be removed after deleting both files");
1506
+ }
1507
+
1508
+ #[test]
1509
+ fn updating_index_with_deleted_definitions() {
1510
+ let mut context = GraphTest::new();
1511
+
1512
+ context.index_uri("file:///foo.rb", "module Foo; end");
1513
+
1514
+ let original_definition_length = context.graph().definitions.len();
1515
+ let original_document_length = context.graph().documents.len();
1516
+
1517
+ // Update with empty content to remove definitions but keep the URI
1518
+ context.index_uri("file:///foo.rb", "");
1519
+
1520
+ // URI remains if the file was not deleted, but definitions got erased
1521
+ assert_eq!(original_definition_length - 1, context.graph().definitions.len());
1522
+ assert_eq!(original_document_length, context.graph().documents.len());
1523
+ }
1524
+
1525
+ #[test]
1526
+ fn updating_index_with_deleted_definitions_after_resolution() {
1527
+ let mut context = GraphTest::new();
1528
+
1529
+ context.index_uri("file:///foo.rb", "module Foo; end");
1530
+ context.resolve();
1531
+
1532
+ let original_definition_length = context.graph().definitions.len();
1533
+ let original_document_length = context.graph().documents.len();
1534
+
1535
+ assert!(
1536
+ context
1537
+ .graph()
1538
+ .declarations()
1539
+ .get(&DeclarationId::from("Foo"))
1540
+ .is_some()
1541
+ );
1542
+
1543
+ // Update with empty content to remove definitions but keep the URI
1544
+ context.index_uri("file:///foo.rb", "");
1545
+
1546
+ // URI remains if the file was not deleted, but definitions and declarations got erased
1547
+ assert_eq!(original_definition_length - 1, context.graph().definitions.len());
1548
+ assert_eq!(original_document_length, context.graph().documents.len());
1549
+
1550
+ assert!(
1551
+ context
1552
+ .graph()
1553
+ .declarations()
1554
+ .get(&DeclarationId::from("Foo"))
1555
+ .is_none()
1556
+ );
1557
+ }
1558
+
1559
+ #[test]
1560
+ fn updating_index_with_deleted_references() {
1561
+ let mut context = GraphTest::new();
1562
+
1563
+ context.index_uri("file:///definition.rb", "module Foo; end");
1564
+ context.index_uri(
1565
+ "file:///references.rb",
1566
+ r"
1567
+ Foo
1568
+ bar
1569
+ BAZ
1570
+ ",
1571
+ );
1572
+ context.resolve();
1573
+
1574
+ assert_eq!(context.graph().documents.len(), 3);
1575
+ assert_eq!(context.graph().method_references.len(), 1);
1576
+ assert_eq!(context.graph().constant_references.len(), 6);
1577
+ {
1578
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1579
+ assert_eq!(declaration.as_namespace().unwrap().references().len(), 1);
1580
+ }
1581
+
1582
+ // Update with empty content to remove definitions but keep the URI
1583
+ context.index_uri("file:///references.rb", "");
1584
+
1585
+ // URI remains if the file was not deleted, but references got erased
1586
+ assert_eq!(context.graph().documents.len(), 3);
1587
+ assert!(context.graph().method_references.is_empty());
1588
+ assert_eq!(context.graph().constant_references.len(), 4);
1589
+ {
1590
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1591
+ assert!(declaration.as_namespace().unwrap().references().is_empty());
1592
+ }
1593
+ }
1594
+
1595
+ #[test]
1596
+ fn invalidating_ancestor_chains_when_document_changes() {
1597
+ let mut context = GraphTest::new();
1598
+
1599
+ context.index_uri("file:///a.rb", "class Foo; include Bar; def method_name; end; end");
1600
+ context.index_uri("file:///b.rb", "class Foo; end");
1601
+ context.index_uri("file:///c.rb", "module Bar; end");
1602
+ context.index_uri("file:///d.rb", "class Baz < Foo; end");
1603
+ context.resolve();
1604
+
1605
+ let foo_declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1606
+ assert!(matches!(
1607
+ foo_declaration.as_namespace().unwrap().ancestors(),
1608
+ Ancestors::Complete(_)
1609
+ ));
1610
+
1611
+ let baz_declaration = context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap();
1612
+ assert!(matches!(
1613
+ baz_declaration.as_namespace().unwrap().ancestors(),
1614
+ Ancestors::Complete(_)
1615
+ ));
1616
+
1617
+ {
1618
+ let Declaration::Namespace(Namespace::Module(_bar)) =
1619
+ context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
1620
+ else {
1621
+ panic!("Expected Bar to be a module");
1622
+ };
1623
+ assert_descendants!(context, "Bar", ["Foo"]);
1624
+ }
1625
+ assert_descendants!(context, "Foo", ["Baz"]);
1626
+
1627
+ context.index_uri("file:///a.rb", "");
1628
+
1629
+ {
1630
+ let Declaration::Namespace(Namespace::Class(foo)) =
1631
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
1632
+ else {
1633
+ panic!("Expected Foo to be a class");
1634
+ };
1635
+ assert!(matches!(foo.ancestors(), Ancestors::Partial(a) if a.is_empty()));
1636
+ assert!(foo.descendants().is_empty());
1637
+
1638
+ let Declaration::Namespace(Namespace::Class(baz)) =
1639
+ context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap()
1640
+ else {
1641
+ panic!("Expected Baz to be a class");
1642
+ };
1643
+ assert!(matches!(baz.ancestors(), Ancestors::Partial(a) if a.is_empty()));
1644
+ assert!(baz.descendants().is_empty());
1645
+
1646
+ let Declaration::Namespace(Namespace::Module(bar)) =
1647
+ context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
1648
+ else {
1649
+ panic!("Expected Bar to be a module");
1650
+ };
1651
+ assert!(!bar.descendants().contains(&DeclarationId::from("Foo")));
1652
+ }
1653
+
1654
+ context.resolve();
1655
+
1656
+ let baz_declaration = context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap();
1657
+ assert!(matches!(
1658
+ baz_declaration.as_namespace().unwrap().clone_ancestors(),
1659
+ Ancestors::Complete(_)
1660
+ ));
1661
+
1662
+ assert_descendants!(context, "Foo", ["Baz"]);
1663
+ }
1664
+
1665
+ #[test]
1666
+ fn name_count_increments_for_duplicates() {
1667
+ let mut context = GraphTest::new();
1668
+
1669
+ context.index_uri("file:///foo.rb", "module Foo; end");
1670
+ context.index_uri("file:///foo2.rb", "module Foo; end");
1671
+ context.index_uri("file:///foo3.rb", "Foo");
1672
+ context.resolve();
1673
+
1674
+ assert_eq!(context.graph().names().len(), 7);
1675
+ let foo_str_id = StringId::from("Foo");
1676
+ let name_ref = context
1677
+ .graph()
1678
+ .names()
1679
+ .values()
1680
+ .find(|n| *n.str() == foo_str_id)
1681
+ .unwrap();
1682
+ assert_eq!(name_ref.ref_count(), 3);
1683
+ }
1684
+
1685
+ #[test]
1686
+ fn string_ref_count_increments_for_duplicate_definitions() {
1687
+ let mut context = GraphTest::new();
1688
+
1689
+ context.index_uri(
1690
+ "file:///foo.rb",
1691
+ "
1692
+ def method_name; end
1693
+ attr_accessor :accessor_name
1694
+ attr_reader :reader_name
1695
+ attr_writer :writer_name
1696
+ $global_var = 1
1697
+ @@class_var = 1
1698
+ class Foo
1699
+ def initialize
1700
+ @instance_var = 1
1701
+ end
1702
+ end
1703
+ def old_method; end
1704
+ alias_method :new_method, :old_method
1705
+ $old_global = 1
1706
+ alias $new_global $old_global
1707
+ ",
1708
+ );
1709
+
1710
+ context.resolve();
1711
+
1712
+ let strings = context.graph().strings();
1713
+ assert_eq!(strings.get(&StringId::from("method_name()")).unwrap().ref_count(), 1);
1714
+ assert_eq!(strings.get(&StringId::from("accessor_name()")).unwrap().ref_count(), 1);
1715
+ assert_eq!(strings.get(&StringId::from("reader_name()")).unwrap().ref_count(), 1);
1716
+ assert_eq!(strings.get(&StringId::from("writer_name()")).unwrap().ref_count(), 1);
1717
+ assert_eq!(strings.get(&StringId::from("$global_var")).unwrap().ref_count(), 1);
1718
+ assert_eq!(strings.get(&StringId::from("@@class_var")).unwrap().ref_count(), 1);
1719
+ assert_eq!(strings.get(&StringId::from("@instance_var")).unwrap().ref_count(), 1);
1720
+ assert_eq!(strings.get(&StringId::from("old_method()")).unwrap().ref_count(), 2);
1721
+ assert_eq!(strings.get(&StringId::from("new_method()")).unwrap().ref_count(), 1);
1722
+ assert_eq!(strings.get(&StringId::from("$old_global")).unwrap().ref_count(), 2);
1723
+ assert_eq!(strings.get(&StringId::from("$new_global")).unwrap().ref_count(), 1);
1724
+ }
1725
+
1726
+ #[test]
1727
+ fn updating_index_with_deleted_names() {
1728
+ let mut context = GraphTest::new();
1729
+
1730
+ context.index_uri("file:///foo.rb", "module Foo; end");
1731
+ context.index_uri("file:///bar.rb", "Foo");
1732
+ context.resolve();
1733
+
1734
+ assert_eq!(context.graph().names().len(), 7);
1735
+ let foo_str_id = StringId::from("Foo");
1736
+ let foo_name = context
1737
+ .graph()
1738
+ .names()
1739
+ .values()
1740
+ .find(|n| *n.str() == foo_str_id)
1741
+ .unwrap();
1742
+ assert_eq!(foo_name.ref_count(), 2);
1743
+
1744
+ context.delete_uri("file:///foo.rb");
1745
+ assert_eq!(context.graph().names().len(), 7);
1746
+ let foo_name = context
1747
+ .graph()
1748
+ .names()
1749
+ .values()
1750
+ .find(|n| *n.str() == foo_str_id)
1751
+ .unwrap();
1752
+ assert_eq!(foo_name.ref_count(), 1);
1753
+
1754
+ context.delete_uri("file:///bar.rb");
1755
+ assert_eq!(context.graph().names().len(), 6);
1756
+ }
1757
+
1758
+ #[test]
1759
+ fn updating_index_with_deleted_strings() {
1760
+ let mut context = GraphTest::new();
1761
+
1762
+ context.index_uri(
1763
+ "file:///foo.rb",
1764
+ "
1765
+ Foo
1766
+ foo.method_call
1767
+ def method_name; end
1768
+ ",
1769
+ );
1770
+ context.resolve();
1771
+
1772
+ let strings = context.graph().strings();
1773
+ assert!(strings.get(&StringId::from("Foo")).is_some());
1774
+ assert!(strings.get(&StringId::from("method_call")).is_some());
1775
+ assert!(strings.get(&StringId::from("method_name()")).is_some());
1776
+
1777
+ context.delete_uri("file:///foo.rb");
1778
+ let strings = context.graph().strings();
1779
+ assert!(strings.get(&StringId::from("Foo")).is_none());
1780
+ assert!(strings.get(&StringId::from("method_call")).is_none());
1781
+ assert!(strings.get(&StringId::from("method_name()")).is_none());
1782
+ }
1783
+
1784
+ #[test]
1785
+ fn updating_index_with_new_definitions() {
1786
+ let mut context = GraphTest::new();
1787
+
1788
+ context.index_uri("file:///foo.rb", "module Foo; end");
1789
+ context.resolve();
1790
+
1791
+ assert_eq!(context.graph().definitions.len(), 6);
1792
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1793
+ assert_eq!(declaration.name(), "Foo");
1794
+ let document = context.graph().documents.get(&UriId::from("file:///foo.rb")).unwrap();
1795
+ assert_eq!(document.uri(), "file:///foo.rb");
1796
+ assert_eq!(declaration.definitions().len(), 1);
1797
+ assert_eq!(document.definitions().len(), 1);
1798
+ }
1799
+
1800
+ #[test]
1801
+ fn updating_existing_definitions() {
1802
+ let mut context = GraphTest::new();
1803
+
1804
+ context.index_uri("file:///foo.rb", "module Foo; end");
1805
+ // Update with the same definition but at a different position (with content before it)
1806
+ context.index_uri("file:///foo.rb", "\n\n\n\n\n\nmodule Foo; end");
1807
+ context.resolve();
1808
+
1809
+ assert_eq!(context.graph().definitions.len(), 6);
1810
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1811
+ assert_eq!(declaration.name(), "Foo");
1812
+ assert_eq!(
1813
+ context
1814
+ .graph()
1815
+ .documents()
1816
+ .get(&UriId::from("file:///foo.rb"))
1817
+ .unwrap()
1818
+ .uri(),
1819
+ "file:///foo.rb"
1820
+ );
1821
+
1822
+ let definitions = context.graph().get("Foo").unwrap();
1823
+ assert_eq!(definitions.len(), 1);
1824
+ assert_eq!(definitions[0].offset().start(), 6);
1825
+ }
1826
+
1827
+ #[test]
1828
+ fn adding_another_definition_from_a_different_uri() {
1829
+ let mut context = GraphTest::new();
1830
+
1831
+ context.index_uri("file:///foo.rb", "module Foo; end");
1832
+ context.index_uri("file:///foo2.rb", "\n\n\n\n\nmodule Foo; end");
1833
+ context.resolve();
1834
+
1835
+ let definitions = context.graph().get("Foo").unwrap();
1836
+ let mut offsets = definitions.iter().map(|d| d.offset().start()).collect::<Vec<_>>();
1837
+ offsets.sort_unstable();
1838
+ assert_eq!(definitions.len(), 2);
1839
+ assert_eq!(vec![0, 5], offsets);
1840
+ }
1841
+
1842
+ #[test]
1843
+ fn adding_a_second_definition_from_the_same_uri() {
1844
+ let mut context = GraphTest::new();
1845
+
1846
+ context.index_uri("file:///foo.rb", "module Foo; end");
1847
+
1848
+ // Update with multiple definitions of the same module in one file
1849
+ context.index_uri("file:///foo.rb", {
1850
+ "
1851
+ module Foo; end
1852
+
1853
+
1854
+ module Foo; end
1855
+ "
1856
+ });
1857
+
1858
+ context.resolve();
1859
+
1860
+ let definitions = context.graph().get("Foo").unwrap();
1861
+ assert_eq!(definitions.len(), 2);
1862
+
1863
+ let mut offsets = definitions
1864
+ .iter()
1865
+ .map(|d| [d.offset().start(), d.offset().end()])
1866
+ .collect::<Vec<_>>();
1867
+ offsets.sort_unstable();
1868
+ assert_eq!([0, 15], offsets[0]);
1869
+ assert_eq!([18, 33], offsets[1]);
1870
+ }
1871
+
1872
+ #[test]
1873
+ fn get_documentation() {
1874
+ let mut context = GraphTest::new();
1875
+
1876
+ context.index_uri("file:///foo.rb", {
1877
+ "
1878
+ # This is a class comment
1879
+ # Multi-line comment
1880
+ class CommentedClass; end
1881
+
1882
+ # Module comment
1883
+ module CommentedModule; end
1884
+
1885
+ class NoCommentClass; end
1886
+ "
1887
+ });
1888
+
1889
+ context.resolve();
1890
+
1891
+ let definitions = context.graph().get("CommentedClass").unwrap();
1892
+ let def = definitions.first().unwrap();
1893
+ assert_eq!(
1894
+ def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
1895
+ ["# This is a class comment", "# Multi-line comment"]
1896
+ );
1897
+
1898
+ let definitions = context.graph().get("CommentedModule").unwrap();
1899
+ let def = definitions.first().unwrap();
1900
+ assert_eq!(
1901
+ def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
1902
+ ["# Module comment"]
1903
+ );
1904
+
1905
+ let definitions = context.graph().get("NoCommentClass").unwrap();
1906
+ let def = definitions.first().unwrap();
1907
+ assert!(def.comments().is_empty());
1908
+ }
1909
+
1910
+ #[test]
1911
+ fn members_are_updated_when_definitions_get_deleted() {
1912
+ let mut context = GraphTest::new();
1913
+ // Initially, have `Foo` defined twice with a member called `Bar`
1914
+ context.index_uri("file:///foo.rb", {
1915
+ r"
1916
+ module Foo
1917
+ end
1918
+ "
1919
+ });
1920
+ context.index_uri("file:///foo2.rb", {
1921
+ r"
1922
+ module Foo
1923
+ class Bar; end
1924
+ end
1925
+ "
1926
+ });
1927
+ context.resolve();
1928
+
1929
+ assert_members_eq!(context, "Foo", ["Bar"]);
1930
+
1931
+ // Delete `Bar`
1932
+ context.index_uri("file:///foo2.rb", {
1933
+ r"
1934
+ module Foo
1935
+ end
1936
+ "
1937
+ });
1938
+ context.resolve();
1939
+
1940
+ assert_no_members!(context, "Foo");
1941
+ }
1942
+
1943
+ #[test]
1944
+ fn updating_index_with_deleted_diagnostics() {
1945
+ let mut context = GraphTest::new();
1946
+
1947
+ // TODO: Add resolution error to test diagnostics attached to declarations
1948
+ context.index_uri("file:///foo.rb", "class Foo");
1949
+ assert!(!context.graph().all_diagnostics().is_empty());
1950
+
1951
+ context.index_uri("file:///foo.rb", "class Foo; end");
1952
+ assert_no_diagnostics!(&context);
1953
+ }
1954
+
1955
+ #[test]
1956
+ fn diagnostics_are_collected() {
1957
+ let mut context = GraphTest::new();
1958
+
1959
+ context.index_uri("file:///foo1.rb", {
1960
+ r"
1961
+ class Foo
1962
+ "
1963
+ });
1964
+
1965
+ context.index_uri("file:///foo2.rb", {
1966
+ r"
1967
+ foo = 42
1968
+ "
1969
+ });
1970
+
1971
+ let mut diagnostics: Vec<String> = context
1972
+ .graph()
1973
+ .all_diagnostics()
1974
+ .iter()
1975
+ .map(|d| {
1976
+ format!(
1977
+ "{}: {} ({})",
1978
+ d.rule(),
1979
+ d.message(),
1980
+ context.graph().documents().get(d.uri_id()).unwrap().uri()
1981
+ )
1982
+ })
1983
+ .collect();
1984
+
1985
+ diagnostics.sort();
1986
+
1987
+ assert_eq!(
1988
+ vec![
1989
+ "parse-error: expected an `end` to close the `class` statement (file:///foo1.rb)",
1990
+ "parse-error: unexpected end-of-input, assuming it is closing the parent top level context (file:///foo1.rb)",
1991
+ "parse-warning: assigned but unused variable - foo (file:///foo2.rb)",
1992
+ ],
1993
+ diagnostics,
1994
+ );
1995
+ }
1996
+
1997
+ #[test]
1998
+ fn removing_method_def_with_conflicting_constant_name() {
1999
+ let mut context = GraphTest::new();
2000
+ context.index_uri("file:///foo.rb", {
2001
+ "
2002
+ class Foo
2003
+ class Array; end
2004
+ end
2005
+ "
2006
+ });
2007
+ context.index_uri("file:///foo2.rb", {
2008
+ "
2009
+ class Foo
2010
+ def Array; end
2011
+ end
2012
+ "
2013
+ });
2014
+
2015
+ context.resolve();
2016
+ // Removing the method should not remove the constant
2017
+ context.index_uri("file:///foo2.rb", "");
2018
+
2019
+ let foo = context
2020
+ .graph()
2021
+ .declarations()
2022
+ .get(&DeclarationId::from("Foo"))
2023
+ .unwrap()
2024
+ .as_namespace()
2025
+ .unwrap();
2026
+
2027
+ assert!(foo.member(&StringId::from("Array")).is_some());
2028
+ assert!(foo.member(&StringId::from("Array()")).is_none());
2029
+ }
2030
+
2031
+ #[test]
2032
+ fn removing_constant_with_conflicting_method_name() {
2033
+ let mut context = GraphTest::new();
2034
+ context.index_uri("file:///foo.rb", {
2035
+ "
2036
+ class Foo
2037
+ class Array; end
2038
+ end
2039
+ "
2040
+ });
2041
+ context.index_uri("file:///foo2.rb", {
2042
+ "
2043
+ class Foo
2044
+ def Array; end
2045
+ end
2046
+ "
2047
+ });
2048
+
2049
+ context.resolve();
2050
+ // Removing the method should not remove the constant
2051
+ context.index_uri("file:///foo.rb", "");
2052
+
2053
+ let foo = context
2054
+ .graph()
2055
+ .declarations()
2056
+ .get(&DeclarationId::from("Foo"))
2057
+ .unwrap()
2058
+ .as_namespace()
2059
+ .unwrap();
2060
+ assert!(foo.member(&StringId::from("Array()")).is_some());
2061
+ assert!(foo.member(&StringId::from("Array")).is_none());
2062
+ }
2063
+
2064
+ #[test]
2065
+ fn deleting_class_also_deletes_singleton_class() {
2066
+ let mut context = GraphTest::new();
2067
+
2068
+ context.index_uri("file:///foo.rb", {
2069
+ r"
2070
+ class Foo
2071
+ def self.hello; end
2072
+ end
2073
+ "
2074
+ });
2075
+ context.resolve();
2076
+
2077
+ assert!(context.graph().get("Foo").is_some());
2078
+ assert!(context.graph().get("Foo::<Foo>").is_some());
2079
+
2080
+ context.delete_uri("file:///foo.rb");
2081
+
2082
+ assert!(context.graph().get("Foo").is_none());
2083
+ assert!(context.graph().get("Foo::<Foo>").is_none());
2084
+ }
2085
+
2086
+ #[test]
2087
+ fn deleting_module_also_deletes_singleton_class() {
2088
+ let mut context = GraphTest::new();
2089
+
2090
+ context.index_uri("file:///bar.rb", {
2091
+ r"
2092
+ module Bar
2093
+ def self.greet; end
2094
+ end
2095
+ "
2096
+ });
2097
+ context.resolve();
2098
+
2099
+ assert!(context.graph().get("Bar").is_some());
2100
+ assert!(context.graph().get("Bar::<Bar>").is_some());
2101
+
2102
+ context.delete_uri("file:///bar.rb");
2103
+
2104
+ assert!(context.graph().get("Bar").is_none());
2105
+ assert!(context.graph().get("Bar::<Bar>").is_none());
2106
+ }
2107
+
2108
+ #[test]
2109
+ fn deleting_nested_class_also_deletes_singleton_class() {
2110
+ let mut context = GraphTest::new();
2111
+
2112
+ context.index_uri(
2113
+ "file:///nested.rb",
2114
+ r"
2115
+ class Outer
2116
+ class Inner
2117
+ def self.method; end
2118
+ end
2119
+ end
2120
+ ",
2121
+ );
2122
+ context.resolve();
2123
+
2124
+ assert!(context.graph().get("Outer").is_some());
2125
+ assert!(context.graph().get("Outer::Inner").is_some());
2126
+ assert!(context.graph().get("Outer::Inner::<Inner>").is_some());
2127
+
2128
+ context.delete_uri("file:///nested.rb");
2129
+
2130
+ assert!(context.graph().get("Outer").is_none());
2131
+ assert!(context.graph().get("Outer::Inner").is_none());
2132
+ assert!(context.graph().get("Outer::Inner::<Inner>").is_none());
2133
+ }
2134
+
2135
+ #[test]
2136
+ fn deleting_singleton_class_also_deletes_its_singleton_class() {
2137
+ let mut context = GraphTest::new();
2138
+
2139
+ context.index_uri(
2140
+ "file:///foo.rb",
2141
+ r"
2142
+ class Foo
2143
+ class << self
2144
+ def self.hello; end
2145
+ end
2146
+ end
2147
+ ",
2148
+ );
2149
+ context.resolve();
2150
+
2151
+ assert!(context.graph().get("Foo").is_some());
2152
+ assert!(context.graph().get("Foo::<Foo>").is_some());
2153
+ assert!(context.graph().get("Foo::<Foo>::<<Foo>>").is_some());
2154
+
2155
+ context.delete_uri("file:///foo.rb");
2156
+
2157
+ assert!(context.graph().get("Foo").is_none());
2158
+ assert!(context.graph().get("Foo::<Foo>").is_none());
2159
+ assert!(context.graph().get("Foo::<Foo>::<<Foo>>").is_none());
2160
+ }
2161
+
2162
+ #[test]
2163
+ fn indexing_the_same_document_twice() {
2164
+ let mut context = GraphTest::new();
2165
+ let source = "
2166
+ module Bar; end
2167
+
2168
+ $global_var_1 = 1
2169
+ alias $global_alias_1 $global_var_1
2170
+ ALIAS_CONST_1 = Bar
2171
+
2172
+ class Foo
2173
+ alias $global_alias_2 $global_var_1
2174
+ attr_reader :attr_1
2175
+ attr_writer :attr_2
2176
+ attr_accessor :attr_3
2177
+ ALIAS_CONST_2 = Bar
2178
+
2179
+ $global_var_2 = 1
2180
+ @ivar_1 = 1
2181
+ @@class_var_1 = 1
2182
+
2183
+ def method_1
2184
+ $global_var_3 = 1
2185
+ @ivar_2 = 1
2186
+ @@class_var_2 = 1
2187
+ ALIAS_CONST_3 = Bar
2188
+ end
2189
+ alias_method :aliased_method_1, :method_1
2190
+
2191
+ def self.method_2
2192
+ $global_var_4 = 1
2193
+ @ivar_3 = 1
2194
+ @@class_var_3 = 1
2195
+ ALIAS_CONST_4 = Bar
2196
+ end
2197
+
2198
+ class << self
2199
+ alias $global_alias_3 $global_var_1
2200
+ attr_reader :attr_4
2201
+ attr_writer :attr_5
2202
+ attr_accessor :attr_6
2203
+ ALIAS_CONST_5 = Bar
2204
+
2205
+ $global_var_3 = 1
2206
+ @ivar_4 = 1
2207
+ @@class_var_4 = 1
2208
+
2209
+ def method_3
2210
+ $global_var_4 = 1
2211
+ @ivar_5 = 1
2212
+ @@class_var_5 = 1
2213
+ ALIAS_CONST_6 = Bar
2214
+ end
2215
+ alias_method :aliased_method_1, :method_1
2216
+
2217
+ def self.method_4
2218
+ $global_var_5 = 1
2219
+ @ivar_6 = 1
2220
+ @@class_var_6 = 1
2221
+ ALIAS_CONST_7 = Bar
2222
+ end
2223
+ end
2224
+ end
2225
+ ";
2226
+
2227
+ context.index_uri("file:///foo.rb", source);
2228
+ assert_eq!(49, context.graph().definitions.len());
2229
+ assert_eq!(13, context.graph().constant_references.len());
2230
+ assert_eq!(2, context.graph().method_references.len());
2231
+ assert_eq!(2, context.graph().documents.len());
2232
+ assert_eq!(19, context.graph().names.len());
2233
+ assert_eq!(47, context.graph().strings.len());
2234
+ context.index_uri("file:///foo.rb", source);
2235
+ assert_eq!(49, context.graph().definitions.len());
2236
+ assert_eq!(13, context.graph().constant_references.len());
2237
+ assert_eq!(2, context.graph().method_references.len());
2238
+ assert_eq!(2, context.graph().documents.len());
2239
+ assert_eq!(19, context.graph().names.len());
2240
+ assert_eq!(47, context.graph().strings.len());
2241
+ }
2242
+
2243
+ #[test]
2244
+ fn resolve_alias_follows_chain_to_namespace() {
2245
+ let mut context = GraphTest::new();
2246
+
2247
+ context.index_uri(
2248
+ "file:///foo.rb",
2249
+ "
2250
+ class Original; end
2251
+ Alias1 = Original
2252
+ Alias2 = Alias1
2253
+ ",
2254
+ );
2255
+ context.resolve();
2256
+
2257
+ let target = context.graph().resolve_alias(&DeclarationId::from("Alias2"));
2258
+ assert_eq!(target, Some(DeclarationId::from("Original")));
2259
+ }
2260
+
2261
+ #[test]
2262
+ fn resolve_alias_returns_none_for_circular_aliases() {
2263
+ let mut context = GraphTest::new();
2264
+
2265
+ context.index_uri(
2266
+ "file:///foo.rb",
2267
+ "
2268
+ module Foo
2269
+ A = B
2270
+ B = A
2271
+ end
2272
+ ",
2273
+ );
2274
+ context.resolve();
2275
+
2276
+ assert_eq!(context.graph().resolve_alias(&DeclarationId::from("Foo::A")), None);
2277
+ assert_eq!(context.graph().resolve_alias(&DeclarationId::from("Foo::B")), None);
2278
+ }
2279
+
2280
+ #[test]
2281
+ fn resolve_alias_returns_none_for_non_alias() {
2282
+ let mut context = GraphTest::new();
2283
+
2284
+ context.index_uri("file:///foo.rb", "class Foo; end");
2285
+ context.resolve();
2286
+
2287
+ assert!(context.graph().resolve_alias(&DeclarationId::from("Foo")).is_none());
2288
+ }
2289
+
2290
+ #[test]
2291
+ fn deleting_sole_definition_removes_the_name_entirely() {
2292
+ let mut context = GraphTest::new();
2293
+
2294
+ context.index_uri("file:///foo.rb", "module Foo; end\nBar");
2295
+ context.index_uri("file:///bar.rb", "module Bar; end");
2296
+ context.resolve();
2297
+
2298
+ // Bar declaration should have 1 reference (from foo.rb)
2299
+ let bar_decl = context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap();
2300
+ assert_eq!(bar_decl.as_namespace().unwrap().references().len(), 1);
2301
+
2302
+ // Update foo.rb to remove the Bar reference
2303
+ context.index_uri("file:///foo.rb", "module Foo; end");
2304
+ context.resolve();
2305
+
2306
+ let bar_decl = context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap();
2307
+ assert!(
2308
+ bar_decl.as_namespace().unwrap().references().is_empty(),
2309
+ "Reference to Bar should be detached from declaration"
2310
+ );
2311
+
2312
+ // Delete bar.rb — the Bar name should be fully removed
2313
+ let bar_name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2314
+ context.index_uri("file:///bar.rb", "");
2315
+ context.resolve();
2316
+
2317
+ assert!(
2318
+ context
2319
+ .graph()
2320
+ .declarations()
2321
+ .get(&DeclarationId::from("Bar"))
2322
+ .is_none(),
2323
+ "Bar declaration should be removed"
2324
+ );
2325
+ assert!(
2326
+ context.graph().names().get(&bar_name_id).is_none(),
2327
+ "Bar name should be removed from the names map"
2328
+ );
2329
+ }
2330
+ }
2331
+
2332
+ #[cfg(test)]
2333
+ mod incremental_resolution_tests {
2334
+ use crate::model::name::NameRef;
2335
+ use crate::test_utils::GraphTest;
2336
+ use crate::{
2337
+ assert_alias_targets_contain, assert_ancestors_eq, assert_constant_reference_to,
2338
+ assert_constant_reference_unresolved, assert_declaration_does_not_exist, assert_declaration_exists,
2339
+ assert_declaration_references_count_eq, assert_members_eq, assert_no_constant_alias_target,
2340
+ };
2341
+
2342
+ const NO_ANCESTORS: [&str; 0] = [];
2343
+
2344
+ /// Asserts no declaration holds a definition ID absent from the graph.
2345
+ fn assert_no_dangling_definitions(graph: &super::Graph) {
2346
+ for decl in graph.declarations().values() {
2347
+ for def_id in decl.definitions() {
2348
+ assert!(
2349
+ graph.definitions().contains_key(def_id),
2350
+ "Declaration `{}` references dangling definition {def_id:?}",
2351
+ decl.name(),
2352
+ );
2353
+ }
2354
+ }
2355
+ }
2356
+
2357
+ #[test]
2358
+ fn new_namespace_shadowing_include_target_invalidates_references() {
2359
+ let mut context = GraphTest::new();
2360
+
2361
+ context.index_uri(
2362
+ "file:///foo.rb",
2363
+ r"
2364
+ module Foo
2365
+ module Bar
2366
+ module Baz
2367
+ end
2368
+ end
2369
+ end
2370
+ ",
2371
+ );
2372
+ context.index_uri(
2373
+ "file:///qux.rb",
2374
+ r"
2375
+ module Foo
2376
+ module Bar
2377
+ module Baz
2378
+ class Qux
2379
+ include Bar
2380
+ end
2381
+ end
2382
+ end
2383
+ end
2384
+ ",
2385
+ );
2386
+ context.resolve();
2387
+
2388
+ assert_constant_reference_to!(context, "Foo::Bar", "file:///qux.rb:5:17-5:20");
2389
+ assert_declaration_references_count_eq!(context, "Foo::Bar", 1);
2390
+ assert_ancestors_eq!(
2391
+ context,
2392
+ "Foo::Bar::Baz::Qux",
2393
+ ["Foo::Bar::Baz::Qux", "Foo::Bar", "Object", "Kernel", "BasicObject"]
2394
+ );
2395
+
2396
+ context.index_uri(
2397
+ "file:///foo.rb",
2398
+ r"
2399
+ module Foo
2400
+ module Bar
2401
+ module Baz
2402
+ module Bar; end
2403
+ end
2404
+ end
2405
+ end
2406
+ ",
2407
+ );
2408
+
2409
+ assert_constant_reference_unresolved!(context, "Bar");
2410
+ assert_declaration_references_count_eq!(context, "Foo::Bar", 0);
2411
+ assert_ancestors_eq!(context, "Foo::Bar::Baz::Qux", NO_ANCESTORS);
2412
+
2413
+ context.resolve();
2414
+
2415
+ // Bar now resolves to the new Foo::Bar::Baz::Bar (shadowing Foo::Bar)
2416
+ assert_ancestors_eq!(
2417
+ context,
2418
+ "Foo::Bar::Baz::Qux",
2419
+ [
2420
+ "Foo::Bar::Baz::Qux",
2421
+ "Foo::Bar::Baz::Bar",
2422
+ "Object",
2423
+ "Kernel",
2424
+ "BasicObject"
2425
+ ]
2426
+ );
2427
+ }
2428
+
2429
+ #[test]
2430
+ fn deleting_include_file_invalidates_ancestors_and_references() {
2431
+ let mut context = GraphTest::new();
2432
+
2433
+ context.index_uri(
2434
+ "file:///foo.rb",
2435
+ r"
2436
+ module Foo
2437
+ CONST = 1
2438
+ end
2439
+
2440
+ class Bar
2441
+ CONST
2442
+ end
2443
+ ",
2444
+ );
2445
+ context.index_uri(
2446
+ "file:///bar.rb",
2447
+ r"
2448
+ class Bar
2449
+ include Foo
2450
+ end
2451
+ ",
2452
+ );
2453
+ context.resolve();
2454
+
2455
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///foo.rb:6:3-6:8");
2456
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 1);
2457
+ assert_ancestors_eq!(context, "Bar", ["Bar", "Foo", "Object", "Kernel", "BasicObject"]);
2458
+
2459
+ context.delete_uri("file:///bar.rb");
2460
+
2461
+ assert_constant_reference_unresolved!(context, "CONST");
2462
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 0);
2463
+ assert_ancestors_eq!(context, "Bar", NO_ANCESTORS);
2464
+
2465
+ context.resolve();
2466
+
2467
+ // Bar no longer includes Foo, so CONST is unresolvable
2468
+ assert_constant_reference_unresolved!(context, "CONST");
2469
+ assert_ancestors_eq!(context, "Bar", ["Bar", "Object", "Kernel", "BasicObject"]);
2470
+ }
2471
+
2472
+ #[test]
2473
+ fn invalidating_constant_aliases() {
2474
+ let mut context = GraphTest::new();
2475
+
2476
+ context.index_uri(
2477
+ "file:///foo.rb",
2478
+ r"
2479
+ module Foo
2480
+ CONST = 1
2481
+ end
2482
+
2483
+ class Bar
2484
+ ALIAS_CONST = CONST
2485
+ end
2486
+ ",
2487
+ );
2488
+ context.index_uri(
2489
+ "file:///bar.rb",
2490
+ r"
2491
+ class Bar
2492
+ include Foo
2493
+ end
2494
+ ",
2495
+ );
2496
+ context.resolve();
2497
+
2498
+ assert_alias_targets_contain!(context, "Bar::ALIAS_CONST", "Foo::CONST");
2499
+
2500
+ context.delete_uri("file:///bar.rb");
2501
+
2502
+ assert_no_constant_alias_target!(context, "Bar::ALIAS_CONST");
2503
+
2504
+ context.resolve();
2505
+
2506
+ // Without the include, ALIAS_CONST = CONST can't resolve CONST through Foo
2507
+ assert_no_constant_alias_target!(context, "Bar::ALIAS_CONST");
2508
+ }
2509
+
2510
+ #[test]
2511
+ fn new_constant_in_existing_chain_invalidates_references() {
2512
+ let mut context = GraphTest::new();
2513
+
2514
+ context.index_uri(
2515
+ "file:///foo.rb",
2516
+ r"
2517
+ module Foo
2518
+ CONST = 1
2519
+ end
2520
+
2521
+ module Bar
2522
+ end
2523
+ ",
2524
+ );
2525
+ context.index_uri(
2526
+ "file:///foo2.rb",
2527
+ r"
2528
+ class Baz
2529
+ include Foo
2530
+ prepend Bar
2531
+
2532
+ CONST
2533
+ end
2534
+ ",
2535
+ );
2536
+ context.resolve();
2537
+
2538
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///foo2.rb:5:3-5:8");
2539
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 1);
2540
+
2541
+ context.index_uri(
2542
+ "file:///foo3.rb",
2543
+ r"
2544
+ module Bar
2545
+ CONST = 2
2546
+ end
2547
+ ",
2548
+ );
2549
+
2550
+ assert_constant_reference_unresolved!(context, "CONST");
2551
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 0);
2552
+
2553
+ context.resolve();
2554
+
2555
+ // CONST now resolves to Bar::CONST (prepended, so it's higher in the chain than Foo)
2556
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///foo2.rb:5:3-5:8");
2557
+ }
2558
+
2559
+ #[test]
2560
+ fn deep_ancestor_chain_invalidation() {
2561
+ let mut context = GraphTest::new();
2562
+
2563
+ context.index_uri(
2564
+ "file:///a.rb",
2565
+ r"
2566
+ module A
2567
+ DEEP_CONST = 1
2568
+ end
2569
+ module B
2570
+ include A
2571
+ end
2572
+ module C
2573
+ include B
2574
+ end
2575
+ class D
2576
+ include C
2577
+ DEEP_CONST
2578
+ end
2579
+ ",
2580
+ );
2581
+ context.resolve();
2582
+
2583
+ assert_constant_reference_to!(context, "A::DEEP_CONST", "file:///a.rb:12:3-12:13");
2584
+
2585
+ context.index_uri(
2586
+ "file:///b.rb",
2587
+ r"
2588
+ module C
2589
+ prepend B
2590
+ end
2591
+ ",
2592
+ );
2593
+
2594
+ assert_constant_reference_unresolved!(context, "DEEP_CONST");
2595
+
2596
+ context.resolve();
2597
+
2598
+ // C now also prepends B. DEEP_CONST still resolves through the chain.
2599
+ assert_constant_reference_to!(context, "A::DEEP_CONST", "file:///a.rb:12:3-12:13");
2600
+ }
2601
+
2602
+ #[test]
2603
+ fn new_lexical_definition_takes_priority_over_inherited_one() {
2604
+ let mut context = GraphTest::new();
2605
+
2606
+ // Foo::Bar::Baz exists via nesting
2607
+ context.index_uri(
2608
+ "file:///inheritance.rb",
2609
+ r"
2610
+ module Foo
2611
+ module Bar
2612
+ module Baz; end
2613
+ end
2614
+ end
2615
+ ",
2616
+ );
2617
+ // Qux includes Foo::Bar, so Baz is available through inheritance.
2618
+ // `class Baz::Zip` resolves Baz through the ancestor chain to Foo::Bar::Baz.
2619
+ context.index_uri(
2620
+ "file:///main.rb",
2621
+ r"
2622
+ module Qux
2623
+ include Foo::Bar
2624
+
2625
+ class Baz::Zip; end
2626
+ end
2627
+ ",
2628
+ );
2629
+ context.resolve();
2630
+
2631
+ // Baz in `class Baz::Zip` resolves to Foo::Bar::Baz (via inheritance),
2632
+ // so Zip becomes Foo::Bar::Baz::Zip
2633
+ assert_constant_reference_to!(context, "Foo::Bar::Baz", "file:///main.rb:4:9-4:12");
2634
+ assert_declaration_exists!(context, "Foo::Bar::Baz::Zip");
2635
+
2636
+ // Add Qux::Baz — lexical scope should now take priority over inheritance
2637
+ context.index_uri(
2638
+ "file:///new.rb",
2639
+ r"
2640
+ module Qux
2641
+ class Baz; end
2642
+ end
2643
+ ",
2644
+ );
2645
+ context.resolve();
2646
+
2647
+ // Baz now resolves to Qux::Baz (lexical scope wins over inheritance),
2648
+ // so Zip moves to Qux::Baz::Zip
2649
+ assert_constant_reference_to!(context, "Qux::Baz", "file:///main.rb:4:9-4:12");
2650
+ assert_declaration_exists!(context, "Qux::Baz::Zip");
2651
+ }
2652
+
2653
+ #[test]
2654
+ fn new_file_adding_superclass_invalidates_ancestors() {
2655
+ let mut context = GraphTest::new();
2656
+
2657
+ context.index_uri("file:///foo.rb", "class Foo; end");
2658
+ context.index_uri("file:///bar.rb", "class Bar; end");
2659
+ context.resolve();
2660
+
2661
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Object", "Kernel", "BasicObject"]);
2662
+
2663
+ // A new file reopens Foo with a superclass -- ancestors must be invalidated
2664
+ context.index_uri(
2665
+ "file:///foo2.rb",
2666
+ r"
2667
+ class Foo < Bar
2668
+ end
2669
+ ",
2670
+ );
2671
+
2672
+ assert_ancestors_eq!(context, "Foo", NO_ANCESTORS);
2673
+
2674
+ context.resolve();
2675
+
2676
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Bar", "Object", "Kernel", "BasicObject"]);
2677
+ }
2678
+
2679
+ #[test]
2680
+ fn deleting_module_invalidates_multiple_includers() {
2681
+ let mut context = GraphTest::new();
2682
+
2683
+ context.index_uri(
2684
+ "file:///m.rb",
2685
+ r"
2686
+ module M
2687
+ CONST = 1
2688
+ end
2689
+ ",
2690
+ );
2691
+ context.index_uri(
2692
+ "file:///a.rb",
2693
+ r"
2694
+ class A
2695
+ include M
2696
+ CONST
2697
+ end
2698
+ ",
2699
+ );
2700
+ context.index_uri(
2701
+ "file:///b.rb",
2702
+ r"
2703
+ class B
2704
+ include M
2705
+ CONST
2706
+ end
2707
+ ",
2708
+ );
2709
+ context.resolve();
2710
+
2711
+ assert_constant_reference_to!(context, "M::CONST", "file:///a.rb:3:3-3:8");
2712
+ assert_ancestors_eq!(context, "A", ["A", "M", "Object", "Kernel", "BasicObject"]);
2713
+ assert_ancestors_eq!(context, "B", ["B", "M", "Object", "Kernel", "BasicObject"]);
2714
+
2715
+ context.delete_uri("file:///m.rb");
2716
+
2717
+ assert_ancestors_eq!(context, "A", NO_ANCESTORS);
2718
+ assert_ancestors_eq!(context, "B", NO_ANCESTORS);
2719
+
2720
+ context.resolve();
2721
+
2722
+ // M is gone, but `include M` still exists in the source — M is Partial (unresolvable)
2723
+ assert_ancestors_eq!(context, "A", ["A", Partial("M"), "Object", "Kernel", "BasicObject"]);
2724
+ assert_ancestors_eq!(context, "B", ["B", Partial("M"), "Object", "Kernel", "BasicObject"]);
2725
+ assert_constant_reference_unresolved!(context, "CONST");
2726
+ }
2727
+
2728
+ #[test]
2729
+ fn extend_mixin_invalidation() {
2730
+ let mut context = GraphTest::new();
2731
+
2732
+ context.index_uri(
2733
+ "file:///helpers.rb",
2734
+ r"
2735
+ module Helpers
2736
+ HELPER_CONST = 1
2737
+ end
2738
+ ",
2739
+ );
2740
+ context.index_uri(
2741
+ "file:///foo.rb",
2742
+ r"
2743
+ class Foo
2744
+ extend Helpers
2745
+ end
2746
+ ",
2747
+ );
2748
+ context.resolve();
2749
+
2750
+ assert_declaration_exists!(context, "Helpers");
2751
+ assert_declaration_exists!(context, "Helpers::HELPER_CONST");
2752
+
2753
+ context.delete_uri("file:///helpers.rb");
2754
+ context.resolve();
2755
+
2756
+ assert_declaration_does_not_exist!(context, "Helpers");
2757
+ assert_declaration_does_not_exist!(context, "Helpers::HELPER_CONST");
2758
+ }
2759
+
2760
+ #[test]
2761
+ fn superclass_change_invalidates_ancestors() {
2762
+ let mut context = GraphTest::new();
2763
+
2764
+ context.index_uri(
2765
+ "file:///bar.rb",
2766
+ r"
2767
+ class Bar
2768
+ CONST = 1
2769
+ end
2770
+ ",
2771
+ );
2772
+ context.index_uri(
2773
+ "file:///baz.rb",
2774
+ r"
2775
+ class Baz
2776
+ CONST = 2
2777
+ end
2778
+ ",
2779
+ );
2780
+ context.index_uri(
2781
+ "file:///foo.rb",
2782
+ r"
2783
+ class Foo < Bar
2784
+ end
2785
+ ",
2786
+ );
2787
+ context.index_uri(
2788
+ "file:///ref.rb",
2789
+ r"
2790
+ class Foo
2791
+ CONST
2792
+ end
2793
+ ",
2794
+ );
2795
+ context.resolve();
2796
+
2797
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Bar", "Object", "Kernel", "BasicObject"]);
2798
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///ref.rb:2:3-2:8");
2799
+
2800
+ context.index_uri(
2801
+ "file:///foo.rb",
2802
+ r"
2803
+ class Foo < Baz
2804
+ end
2805
+ ",
2806
+ );
2807
+
2808
+ assert_ancestors_eq!(context, "Foo", NO_ANCESTORS);
2809
+ assert_constant_reference_unresolved!(context, "CONST");
2810
+
2811
+ context.resolve();
2812
+
2813
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Baz", "Object", "Kernel", "BasicObject"]);
2814
+ assert_constant_reference_to!(context, "Baz::CONST", "file:///ref.rb:2:3-2:8");
2815
+ }
2816
+
2817
+ #[test]
2818
+ fn constant_promotion_during_invalidation() {
2819
+ let mut context = GraphTest::new();
2820
+
2821
+ context.index_uri("file:///foo.rb", "Foo = 1");
2822
+ context.resolve();
2823
+
2824
+ assert_declaration_exists!(context, "Foo");
2825
+
2826
+ context.index_uri(
2827
+ "file:///foo_class.rb",
2828
+ r"
2829
+ class Foo
2830
+ end
2831
+ ",
2832
+ );
2833
+ context.resolve();
2834
+
2835
+ assert_declaration_exists!(context, "Foo");
2836
+ assert_members_eq!(
2837
+ context,
2838
+ "Object",
2839
+ ["BasicObject", "Class", "Foo", "Kernel", "Module", "Object"]
2840
+ );
2841
+ }
2842
+
2843
+ #[test]
2844
+ fn multiple_simultaneous_ancestor_changes() {
2845
+ let mut context = GraphTest::new();
2846
+
2847
+ context.index_uri(
2848
+ "file:///m1.rb",
2849
+ r"
2850
+ module M1
2851
+ CONST1 = 1
2852
+ end
2853
+ ",
2854
+ );
2855
+ context.index_uri(
2856
+ "file:///m2.rb",
2857
+ r"
2858
+ module M2
2859
+ CONST2 = 2
2860
+ end
2861
+ ",
2862
+ );
2863
+ context.index_uri(
2864
+ "file:///foo.rb",
2865
+ r"
2866
+ class Foo
2867
+ include M1
2868
+ include M2
2869
+ CONST1
2870
+ CONST2
2871
+ end
2872
+ ",
2873
+ );
2874
+ context.resolve();
2875
+
2876
+ assert_ancestors_eq!(context, "Foo", ["Foo", "M2", "M1", "Object", "Kernel", "BasicObject"]);
2877
+ assert_constant_reference_to!(context, "M1::CONST1", "file:///foo.rb:4:3-4:9");
2878
+ assert_constant_reference_to!(context, "M2::CONST2", "file:///foo.rb:5:3-5:9");
2879
+
2880
+ context.delete_uri("file:///m1.rb");
2881
+ context.delete_uri("file:///m2.rb");
2882
+
2883
+ assert_ancestors_eq!(context, "Foo", NO_ANCESTORS);
2884
+
2885
+ context.resolve();
2886
+
2887
+ assert_ancestors_eq!(
2888
+ context,
2889
+ "Foo",
2890
+ ["Foo", Partial("M2"), Partial("M1"), "Object", "Kernel", "BasicObject"]
2891
+ );
2892
+ assert_declaration_does_not_exist!(context, "M1");
2893
+ assert_declaration_does_not_exist!(context, "M2");
2894
+ assert_constant_reference_unresolved!(context, "CONST1");
2895
+ assert_constant_reference_unresolved!(context, "CONST2");
2896
+ }
2897
+
2898
+ #[test]
2899
+ fn nested_name_reference_resolves_through_lexical_scope() {
2900
+ let mut context = GraphTest::new();
2901
+
2902
+ context.index_uri(
2903
+ "file:///foo.rb",
2904
+ r"
2905
+ module Foo
2906
+ CONST = 1
2907
+ class Bar
2908
+ CONST
2909
+ end
2910
+ end
2911
+ ",
2912
+ );
2913
+ context.resolve();
2914
+
2915
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///foo.rb:4:5-4:10");
2916
+
2917
+ // Update the file — reference still resolves to Foo::CONST
2918
+ context.index_uri(
2919
+ "file:///foo.rb",
2920
+ r"
2921
+ module Foo
2922
+ CONST = 2
2923
+ class Bar
2924
+ CONST
2925
+ end
2926
+ end
2927
+ ",
2928
+ );
2929
+ context.resolve();
2930
+
2931
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///foo.rb:4:5-4:10");
2932
+ }
2933
+
2934
+ #[test]
2935
+ fn child_name_edge_triggers_structural_cascade_on_parent_removal() {
2936
+ let mut context = GraphTest::new();
2937
+
2938
+ context.index_uri(
2939
+ "file:///foo.rb",
2940
+ r"
2941
+ module Foo
2942
+ end
2943
+ ",
2944
+ );
2945
+ context.index_uri(
2946
+ "file:///bar.rb",
2947
+ r"
2948
+ class Foo::Bar
2949
+ CONST
2950
+ end
2951
+ ",
2952
+ );
2953
+ context.index_uri(
2954
+ "file:///const.rb",
2955
+ r"
2956
+ module Foo
2957
+ CONST = 1
2958
+ end
2959
+ ",
2960
+ );
2961
+ context.resolve();
2962
+
2963
+ assert_declaration_exists!(context, "Foo");
2964
+ assert_declaration_exists!(context, "Foo::Bar");
2965
+ assert_members_eq!(context, "Foo", ["Bar", "CONST"]);
2966
+
2967
+ // Delete foo.rb — Foo loses one definition but survives (const.rb still defines it)
2968
+ context.delete_uri("file:///foo.rb");
2969
+
2970
+ // After invalidation but before re-resolve: Bar's name should be unresolved
2971
+ assert_constant_reference_unresolved!(context, "CONST");
2972
+
2973
+ context.resolve();
2974
+
2975
+ // Foo still exists (const.rb defines it). Bar rebuilds as Foo::Bar.
2976
+ // CONST is unresolvable because compact Foo::Bar has no lexical access to Foo's constants.
2977
+ assert_declaration_exists!(context, "Foo");
2978
+ assert_declaration_exists!(context, "Foo::Bar");
2979
+ assert_constant_reference_unresolved!(context, "CONST");
2980
+ }
2981
+
2982
+ #[test]
2983
+ fn ancestor_changes_invalidate_and_re_resolve_constant_references() {
2984
+ let mut context = GraphTest::new();
2985
+
2986
+ context.index_uri(
2987
+ "file:///foo.rb",
2988
+ r"
2989
+ module Foo
2990
+ CONST = 1
2991
+ end
2992
+
2993
+ module Bar
2994
+ CONST = 2
2995
+ end
2996
+ ",
2997
+ );
2998
+ context.index_uri(
2999
+ "file:///foo2.rb",
3000
+ r"
3001
+ class Baz
3002
+ include Foo
3003
+
3004
+ CONST
3005
+ end
3006
+ ",
3007
+ );
3008
+ context.resolve();
3009
+
3010
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///foo2.rb:4:3-4:8");
3011
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 1);
3012
+
3013
+ // Prepending Bar changes Baz's ancestors
3014
+ context.index_uri(
3015
+ "file:///foo3.rb",
3016
+ r"
3017
+ class Baz
3018
+ prepend Bar
3019
+ end
3020
+ ",
3021
+ );
3022
+
3023
+ // Mid-invalidation: CONST is unresolved, detached from Foo::CONST
3024
+ assert_constant_reference_unresolved!(context, "CONST");
3025
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 0);
3026
+
3027
+ // After re-resolve: CONST now points to Bar::CONST (prepend comes first in MRO)
3028
+ context.resolve();
3029
+
3030
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///foo2.rb:4:3-4:8");
3031
+ assert_declaration_references_count_eq!(context, "Bar::CONST", 1);
3032
+ assert_declaration_references_count_eq!(context, "Foo::CONST", 0);
3033
+ }
3034
+
3035
+ #[test]
3036
+ fn re_indexing_same_content_preserves_state() {
3037
+ let mut context = GraphTest::new();
3038
+
3039
+ context.index_uri(
3040
+ "file:///foo.rb",
3041
+ r"
3042
+ module Foo
3043
+ CONST = 1
3044
+ end
3045
+ ",
3046
+ );
3047
+ context.index_uri(
3048
+ "file:///bar.rb",
3049
+ r"
3050
+ class Bar
3051
+ include Foo
3052
+ CONST
3053
+ end
3054
+ ",
3055
+ );
3056
+ context.resolve();
3057
+
3058
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///bar.rb:3:3-3:8");
3059
+ assert_ancestors_eq!(context, "Bar", ["Bar", "Foo", "Object", "Kernel", "BasicObject"]);
3060
+
3061
+ context.index_uri(
3062
+ "file:///bar.rb",
3063
+ r"
3064
+ class Bar
3065
+ include Foo
3066
+ CONST
3067
+ end
3068
+ ",
3069
+ );
3070
+ context.resolve();
3071
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///bar.rb:3:3-3:8");
3072
+ assert_ancestors_eq!(context, "Bar", ["Bar", "Foo", "Object", "Kernel", "BasicObject"]);
3073
+ }
3074
+
3075
+ #[test]
3076
+ fn incremental_resolve_after_delete_and_re_add() {
3077
+ let mut context = GraphTest::new();
3078
+
3079
+ context.index_uri(
3080
+ "file:///foo.rb",
3081
+ r"
3082
+ module Foo
3083
+ CONST = 1
3084
+ end
3085
+ ",
3086
+ );
3087
+ context.index_uri(
3088
+ "file:///bar.rb",
3089
+ r"
3090
+ class Bar
3091
+ include Foo
3092
+ CONST
3093
+ end
3094
+ ",
3095
+ );
3096
+ context.resolve();
3097
+
3098
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///bar.rb:3:3-3:8");
3099
+
3100
+ context.delete_uri("file:///foo.rb");
3101
+ context.index_uri(
3102
+ "file:///foo.rb",
3103
+ r"
3104
+ module Foo
3105
+ CONST = 42
3106
+ end
3107
+ ",
3108
+ );
3109
+
3110
+ context.resolve();
3111
+ assert_constant_reference_to!(context, "Foo::CONST", "file:///bar.rb:3:3-3:8");
3112
+ }
3113
+
3114
+ #[test]
3115
+ fn removing_namespace_declaration_cleans_up_member_methods() {
3116
+ let mut context = GraphTest::new();
3117
+
3118
+ context.index_uri(
3119
+ "file:///foo.rb",
3120
+ r"
3121
+ class Foo
3122
+ def hello; end
3123
+ def world; end
3124
+ end
3125
+ ",
3126
+ );
3127
+ context.resolve();
3128
+
3129
+ assert_declaration_exists!(context, "Foo");
3130
+ assert!(context.graph().get("Foo#hello()").is_some());
3131
+ assert!(context.graph().get("Foo#world()").is_some());
3132
+
3133
+ context.delete_uri("file:///foo.rb");
3134
+ context.resolve();
3135
+
3136
+ assert!(context.graph().get("Foo").is_none());
3137
+ assert!(context.graph().get("Foo#hello()").is_none());
3138
+ assert!(context.graph().get("Foo#world()").is_none());
3139
+ }
3140
+
3141
+ #[test]
3142
+ fn removing_declaration_cascades_to_nested_members() {
3143
+ let mut context = GraphTest::new();
3144
+
3145
+ context.index_uri(
3146
+ "file:///foo.rb",
3147
+ r"
3148
+ module Outer
3149
+ class Inner
3150
+ CONST = 1
3151
+ def method_name; end
3152
+ module Nested; end
3153
+ end
3154
+ end
3155
+ ",
3156
+ );
3157
+ context.resolve();
3158
+
3159
+ assert_declaration_exists!(context, "Outer");
3160
+ assert_declaration_exists!(context, "Outer::Inner");
3161
+ assert_declaration_exists!(context, "Outer::Inner::Nested");
3162
+
3163
+ context.delete_uri("file:///foo.rb");
3164
+ context.resolve();
3165
+
3166
+ assert!(context.graph().get("Outer").is_none());
3167
+ assert!(context.graph().get("Outer::Inner").is_none());
3168
+ assert!(context.graph().get("Outer::Inner::Nested").is_none());
3169
+ assert!(context.graph().get("Outer::Inner#method_name()").is_none());
3170
+ }
3171
+
3172
+ #[test]
3173
+ fn cascade_removes_declaration_with_singleton_and_members() {
3174
+ let mut context = GraphTest::new();
3175
+
3176
+ context.index_uri(
3177
+ "file:///foo.rb",
3178
+ r"
3179
+ module Foo
3180
+ module Bar
3181
+ class Baz
3182
+ def self.class_method; end
3183
+ CONST = 1
3184
+ end
3185
+ end
3186
+ end
3187
+ ",
3188
+ );
3189
+ context.index_uri(
3190
+ "file:///bar.rb",
3191
+ r"
3192
+ module Foo
3193
+ include Bar
3194
+
3195
+ class Baz::Qux
3196
+ def instance_method; end
3197
+ end
3198
+ end
3199
+ ",
3200
+ );
3201
+ context.resolve();
3202
+
3203
+ assert_declaration_exists!(context, "Foo::Bar::Baz::Qux");
3204
+
3205
+ context.index_uri(
3206
+ "file:///baz.rb",
3207
+ r"
3208
+ module Foo
3209
+ module Baz
3210
+ end
3211
+ end
3212
+ ",
3213
+ );
3214
+ context.resolve();
3215
+
3216
+ assert_declaration_does_not_exist!(context, "Foo::Bar::Baz::Qux");
3217
+ assert!(context.graph().get("Foo::Bar::Baz::Qux#instance_method()").is_none());
3218
+ }
3219
+
3220
+ #[test]
3221
+ fn adding_include_resolves_previously_unresolved_references() {
3222
+ let mut context = GraphTest::new();
3223
+
3224
+ context.index_uri(
3225
+ "file:///foo.rb",
3226
+ r"
3227
+ class Foo
3228
+ CONST
3229
+ end
3230
+
3231
+ module Bar
3232
+ CONST = 1
3233
+ end
3234
+ ",
3235
+ );
3236
+ context.resolve();
3237
+
3238
+ // CONST is unresolved (Foo doesn't include Bar yet, CONST not found)
3239
+ assert_constant_reference_unresolved!(context, "CONST");
3240
+
3241
+ context.index_uri(
3242
+ "file:///foo_include.rb",
3243
+ r"
3244
+ class Foo
3245
+ include Bar
3246
+ end
3247
+ ",
3248
+ );
3249
+
3250
+ // After re-resolve, CONST should now resolve through Foo -> Bar
3251
+ context.resolve();
3252
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///foo.rb:2:3-2:8");
3253
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Bar", "Object", "Kernel", "BasicObject"]);
3254
+ }
3255
+
3256
+ #[test]
3257
+ fn re_indexing_module_invalidates_compact_class_inside_it() {
3258
+ let mut context = GraphTest::new();
3259
+
3260
+ context.index_uri(
3261
+ "file:///foo.rb",
3262
+ r"
3263
+ class Foo; end
3264
+ ",
3265
+ );
3266
+
3267
+ context.index_uri(
3268
+ "file:///m.rb",
3269
+ r"
3270
+ module M
3271
+ class Foo::Bar
3272
+ def bar; end
3273
+ end
3274
+ end
3275
+ ",
3276
+ );
3277
+
3278
+ context.resolve();
3279
+
3280
+ assert_declaration_exists!(context, "Foo::Bar");
3281
+ assert_ancestors_eq!(context, "Foo::Bar", ["Foo::Bar", "Object", "Kernel", "BasicObject"]);
3282
+ assert_members_eq!(context, "Foo::Bar", ["bar()"]);
3283
+
3284
+ context.index_uri(
3285
+ "file:///m.rb",
3286
+ r"
3287
+ module M
3288
+ module Foo; end
3289
+
3290
+ class Foo::Bar
3291
+ def bar; end
3292
+ end
3293
+ end
3294
+ ",
3295
+ );
3296
+ context.resolve();
3297
+
3298
+ assert_declaration_exists!(context, "M::Foo::Bar");
3299
+ assert_ancestors_eq!(
3300
+ context,
3301
+ "M::Foo::Bar",
3302
+ ["M::Foo::Bar", "Object", "Kernel", "BasicObject"]
3303
+ );
3304
+ assert_members_eq!(context, "M::Foo::Bar", ["bar()"]);
3305
+ }
3306
+
3307
+ #[test]
3308
+ fn invalidating_namespace_cascades_to_compact_class_and_its_members() {
3309
+ let mut context = GraphTest::new();
3310
+
3311
+ context.index_uri(
3312
+ "file:///foo.rb",
3313
+ r"
3314
+ class Foo
3315
+ end
3316
+ ",
3317
+ );
3318
+
3319
+ context.index_uri(
3320
+ "file:///bar.rb",
3321
+ r"
3322
+ class Foo::Bar
3323
+ def bar; end
3324
+ end
3325
+ ",
3326
+ );
3327
+
3328
+ context.resolve();
3329
+
3330
+ assert_declaration_exists!(context, "Foo");
3331
+ assert_declaration_exists!(context, "Foo::Bar");
3332
+ assert_ancestors_eq!(context, "Foo::Bar", ["Foo::Bar", "Object", "Kernel", "BasicObject"]);
3333
+ assert_members_eq!(context, "Foo", ["Bar"]);
3334
+ assert_members_eq!(context, "Foo::Bar", ["bar()"]);
3335
+
3336
+ context.index_uri(
3337
+ "file:///foo.rb",
3338
+ r"
3339
+ class Baz; end
3340
+
3341
+ Foo = Baz
3342
+
3343
+ class Foo::Bar
3344
+ def bar; end
3345
+ end
3346
+ ",
3347
+ );
3348
+ context.resolve();
3349
+
3350
+ assert_declaration_exists!(context, "Baz::Bar");
3351
+ assert_ancestors_eq!(context, "Baz", ["Baz", "Object", "Kernel", "BasicObject"]);
3352
+ assert_ancestors_eq!(context, "Baz::Bar", ["Baz::Bar", "Object", "Kernel", "BasicObject"]);
3353
+ assert_members_eq!(context, "Baz", ["Bar"]);
3354
+ assert_members_eq!(context, "Baz::Bar", ["bar()"]);
3355
+ }
3356
+ #[test]
3357
+ fn switching_include_target_invalidates_ancestors_and_references() {
3358
+ let mut context = GraphTest::new();
3359
+
3360
+ context.index_uri(
3361
+ "file:///m.rb",
3362
+ r"
3363
+ module M1
3364
+ CONST = 1
3365
+ end
3366
+ module M2
3367
+ CONST = 2
3368
+ end
3369
+ ",
3370
+ );
3371
+ context.index_uri(
3372
+ "file:///foo.rb",
3373
+ r"
3374
+ class Foo
3375
+ include M1
3376
+ CONST
3377
+ end
3378
+ ",
3379
+ );
3380
+ context.resolve();
3381
+
3382
+ assert_ancestors_eq!(context, "Foo", ["Foo", "M1", "Object", "Kernel", "BasicObject"]);
3383
+ assert_constant_reference_to!(context, "M1::CONST", "file:///foo.rb:3:3-3:8");
3384
+
3385
+ context.index_uri(
3386
+ "file:///foo.rb",
3387
+ r"
3388
+ class Foo
3389
+ include M2
3390
+ CONST
3391
+ end
3392
+ ",
3393
+ );
3394
+
3395
+ // Middle state: Foo's only definition was in foo.rb, so the declaration is removed.
3396
+ // CONST reference is unresolved.
3397
+ assert_declaration_does_not_exist!(context, "Foo");
3398
+ assert_constant_reference_unresolved!(context, "CONST");
3399
+
3400
+ context.resolve();
3401
+
3402
+ assert_ancestors_eq!(context, "Foo", ["Foo", "M2", "Object", "Kernel", "BasicObject"]);
3403
+ assert_constant_reference_to!(context, "M2::CONST", "file:///foo.rb:3:3-3:8");
3404
+ }
3405
+
3406
+ #[test]
3407
+ fn removing_superclass_invalidates_ancestors() {
3408
+ let mut context = GraphTest::new();
3409
+
3410
+ context.index_uri(
3411
+ "file:///bar.rb",
3412
+ r"
3413
+ class Bar
3414
+ CONST = 1
3415
+ end
3416
+ ",
3417
+ );
3418
+ context.index_uri(
3419
+ "file:///foo.rb",
3420
+ r"
3421
+ class Foo < Bar
3422
+ CONST
3423
+ end
3424
+ ",
3425
+ );
3426
+ context.resolve();
3427
+
3428
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Bar", "Object", "Kernel", "BasicObject"]);
3429
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///foo.rb:2:3-2:8");
3430
+
3431
+ context.index_uri(
3432
+ "file:///foo.rb",
3433
+ r"
3434
+ class Foo
3435
+ CONST
3436
+ end
3437
+ ",
3438
+ );
3439
+
3440
+ // Middle state: Foo's only definition was in foo.rb, so the declaration is removed.
3441
+ assert_declaration_does_not_exist!(context, "Foo");
3442
+ assert_constant_reference_unresolved!(context, "CONST");
3443
+
3444
+ context.resolve();
3445
+
3446
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Object", "Kernel", "BasicObject"]);
3447
+ assert_constant_reference_unresolved!(context, "CONST");
3448
+ }
3449
+
3450
+ #[test]
3451
+ fn changing_alias_target_invalidates_dependents() {
3452
+ let mut context = GraphTest::new();
3453
+
3454
+ context.index_uri(
3455
+ "file:///targets.rb",
3456
+ r"
3457
+ class Bar
3458
+ CONST = 1
3459
+ end
3460
+ class Baz
3461
+ CONST = 2
3462
+ end
3463
+ ",
3464
+ );
3465
+ context.index_uri(
3466
+ "file:///alias.rb",
3467
+ r"
3468
+ Foo = Bar
3469
+ ",
3470
+ );
3471
+ context.index_uri(
3472
+ "file:///ref.rb",
3473
+ r"
3474
+ Foo::CONST
3475
+ ",
3476
+ );
3477
+ context.resolve();
3478
+
3479
+ assert_constant_reference_to!(context, "Bar::CONST", "file:///ref.rb:1:6-1:11");
3480
+
3481
+ context.index_uri(
3482
+ "file:///alias.rb",
3483
+ r"
3484
+ Foo = Baz
3485
+ ",
3486
+ );
3487
+
3488
+ // Middle state: old Foo alias declaration removed, CONST ref unresolved
3489
+ assert_constant_reference_unresolved!(context, "CONST");
3490
+
3491
+ context.resolve();
3492
+
3493
+ assert_constant_reference_to!(context, "Baz::CONST", "file:///ref.rb:1:6-1:11");
3494
+ }
3495
+
3496
+ #[test]
3497
+ fn switching_mixin_order_invalidates_ancestor_chain() {
3498
+ let mut context = GraphTest::new();
3499
+
3500
+ context.index_uri(
3501
+ "file:///m.rb",
3502
+ r"
3503
+ module Bar; end
3504
+ module Baz; end
3505
+ ",
3506
+ );
3507
+ context.index_uri(
3508
+ "file:///foo.rb",
3509
+ r"
3510
+ class Foo
3511
+ include Bar
3512
+ include Baz
3513
+ end
3514
+ ",
3515
+ );
3516
+ context.resolve();
3517
+
3518
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Baz", "Bar", "Object", "Kernel", "BasicObject"]);
3519
+
3520
+ context.index_uri(
3521
+ "file:///foo.rb",
3522
+ r"
3523
+ class Foo
3524
+ include Baz
3525
+ include Bar
3526
+ end
3527
+ ",
3528
+ );
3529
+
3530
+ // Middle state: Foo's only definition was in foo.rb, so the declaration is removed.
3531
+ assert_declaration_does_not_exist!(context, "Foo");
3532
+
3533
+ context.resolve();
3534
+
3535
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Bar", "Baz", "Object", "Kernel", "BasicObject"]);
3536
+ }
3537
+ #[test]
3538
+ fn adding_mixin_to_multi_definition_declaration_updates_ancestors() {
3539
+ let mut context = GraphTest::new();
3540
+
3541
+ // Foo is defined in two files
3542
+ context.index_uri(
3543
+ "file:///foo1.rb",
3544
+ r"
3545
+ class Foo
3546
+ def bar; end
3547
+ end
3548
+ ",
3549
+ );
3550
+ context.index_uri(
3551
+ "file:///foo2.rb",
3552
+ r"
3553
+ class Foo
3554
+ def baz; end
3555
+ end
3556
+ ",
3557
+ );
3558
+ context.index_uri(
3559
+ "file:///m.rb",
3560
+ r"
3561
+ module M
3562
+ CONST = 1
3563
+ end
3564
+ ",
3565
+ );
3566
+ context.resolve();
3567
+
3568
+ assert_ancestors_eq!(context, "Foo", ["Foo", "Object", "Kernel", "BasicObject"]);
3569
+
3570
+ // Re-index foo2.rb to add a mixin. Foo survives (foo1.rb still defines it)
3571
+ // and enters the update path, pushing Unit::Ancestors.
3572
+ context.index_uri(
3573
+ "file:///foo2.rb",
3574
+ r"
3575
+ class Foo
3576
+ include M
3577
+ def baz; end
3578
+ end
3579
+ ",
3580
+ );
3581
+ context.resolve();
3582
+
3583
+ assert_ancestors_eq!(context, "Foo", ["Foo", "M", "Object", "Kernel", "BasicObject"]);
3584
+ }
3585
+ /// Verifies that incremental resolution produces identical results to a fresh
3586
+ /// full resolution by building the same final state through two different paths.
3587
+ #[test]
3588
+ fn incremental_resolution_matches_fresh_resolution() {
3589
+ // Path 1: Incremental — index, resolve, modify, resolve again
3590
+ let mut incremental = GraphTest::new();
3591
+ incremental.index_uri(
3592
+ "file:///foo.rb",
3593
+ r"
3594
+ module Foo
3595
+ CONST = 1
3596
+ end
3597
+ class Bar
3598
+ include Foo
3599
+ CONST
3600
+ end
3601
+ ",
3602
+ );
3603
+ incremental.index_uri(
3604
+ "file:///baz.rb",
3605
+ r"
3606
+ module Baz
3607
+ CONST = 2
3608
+ end
3609
+ ",
3610
+ );
3611
+ incremental.resolve();
3612
+
3613
+ // Modify: switch include from Foo to Baz
3614
+ incremental.index_uri(
3615
+ "file:///foo.rb",
3616
+ r"
3617
+ module Foo
3618
+ CONST = 1
3619
+ end
3620
+ class Bar
3621
+ include Baz
3622
+ CONST
3623
+ end
3624
+ ",
3625
+ );
3626
+ incremental.resolve();
3627
+
3628
+ // Path 2: Fresh — index the final state directly, resolve once
3629
+ let mut fresh = GraphTest::new();
3630
+ fresh.index_uri(
3631
+ "file:///foo.rb",
3632
+ r"
3633
+ module Foo
3634
+ CONST = 1
3635
+ end
3636
+ class Bar
3637
+ include Baz
3638
+ CONST
3639
+ end
3640
+ ",
3641
+ );
3642
+ fresh.index_uri(
3643
+ "file:///baz.rb",
3644
+ r"
3645
+ module Baz
3646
+ CONST = 2
3647
+ end
3648
+ ",
3649
+ );
3650
+ fresh.resolve();
3651
+
3652
+ // Compare: both paths should produce identical resolved state
3653
+ assert_ancestors_eq!(incremental, "Bar", ["Bar", "Baz", "Object", "Kernel", "BasicObject"]);
3654
+ assert_ancestors_eq!(fresh, "Bar", ["Bar", "Baz", "Object", "Kernel", "BasicObject"]);
3655
+
3656
+ assert_constant_reference_to!(incremental, "Baz::CONST", "file:///foo.rb:6:3-6:8");
3657
+ assert_constant_reference_to!(fresh, "Baz::CONST", "file:///foo.rb:6:3-6:8");
3658
+
3659
+ assert_members_eq!(incremental, "Foo", ["CONST"]);
3660
+ assert_members_eq!(fresh, "Foo", ["CONST"]);
3661
+
3662
+ assert_members_eq!(incremental, "Baz", ["CONST"]);
3663
+ assert_members_eq!(fresh, "Baz", ["CONST"]);
3664
+
3665
+ // Verify stale references are cleaned up
3666
+ assert_declaration_references_count_eq!(incremental, "Foo::CONST", 0);
3667
+ assert_declaration_references_count_eq!(fresh, "Foo::CONST", 0);
3668
+ assert_declaration_references_count_eq!(incremental, "Baz::CONST", 1);
3669
+ assert_declaration_references_count_eq!(fresh, "Baz::CONST", 1);
3670
+ }
3671
+
3672
+ #[test]
3673
+ fn no_dangling_definitions_after_sequential_deletions() {
3674
+ let mut context = GraphTest::new();
3675
+ context.index_uri("file:///a.rb", "module Foo; end");
3676
+ context.index_uri("file:///b.rb", "module Foo; end");
3677
+ context.index_uri("file:///c.rb", "module Foo; class << self; def bar; end; end; end");
3678
+ context.resolve();
3679
+
3680
+ context.delete_uri("file:///b.rb");
3681
+ context.delete_uri("file:///c.rb");
3682
+
3683
+ assert_no_dangling_definitions(context.graph());
3684
+ }
3685
+
3686
+ #[test]
3687
+ fn singleton_class_preserved_after_delete_and_reindex() {
3688
+ let mut context = GraphTest::new();
3689
+
3690
+ context.index_uri("file:///foo.rb", "class Foo; end");
3691
+ context.index_uri("file:///bar.rb", "Foo.new");
3692
+ context.resolve();
3693
+
3694
+ assert_declaration_exists!(context, "Foo");
3695
+ assert_declaration_exists!(context, "Foo::<Foo>");
3696
+
3697
+ context.delete_uri("file:///foo.rb");
3698
+ context.resolve();
3699
+
3700
+ context.index_uri("file:///foo.rb", "class Foo; end");
3701
+ context.resolve();
3702
+
3703
+ assert_declaration_exists!(context, "Foo");
3704
+ assert_declaration_exists!(context, "Foo::<Foo>");
3705
+ }
3706
+
3707
+ #[test]
3708
+ fn singleton_recreated_when_reference_nested_in_compact_class() {
3709
+ let mut context = GraphTest::new();
3710
+
3711
+ context.index_uri("file:///parent.rb", "module Parent; end");
3712
+ context.index_uri("file:///target.rb", "class Parent::Target; end");
3713
+ context.index_uri("file:///caller.rb", "class Parent::Caller; Parent::Target.new; end");
3714
+ context.resolve();
3715
+
3716
+ assert_declaration_exists!(context, "Parent::Target");
3717
+ assert_declaration_exists!(context, "Parent::Target::<Target>");
3718
+
3719
+ context.delete_uri("file:///parent.rb");
3720
+ context.delete_uri("file:///target.rb");
3721
+ context.resolve();
3722
+
3723
+ context.index_uri("file:///parent.rb", "module Parent; end");
3724
+ context.index_uri("file:///target.rb", "class Parent::Target; end");
3725
+ context.resolve();
3726
+
3727
+ assert_declaration_exists!(context, "Parent::Target");
3728
+ assert_declaration_exists!(context, "Parent::Target::<Target>");
3729
+ }
3730
+
3731
+ #[test]
3732
+ fn no_duplicate_definition_on_identical_file_delete_readd() {
3733
+ let source = "class Foo; def self.run; end; def run; end; end";
3734
+
3735
+ let mut context = GraphTest::new();
3736
+ context.index_uri("file:///a.rb", source);
3737
+ context.index_uri("file:///b.rb", source);
3738
+ context.resolve();
3739
+
3740
+ assert_declaration_exists!(context, "Foo");
3741
+ assert_declaration_exists!(context, "Foo::<Foo>#run()");
3742
+ assert_declaration_exists!(context, "Foo#run()");
3743
+
3744
+ context.delete_uri("file:///a.rb");
3745
+ context.resolve();
3746
+
3747
+ context.index_uri("file:///a.rb", source);
3748
+ context.resolve();
3749
+
3750
+ assert_declaration_exists!(context, "Foo");
3751
+ assert_declaration_exists!(context, "Foo::<Foo>#run()");
3752
+ assert_declaration_exists!(context, "Foo#run()");
3753
+ }
3754
+ } // mod incremental_resolution_tests