rubydex 0.1.0.beta1-x86_64-linux → 0.1.0.beta2-x86_64-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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/ext/rubydex/declaration.c +146 -0
  3. data/ext/rubydex/declaration.h +10 -0
  4. data/ext/rubydex/definition.c +234 -0
  5. data/ext/rubydex/definition.h +28 -0
  6. data/ext/rubydex/diagnostic.c +6 -0
  7. data/ext/rubydex/diagnostic.h +11 -0
  8. data/ext/rubydex/document.c +98 -0
  9. data/ext/rubydex/document.h +10 -0
  10. data/ext/rubydex/extconf.rb +36 -15
  11. data/ext/rubydex/graph.c +405 -0
  12. data/ext/rubydex/graph.h +10 -0
  13. data/ext/rubydex/handle.h +44 -0
  14. data/ext/rubydex/location.c +22 -0
  15. data/ext/rubydex/location.h +15 -0
  16. data/ext/rubydex/reference.c +104 -0
  17. data/ext/rubydex/reference.h +16 -0
  18. data/ext/rubydex/rubydex.c +22 -0
  19. data/ext/rubydex/utils.c +27 -0
  20. data/ext/rubydex/utils.h +13 -0
  21. data/lib/rubydex/3.2/rubydex.so +0 -0
  22. data/lib/rubydex/3.3/rubydex.so +0 -0
  23. data/lib/rubydex/3.4/rubydex.so +0 -0
  24. data/lib/rubydex/4.0/rubydex.so +0 -0
  25. data/lib/rubydex/librubydex_sys.so +0 -0
  26. data/lib/rubydex/version.rb +1 -1
  27. data/rust/Cargo.lock +1275 -0
  28. data/rust/Cargo.toml +23 -0
  29. data/rust/about.hbs +78 -0
  30. data/rust/about.toml +9 -0
  31. data/rust/rubydex/Cargo.toml +41 -0
  32. data/rust/rubydex/src/diagnostic.rs +108 -0
  33. data/rust/rubydex/src/errors.rs +28 -0
  34. data/rust/rubydex/src/indexing/local_graph.rs +172 -0
  35. data/rust/rubydex/src/indexing/ruby_indexer.rs +5397 -0
  36. data/rust/rubydex/src/indexing.rs +128 -0
  37. data/rust/rubydex/src/job_queue.rs +186 -0
  38. data/rust/rubydex/src/lib.rs +15 -0
  39. data/rust/rubydex/src/listing.rs +249 -0
  40. data/rust/rubydex/src/main.rs +116 -0
  41. data/rust/rubydex/src/model/comment.rs +24 -0
  42. data/rust/rubydex/src/model/declaration.rs +541 -0
  43. data/rust/rubydex/src/model/definitions.rs +1475 -0
  44. data/rust/rubydex/src/model/document.rs +111 -0
  45. data/rust/rubydex/src/model/encoding.rs +22 -0
  46. data/rust/rubydex/src/model/graph.rs +1387 -0
  47. data/rust/rubydex/src/model/id.rs +90 -0
  48. data/rust/rubydex/src/model/identity_maps.rs +54 -0
  49. data/rust/rubydex/src/model/ids.rs +32 -0
  50. data/rust/rubydex/src/model/name.rs +188 -0
  51. data/rust/rubydex/src/model/references.rs +129 -0
  52. data/rust/rubydex/src/model/string_ref.rs +44 -0
  53. data/rust/rubydex/src/model/visibility.rs +41 -0
  54. data/rust/rubydex/src/model.rs +13 -0
  55. data/rust/rubydex/src/offset.rs +70 -0
  56. data/rust/rubydex/src/position.rs +6 -0
  57. data/rust/rubydex/src/query.rs +103 -0
  58. data/rust/rubydex/src/resolution.rs +4421 -0
  59. data/rust/rubydex/src/stats/memory.rs +71 -0
  60. data/rust/rubydex/src/stats/timer.rs +126 -0
  61. data/rust/rubydex/src/stats.rs +9 -0
  62. data/rust/rubydex/src/test_utils/context.rs +226 -0
  63. data/rust/rubydex/src/test_utils/graph_test.rs +229 -0
  64. data/rust/rubydex/src/test_utils/local_graph_test.rs +166 -0
  65. data/rust/rubydex/src/test_utils.rs +52 -0
  66. data/rust/rubydex/src/visualization/dot.rs +176 -0
  67. data/rust/rubydex/src/visualization.rs +6 -0
  68. data/rust/rubydex/tests/cli.rs +167 -0
  69. data/rust/rubydex-sys/Cargo.toml +20 -0
  70. data/rust/rubydex-sys/build.rs +14 -0
  71. data/rust/rubydex-sys/cbindgen.toml +12 -0
  72. data/rust/rubydex-sys/src/declaration_api.rs +114 -0
  73. data/rust/rubydex-sys/src/definition_api.rs +350 -0
  74. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  75. data/rust/rubydex-sys/src/document_api.rs +54 -0
  76. data/rust/rubydex-sys/src/graph_api.rs +493 -0
  77. data/rust/rubydex-sys/src/lib.rs +9 -0
  78. data/rust/rubydex-sys/src/location_api.rs +79 -0
  79. data/rust/rubydex-sys/src/name_api.rs +81 -0
  80. data/rust/rubydex-sys/src/reference_api.rs +191 -0
  81. data/rust/rubydex-sys/src/utils.rs +50 -0
  82. data/rust/rustfmt.toml +2 -0
  83. metadata +77 -2
@@ -0,0 +1,1387 @@
1
+ use std::collections::hash_map::Entry;
2
+ use std::sync::LazyLock;
3
+
4
+ use crate::diagnostic::Diagnostic;
5
+ use crate::indexing::local_graph::LocalGraph;
6
+ use crate::model::declaration::{Ancestor, Declaration, Namespace};
7
+ use crate::model::definitions::Definition;
8
+ use crate::model::document::Document;
9
+ use crate::model::encoding::Encoding;
10
+ use crate::model::identity_maps::{IdentityHashMap, IdentityHashSet};
11
+ use crate::model::ids::{DeclarationId, DefinitionId, NameId, ReferenceId, StringId, UriId};
12
+ use crate::model::name::{Name, NameRef, ResolvedName};
13
+ use crate::model::references::{ConstantReference, MethodRef};
14
+ use crate::model::string_ref::StringRef;
15
+ use crate::stats;
16
+
17
+ pub static OBJECT_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Object"));
18
+ pub static MODULE_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Module"));
19
+ pub static CLASS_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Class"));
20
+
21
+ // The `Graph` is the global representation of the entire Ruby codebase. It contains all declarations and their
22
+ // relationships
23
+ #[derive(Default, Debug)]
24
+ pub struct Graph {
25
+ // Map of declaration nodes
26
+ declarations: IdentityHashMap<DeclarationId, Declaration>,
27
+ // Map of document nodes
28
+ documents: IdentityHashMap<UriId, Document>,
29
+ // Map of definition nodes
30
+ definitions: IdentityHashMap<DefinitionId, Definition>,
31
+
32
+ definitions_to_declarations: IdentityHashMap<DefinitionId, DeclarationId>,
33
+
34
+ // Map of unqualified names
35
+ strings: IdentityHashMap<StringId, StringRef>,
36
+ // Map of names
37
+ names: IdentityHashMap<NameId, NameRef>,
38
+ // Map of constant references
39
+ constant_references: IdentityHashMap<ReferenceId, ConstantReference>,
40
+ // Map of method references that still need to be resolved
41
+ method_references: IdentityHashMap<ReferenceId, MethodRef>,
42
+
43
+ /// The position encoding used for LSP line/column locations. Not related to the actual encoding of the file
44
+ position_encoding: Encoding,
45
+ }
46
+
47
+ impl Graph {
48
+ #[must_use]
49
+ pub fn new() -> Self {
50
+ Self {
51
+ declarations: IdentityHashMap::default(),
52
+ definitions: IdentityHashMap::default(),
53
+ definitions_to_declarations: IdentityHashMap::default(),
54
+ documents: IdentityHashMap::default(),
55
+ strings: IdentityHashMap::default(),
56
+ names: IdentityHashMap::default(),
57
+ constant_references: IdentityHashMap::default(),
58
+ method_references: IdentityHashMap::default(),
59
+ position_encoding: Encoding::default(),
60
+ }
61
+ }
62
+
63
+ // Returns an immutable reference to the declarations map
64
+ #[must_use]
65
+ pub fn declarations(&self) -> &IdentityHashMap<DeclarationId, Declaration> {
66
+ &self.declarations
67
+ }
68
+
69
+ /// Returns a mutable reference to the declarations map
70
+ #[must_use]
71
+ pub fn declarations_mut(&mut self) -> &mut IdentityHashMap<DeclarationId, Declaration> {
72
+ &mut self.declarations
73
+ }
74
+
75
+ /// # Panics
76
+ ///
77
+ /// Panics if acquiring a write lock fails
78
+ pub fn add_declaration<F>(&mut self, declaration_id: DeclarationId, definition_id: DefinitionId, constructor: F)
79
+ where
80
+ F: FnOnce() -> Declaration,
81
+ {
82
+ self.definitions_to_declarations.insert(definition_id, declaration_id);
83
+
84
+ let declaration = self.declarations.entry(declaration_id).or_insert_with(constructor);
85
+ declaration.add_definition(definition_id);
86
+ }
87
+
88
+ /// # Panics
89
+ ///
90
+ /// Panics if acquiring a write lock fails
91
+ pub fn clear_declarations(&mut self) {
92
+ self.definitions_to_declarations.clear();
93
+ self.declarations.clear();
94
+ }
95
+
96
+ // Returns an immutable reference to the definitions map
97
+ #[must_use]
98
+ pub fn definitions(&self) -> &IdentityHashMap<DefinitionId, Definition> {
99
+ &self.definitions
100
+ }
101
+
102
+ /// Returns the ID of the unqualified name of a definition
103
+ ///
104
+ /// # Panics
105
+ ///
106
+ /// This will panic if there's inconsistent data in the graph
107
+ #[must_use]
108
+ pub fn definition_string_id(&self, definition: &Definition) -> StringId {
109
+ let id = match definition {
110
+ Definition::Class(it) => {
111
+ let name = self.names.get(it.name_id()).unwrap();
112
+ name.str()
113
+ }
114
+ Definition::SingletonClass(it) => {
115
+ let name = self.names.get(it.name_id()).unwrap();
116
+ name.str()
117
+ }
118
+ Definition::Module(it) => {
119
+ let name = self.names.get(it.name_id()).unwrap();
120
+ name.str()
121
+ }
122
+ Definition::Constant(it) => {
123
+ let name = self.names.get(it.name_id()).unwrap();
124
+ name.str()
125
+ }
126
+ Definition::ConstantAlias(it) => {
127
+ let name = self.names.get(it.name_id()).unwrap();
128
+ name.str()
129
+ }
130
+ Definition::GlobalVariable(it) => it.str_id(),
131
+ Definition::InstanceVariable(it) => it.str_id(),
132
+ Definition::ClassVariable(it) => it.str_id(),
133
+ Definition::AttrAccessor(it) => it.str_id(),
134
+ Definition::AttrReader(it) => it.str_id(),
135
+ Definition::AttrWriter(it) => it.str_id(),
136
+ Definition::Method(it) => it.str_id(),
137
+ Definition::MethodAlias(it) => it.new_name_str_id(),
138
+ Definition::GlobalVariableAlias(it) => it.new_name_str_id(),
139
+ };
140
+
141
+ *id
142
+ }
143
+
144
+ // Returns an immutable reference to the strings map
145
+ #[must_use]
146
+ pub fn strings(&self) -> &IdentityHashMap<StringId, StringRef> {
147
+ &self.strings
148
+ }
149
+
150
+ // Returns an immutable reference to the URI pool map
151
+ #[must_use]
152
+ pub fn documents(&self) -> &IdentityHashMap<UriId, Document> {
153
+ &self.documents
154
+ }
155
+
156
+ #[must_use]
157
+ pub fn definitions_to_declarations(&self) -> &IdentityHashMap<DefinitionId, DeclarationId> {
158
+ &self.definitions_to_declarations
159
+ }
160
+
161
+ // Returns an immutable reference to the constant references map
162
+ #[must_use]
163
+ pub fn constant_references(&self) -> &IdentityHashMap<ReferenceId, ConstantReference> {
164
+ &self.constant_references
165
+ }
166
+
167
+ // Returns an immutable reference to the method references map
168
+ #[must_use]
169
+ pub fn method_references(&self) -> &IdentityHashMap<ReferenceId, MethodRef> {
170
+ &self.method_references
171
+ }
172
+
173
+ #[must_use]
174
+ pub fn all_diagnostics(&self) -> Vec<&Diagnostic> {
175
+ let document_diagnostics = self.documents.values().flat_map(Document::diagnostics);
176
+ let declaration_diagnostics = self.declarations.values().flat_map(Declaration::diagnostics);
177
+ let definition_diagnostics = self.definitions.values().flat_map(Definition::diagnostics);
178
+ let constant_reference_diagnostics = self
179
+ .constant_references
180
+ .values()
181
+ .flat_map(ConstantReference::diagnostics);
182
+ let method_reference_diagnostics = self.method_references.values().flat_map(MethodRef::diagnostics);
183
+
184
+ document_diagnostics
185
+ .chain(declaration_diagnostics)
186
+ .chain(definition_diagnostics)
187
+ .chain(constant_reference_diagnostics)
188
+ .chain(method_reference_diagnostics)
189
+ .collect()
190
+ }
191
+
192
+ /// Interns a string in the graph unless already interned. This method is only used to back the
193
+ /// `Graph#resolve_constant` Ruby API because every string must be interned in the graph to properly resolve.
194
+ pub fn intern_string(&mut self, string: String) -> StringId {
195
+ let string_id = StringId::from(&string);
196
+ match self.strings.entry(string_id) {
197
+ Entry::Occupied(mut entry) => {
198
+ entry.get_mut().increment_ref_count(1);
199
+ }
200
+ Entry::Vacant(entry) => {
201
+ entry.insert(StringRef::new(string));
202
+ }
203
+ }
204
+ string_id
205
+ }
206
+
207
+ /// Registers a name in the graph unless already registered. In regular indexing, this only happens in the local
208
+ /// graph. This method is only used to back the `Graph#resolve_constant` Ruby API because every name must be
209
+ /// registered in the graph to properly resolve
210
+ pub fn add_name(&mut self, name: Name) -> NameId {
211
+ let name_id = name.id();
212
+
213
+ match self.names.entry(name_id) {
214
+ Entry::Occupied(mut entry) => {
215
+ entry.get_mut().increment_ref_count(1);
216
+ }
217
+ Entry::Vacant(entry) => {
218
+ entry.insert(NameRef::Unresolved(Box::new(name)));
219
+ }
220
+ }
221
+
222
+ name_id
223
+ }
224
+
225
+ /// # Panics
226
+ ///
227
+ /// Panics if acquiring a read lock fails
228
+ #[must_use]
229
+ pub fn get(&self, name: &str) -> Option<Vec<&Definition>> {
230
+ let declaration_id = DeclarationId::from(name);
231
+ let declaration = self.declarations.get(&declaration_id)?;
232
+
233
+ Some(
234
+ declaration
235
+ .definitions()
236
+ .iter()
237
+ .filter_map(|id| self.definitions.get(id))
238
+ .collect(),
239
+ )
240
+ }
241
+
242
+ /// Returns all target declaration IDs for a constant alias.
243
+ ///
244
+ /// A constant alias can have multiple definitions (e.g., conditional assignment in different files),
245
+ /// each potentially pointing to a different target. This method collects all resolved targets.
246
+ ///
247
+ /// Returns `None` if the declaration doesn't exist or is not a constant alias.
248
+ /// Returns `Some(vec![])` if no targets have been resolved yet.
249
+ #[must_use]
250
+ pub fn alias_targets(&self, declaration_id: &DeclarationId) -> Option<Vec<DeclarationId>> {
251
+ let declaration = self.declarations.get(declaration_id)?;
252
+
253
+ let Declaration::ConstantAlias(_) = declaration else {
254
+ return None;
255
+ };
256
+
257
+ let mut targets = Vec::new();
258
+ for definition_id in declaration.definitions() {
259
+ let Some(Definition::ConstantAlias(alias_def)) = self.definitions.get(definition_id) else {
260
+ continue;
261
+ };
262
+
263
+ let target_name_id = alias_def.target_name_id();
264
+ let Some(name_ref) = self.names.get(target_name_id) else {
265
+ continue;
266
+ };
267
+
268
+ if let NameRef::Resolved(resolved) = name_ref {
269
+ let target_id = *resolved.declaration_id();
270
+ if !targets.contains(&target_id) {
271
+ targets.push(target_id);
272
+ }
273
+ }
274
+ }
275
+
276
+ Some(targets)
277
+ }
278
+
279
+ #[must_use]
280
+ pub fn names(&self) -> &IdentityHashMap<NameId, NameRef> {
281
+ &self.names
282
+ }
283
+
284
+ /// Decrements the ref count for a name and removes it if the count reaches zero.
285
+ ///
286
+ /// This does not recursively untrack `parent_scope` or `nesting` names.
287
+ pub fn untrack_name(&mut self, name_id: NameId) {
288
+ if let Some(name_ref) = self.names.get_mut(&name_id) {
289
+ let string_id = *name_ref.str();
290
+ if !name_ref.decrement_ref_count() {
291
+ self.names.remove(&name_id);
292
+ }
293
+ self.untrack_string(string_id);
294
+ }
295
+ }
296
+
297
+ fn untrack_string(&mut self, string_id: StringId) {
298
+ if let Some(string_ref) = self.strings.get_mut(&string_id)
299
+ && !string_ref.decrement_ref_count()
300
+ {
301
+ self.strings.remove(&string_id);
302
+ }
303
+ }
304
+
305
+ fn untrack_definition_strings(&mut self, definition: &Definition) {
306
+ match definition {
307
+ Definition::Class(_)
308
+ | Definition::SingletonClass(_)
309
+ | Definition::Module(_)
310
+ | Definition::Constant(_)
311
+ | Definition::ConstantAlias(_) => {}
312
+ Definition::Method(d) => self.untrack_string(*d.str_id()),
313
+ Definition::AttrAccessor(d) => self.untrack_string(*d.str_id()),
314
+ Definition::AttrReader(d) => self.untrack_string(*d.str_id()),
315
+ Definition::AttrWriter(d) => self.untrack_string(*d.str_id()),
316
+ Definition::GlobalVariable(d) => self.untrack_string(*d.str_id()),
317
+ Definition::InstanceVariable(d) => self.untrack_string(*d.str_id()),
318
+ Definition::ClassVariable(d) => self.untrack_string(*d.str_id()),
319
+ Definition::MethodAlias(d) => {
320
+ self.untrack_string(*d.new_name_str_id());
321
+ self.untrack_string(*d.old_name_str_id());
322
+ }
323
+ Definition::GlobalVariableAlias(d) => {
324
+ self.untrack_string(*d.new_name_str_id());
325
+ self.untrack_string(*d.old_name_str_id());
326
+ }
327
+ }
328
+ }
329
+
330
+ /// Decrements the ref count for a name and removes it if the count reaches zero.
331
+ ///
332
+ /// This recursively untracks `parent_scope` and `nesting` names.
333
+ pub fn untrack_name_recursive(&mut self, name_id: NameId) {
334
+ let Some(name_ref) = self.names.get(&name_id) else {
335
+ return;
336
+ };
337
+
338
+ let parent_scope = *name_ref.parent_scope();
339
+ let nesting = *name_ref.nesting();
340
+
341
+ self.untrack_name(name_id);
342
+
343
+ if let Some(parent_scope_id) = parent_scope {
344
+ self.untrack_name_recursive(parent_scope_id);
345
+ }
346
+
347
+ if let Some(nesting_id) = nesting {
348
+ self.untrack_name_recursive(nesting_id);
349
+ }
350
+ }
351
+
352
+ /// Register a member relationship from a declaration to another declaration through its unqualified name id. For example, in
353
+ ///
354
+ /// ```ruby
355
+ /// module Foo
356
+ /// class Bar; end
357
+ /// def baz; end
358
+ /// end
359
+ /// ```
360
+ ///
361
+ /// `Foo` has two members:
362
+ /// ```ruby
363
+ /// {
364
+ /// NameId(Bar) => DeclarationId(Bar)
365
+ /// NameId(baz) => DeclarationId(baz)
366
+ /// }
367
+ /// ```
368
+ ///
369
+ /// # Panics
370
+ ///
371
+ /// Will panic if the declaration ID passed doesn't belong to a namespace declaration
372
+ pub fn add_member(
373
+ &mut self,
374
+ owner_id: &DeclarationId,
375
+ member_declaration_id: DeclarationId,
376
+ member_str_id: StringId,
377
+ ) {
378
+ if let Some(declaration) = self.declarations.get_mut(owner_id) {
379
+ match declaration {
380
+ Declaration::Namespace(Namespace::Class(it)) => it.add_member(member_str_id, member_declaration_id),
381
+ Declaration::Namespace(Namespace::Module(it)) => it.add_member(member_str_id, member_declaration_id),
382
+ Declaration::Namespace(Namespace::SingletonClass(it)) => {
383
+ it.add_member(member_str_id, member_declaration_id);
384
+ }
385
+ Declaration::Constant(_) => {
386
+ // TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
387
+ }
388
+ _ => panic!("Tried to add member to a declaration that isn't a namespace"),
389
+ }
390
+ }
391
+ }
392
+
393
+ /// # Panics
394
+ ///
395
+ /// This function will panic when trying to record a resolve name for a name ID that does not exist
396
+ pub fn record_resolved_name(&mut self, name_id: NameId, declaration_id: DeclarationId) {
397
+ match self.names.entry(name_id) {
398
+ Entry::Occupied(entry) => match entry.get() {
399
+ NameRef::Unresolved(_) => {
400
+ if let NameRef::Unresolved(unresolved) = entry.remove() {
401
+ let resolved_name = NameRef::Resolved(Box::new(ResolvedName::new(*unresolved, declaration_id)));
402
+ self.names.insert(name_id, resolved_name);
403
+ }
404
+ }
405
+ NameRef::Resolved(_) => {
406
+ // TODO: consider if this is a valid scenario with the resolution phase design. Either collect
407
+ // metrics here or panic if it's never supposed to occur
408
+ }
409
+ },
410
+ Entry::Vacant(_) => panic!("Trying to record resolved name for a name ID that does not exist"),
411
+ }
412
+ }
413
+
414
+ /// # Panics
415
+ ///
416
+ /// Panics if acquiring a write lock fails
417
+ pub fn record_resolved_reference(&mut self, reference_id: ReferenceId, declaration_id: DeclarationId) {
418
+ if let Some(declaration) = self.declarations.get_mut(&declaration_id) {
419
+ declaration.add_reference(reference_id);
420
+ }
421
+ }
422
+
423
+ //// Handles the deletion of a document identified by `uri`
424
+ pub fn delete_uri(&mut self, uri: &str) {
425
+ let uri_id = UriId::from(uri);
426
+ self.remove_definitions_for_uri(uri_id);
427
+ self.documents.remove(&uri_id);
428
+ }
429
+
430
+ /// Merges everything in `other` into this Graph. This method is meant to merge all graph representations from
431
+ /// different threads, but not meant to handle updates to the existing global representation
432
+ pub fn extend(&mut self, local_graph: LocalGraph) {
433
+ let (uri_id, document, definitions, strings, names, constant_references, method_references) =
434
+ local_graph.into_parts();
435
+
436
+ self.documents.insert(uri_id, document);
437
+ self.definitions.extend(definitions);
438
+ for (string_id, string_ref) in strings {
439
+ match self.strings.entry(string_id) {
440
+ Entry::Occupied(mut entry) => {
441
+ entry.get_mut().increment_ref_count(string_ref.ref_count());
442
+ }
443
+ Entry::Vacant(entry) => {
444
+ entry.insert(string_ref);
445
+ }
446
+ }
447
+ }
448
+ for (name_id, name_ref) in names {
449
+ match self.names.entry(name_id) {
450
+ Entry::Occupied(mut entry) => {
451
+ entry.get_mut().increment_ref_count(name_ref.ref_count());
452
+ }
453
+ Entry::Vacant(entry) => {
454
+ entry.insert(name_ref);
455
+ }
456
+ }
457
+ }
458
+ self.constant_references.extend(constant_references);
459
+ self.method_references.extend(method_references);
460
+ }
461
+
462
+ /// Updates the global representation with the information contained in `other`, handling deletions, insertions and
463
+ /// updates to existing entries
464
+ pub fn update(&mut self, other: LocalGraph) {
465
+ // For each URI that was indexed through `other`, check what was discovered and update our current global
466
+ // representation
467
+ let uri_id = other.uri_id();
468
+ self.remove_definitions_for_uri(uri_id);
469
+
470
+ self.extend(other);
471
+ }
472
+
473
+ // Removes all nodes and relationships associated to the given URI. This is used to clean up stale data when a
474
+ // document (identified by `uri_id`) changes or when a document is closed and we need to clean up the memory
475
+ fn remove_definitions_for_uri(&mut self, uri_id: UriId) {
476
+ let Some(document) = self.documents.remove(&uri_id) else {
477
+ return;
478
+ };
479
+
480
+ // TODO: Remove method references from method declarations once method inference is implemented
481
+ for ref_id in document.method_references() {
482
+ if let Some(method_ref) = self.method_references.remove(ref_id) {
483
+ self.untrack_string(*method_ref.str());
484
+ }
485
+ }
486
+
487
+ for ref_id in document.constant_references() {
488
+ if let Some(constant_ref) = self.constant_references.remove(ref_id) {
489
+ if let Some(NameRef::Resolved(resolved)) = self.names.get(constant_ref.name_id())
490
+ && let Some(declaration) = self.declarations.get_mut(resolved.declaration_id())
491
+ {
492
+ declaration.remove_reference(ref_id);
493
+ }
494
+ self.untrack_name(*constant_ref.name_id());
495
+ }
496
+ }
497
+
498
+ // Vector of (owner_declaration_id, member_name_id) to delete after processing all definitions
499
+ let mut members_to_delete: Vec<(DeclarationId, StringId)> = Vec::new();
500
+ let mut declarations_to_delete: Vec<DeclarationId> = Vec::new();
501
+ let mut declarations_to_invalidate_ancestor_chains: Vec<DeclarationId> = Vec::new();
502
+
503
+ for def_id in document.definitions() {
504
+ if let Some(definition) = self.definitions.remove(def_id) {
505
+ if let Some(declaration_id) = self.definitions_to_declarations.remove(def_id)
506
+ && let Some(declaration) = self.declarations.get_mut(&declaration_id)
507
+ && declaration.remove_definition(def_id)
508
+ {
509
+ declaration.clear_diagnostics();
510
+ if declaration.as_namespace().is_some() {
511
+ declarations_to_invalidate_ancestor_chains.push(declaration_id);
512
+ }
513
+
514
+ if declaration.has_no_definitions() {
515
+ let unqualified_str_id = StringId::from(&declaration.unqualified_name());
516
+ members_to_delete.push((*declaration.owner_id(), unqualified_str_id));
517
+ declarations_to_delete.push(declaration_id);
518
+
519
+ if let Some(namespace) = declaration.as_namespace()
520
+ && let Some(singleton_id) = namespace.singleton_class()
521
+ {
522
+ declarations_to_delete.push(*singleton_id);
523
+ }
524
+ }
525
+ }
526
+
527
+ if let Some(name_id) = definition.name_id() {
528
+ self.untrack_name(*name_id);
529
+ }
530
+
531
+ self.untrack_definition_strings(&definition);
532
+ }
533
+ }
534
+
535
+ self.invalidate_ancestor_chains(declarations_to_invalidate_ancestor_chains);
536
+
537
+ for declaration_id in declarations_to_delete {
538
+ self.declarations.remove(&declaration_id);
539
+ }
540
+
541
+ // Clean up any members that pointed to declarations that were removed
542
+ for (owner_id, member_str_id) in members_to_delete {
543
+ // Remove the `if` and use `unwrap` once we are indexing RBS files to have `Object`
544
+ if let Some(owner) = self.declarations.get_mut(&owner_id) {
545
+ match owner {
546
+ Declaration::Namespace(Namespace::Class(owner)) => {
547
+ owner.remove_member(&member_str_id);
548
+ }
549
+ Declaration::Namespace(Namespace::SingletonClass(owner)) => {
550
+ owner.remove_member(&member_str_id);
551
+ }
552
+ Declaration::Namespace(Namespace::Module(owner)) => {
553
+ owner.remove_member(&member_str_id);
554
+ }
555
+ _ => {} // Nothing happens
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ fn invalidate_ancestor_chains(&mut self, initial_ids: Vec<DeclarationId>) {
562
+ let mut queue = initial_ids;
563
+ let mut visited = IdentityHashSet::<DeclarationId>::default();
564
+
565
+ while let Some(declaration_id) = queue.pop() {
566
+ if !visited.insert(declaration_id) {
567
+ continue;
568
+ }
569
+
570
+ let namespace = self
571
+ .declarations_mut()
572
+ .get_mut(&declaration_id)
573
+ .unwrap()
574
+ .as_namespace_mut()
575
+ .expect("expected namespace declaration");
576
+
577
+ for ancestor in &namespace.ancestors() {
578
+ if let Ancestor::Complete(ancestor_id) = ancestor {
579
+ self.declarations_mut()
580
+ .get_mut(ancestor_id)
581
+ .unwrap()
582
+ .as_namespace_mut()
583
+ .unwrap()
584
+ .remove_descendant(&declaration_id);
585
+ }
586
+ }
587
+
588
+ let namespace = self
589
+ .declarations_mut()
590
+ .get_mut(&declaration_id)
591
+ .unwrap()
592
+ .as_namespace_mut()
593
+ .unwrap();
594
+
595
+ namespace.for_each_descendant(|descendant_id| {
596
+ queue.push(*descendant_id);
597
+ });
598
+
599
+ namespace.clear_ancestors();
600
+ namespace.clear_descendants();
601
+ }
602
+ }
603
+
604
+ /// Sets the encoding that should be used for transforming byte offsets into LSP code unit line/column positions
605
+ pub fn set_encoding(&mut self, encoding: Encoding) {
606
+ self.position_encoding = encoding;
607
+ }
608
+
609
+ #[must_use]
610
+ pub fn encoding(&self) -> &Encoding {
611
+ &self.position_encoding
612
+ }
613
+
614
+ /// # Panics
615
+ ///
616
+ /// Panics if acquiring a read lock fails
617
+ #[allow(clippy::cast_precision_loss)]
618
+ pub fn print_query_statistics(&self) {
619
+ use std::collections::HashMap;
620
+
621
+ let mut declarations_with_docs = 0;
622
+ let mut total_doc_size = 0;
623
+ let mut declarations_types: HashMap<&str, usize> = HashMap::new();
624
+ let mut definition_types: HashMap<&str, usize> = HashMap::new();
625
+ let mut multi_definition_count = 0;
626
+
627
+ for declaration in self.declarations.values() {
628
+ // Check documentation
629
+ if let Some(definitions) = self.get(declaration.name()) {
630
+ let has_docs = definitions.iter().any(|def| !def.comments().is_empty());
631
+ if has_docs {
632
+ declarations_with_docs += 1;
633
+ let doc_size: usize = definitions.iter().map(|def| def.comments().len()).sum();
634
+ total_doc_size += doc_size;
635
+ }
636
+ }
637
+
638
+ *declarations_types.entry(declaration.kind()).or_insert(0) += 1;
639
+
640
+ // Count definitions by type
641
+ let definition_count = declaration.definitions().len();
642
+ if definition_count > 1 {
643
+ multi_definition_count += 1;
644
+ }
645
+
646
+ for def_id in declaration.definitions() {
647
+ if let Some(def) = self.definitions().get(def_id) {
648
+ *definition_types.entry(def.kind()).or_insert(0) += 1;
649
+ }
650
+ }
651
+ }
652
+
653
+ println!();
654
+ println!("Query statistics");
655
+ let total_declarations = self.declarations.len();
656
+ println!(" Total declarations: {total_declarations}");
657
+ println!(
658
+ " With documentation: {} ({:.1}%)",
659
+ declarations_with_docs,
660
+ stats::percentage(declarations_with_docs, total_declarations)
661
+ );
662
+ println!(
663
+ " Without documentation: {} ({:.1}%)",
664
+ total_declarations - declarations_with_docs,
665
+ stats::percentage(total_declarations - declarations_with_docs, total_declarations)
666
+ );
667
+ println!(" Total documentation size: {total_doc_size} bytes");
668
+ println!(
669
+ " Multi-definition names: {} ({:.1}%)",
670
+ multi_definition_count,
671
+ stats::percentage(multi_definition_count, total_declarations)
672
+ );
673
+
674
+ println!();
675
+ println!("Declaration breakdown:");
676
+ let mut types: Vec<_> = declarations_types.iter().collect();
677
+ types.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
678
+ for (kind, count) in types {
679
+ println!(" {kind:20} {count:6}");
680
+ }
681
+
682
+ println!();
683
+ println!("Definition breakdown:");
684
+ let mut types: Vec<_> = definition_types.iter().collect();
685
+ types.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
686
+ for (kind, count) in types {
687
+ println!(" {kind:20} {count:6}");
688
+ }
689
+ }
690
+ }
691
+
692
+ #[cfg(test)]
693
+ mod tests {
694
+ use super::*;
695
+ use crate::model::comment::Comment;
696
+ use crate::model::declaration::Ancestors;
697
+ use crate::test_utils::GraphTest;
698
+
699
+ #[test]
700
+ fn deleting_a_uri() {
701
+ let mut context = GraphTest::new();
702
+
703
+ context.index_uri("file:///foo.rb", "module Foo; end");
704
+ context.delete_uri("file:///foo.rb");
705
+ context.resolve();
706
+
707
+ assert!(context.graph().documents.is_empty());
708
+ assert!(context.graph().definitions.is_empty());
709
+ // Object is left
710
+ assert!(
711
+ context
712
+ .graph()
713
+ .declarations()
714
+ .get(&DeclarationId::from("Foo"))
715
+ .is_none()
716
+ );
717
+ }
718
+
719
+ #[test]
720
+ fn updating_index_with_deleted_definitions() {
721
+ let mut context = GraphTest::new();
722
+
723
+ context.index_uri("file:///foo.rb", "module Foo; end");
724
+
725
+ assert_eq!(context.graph().definitions.len(), 1);
726
+ assert_eq!(context.graph().documents.len(), 1);
727
+
728
+ // Update with empty content to remove definitions but keep the URI
729
+ context.index_uri("file:///foo.rb", "");
730
+
731
+ assert!(context.graph().definitions.is_empty());
732
+
733
+ // URI remains if the file was not deleted, but definitions got erased
734
+ assert_eq!(context.graph().documents.len(), 1);
735
+ }
736
+
737
+ #[test]
738
+ fn updating_index_with_deleted_definitions_after_resolution() {
739
+ let mut context = GraphTest::new();
740
+
741
+ context.index_uri("file:///foo.rb", "module Foo; end");
742
+ context.resolve();
743
+
744
+ assert_eq!(context.graph().definitions.len(), 1);
745
+ assert_eq!(context.graph().definitions_to_declarations().len(), 1);
746
+ assert_eq!(context.graph().documents.len(), 1);
747
+
748
+ {
749
+ assert!(
750
+ context
751
+ .graph()
752
+ .declarations()
753
+ .get(&DeclarationId::from("Foo"))
754
+ .is_some()
755
+ );
756
+ }
757
+
758
+ // Update with empty content to remove definitions but keep the URI
759
+ context.index_uri("file:///foo.rb", "");
760
+
761
+ assert!(context.graph().definitions.is_empty());
762
+ assert!(context.graph().definitions_to_declarations().is_empty());
763
+ // URI remains if the file was not deleted, but definitions and declarations got erased
764
+ assert_eq!(context.graph().documents.len(), 1);
765
+
766
+ {
767
+ assert!(
768
+ context
769
+ .graph()
770
+ .declarations()
771
+ .get(&DeclarationId::from("Foo"))
772
+ .is_none()
773
+ );
774
+ }
775
+ }
776
+
777
+ #[test]
778
+ fn updating_index_with_deleted_references() {
779
+ let mut context = GraphTest::new();
780
+
781
+ context.index_uri("file:///definition.rb", "module Foo; end");
782
+ context.index_uri(
783
+ "file:///references.rb",
784
+ r"
785
+ Foo
786
+ bar
787
+ BAZ
788
+ ",
789
+ );
790
+ context.resolve();
791
+
792
+ assert_eq!(context.graph().documents.len(), 2);
793
+ assert_eq!(context.graph().method_references.len(), 1);
794
+ assert_eq!(context.graph().constant_references.len(), 2);
795
+ {
796
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
797
+ assert_eq!(declaration.references().len(), 1);
798
+ }
799
+
800
+ // Update with empty content to remove definitions but keep the URI
801
+ context.index_uri("file:///references.rb", "");
802
+
803
+ // URI remains if the file was not deleted, but references got erased
804
+ assert_eq!(context.graph().documents.len(), 2);
805
+ assert!(context.graph().method_references.is_empty());
806
+ assert!(context.graph().constant_references.is_empty());
807
+ {
808
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
809
+ assert!(declaration.references().is_empty());
810
+ }
811
+ }
812
+
813
+ #[test]
814
+ fn invalidating_ancestor_chains_when_document_changes() {
815
+ let mut context = GraphTest::new();
816
+
817
+ context.index_uri("file:///a.rb", "class Foo; include Bar; def method_name; end; end");
818
+ context.index_uri("file:///b.rb", "class Foo; end");
819
+ context.index_uri("file:///c.rb", "module Bar; end");
820
+ context.index_uri("file:///d.rb", "class Baz < Foo; end");
821
+ context.resolve();
822
+
823
+ let foo_declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
824
+ assert!(matches!(
825
+ foo_declaration.as_namespace().unwrap().ancestors(),
826
+ Ancestors::Complete(_)
827
+ ));
828
+
829
+ let baz_declaration = context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap();
830
+ assert!(matches!(
831
+ baz_declaration.as_namespace().unwrap().ancestors(),
832
+ Ancestors::Complete(_)
833
+ ));
834
+
835
+ {
836
+ let Declaration::Namespace(Namespace::Module(bar)) =
837
+ context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
838
+ else {
839
+ panic!("Expected Bar to be a module");
840
+ };
841
+ assert!(bar.descendants().contains(&DeclarationId::from("Foo")));
842
+
843
+ let Declaration::Namespace(Namespace::Class(foo)) =
844
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
845
+ else {
846
+ panic!("Expected Foo to be a class");
847
+ };
848
+ assert!(foo.descendants().contains(&DeclarationId::from("Baz")));
849
+ }
850
+
851
+ context.index_uri("file:///a.rb", "");
852
+
853
+ {
854
+ let Declaration::Namespace(Namespace::Class(foo)) =
855
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
856
+ else {
857
+ panic!("Expected Foo to be a class");
858
+ };
859
+ assert!(matches!(foo.clone_ancestors(), Ancestors::Partial(a) if a.is_empty()));
860
+ assert!(foo.descendants().is_empty());
861
+
862
+ let Declaration::Namespace(Namespace::Class(baz)) =
863
+ context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap()
864
+ else {
865
+ panic!("Expected Baz to be a class");
866
+ };
867
+ assert!(matches!(baz.clone_ancestors(), Ancestors::Partial(a) if a.is_empty()));
868
+ assert!(baz.descendants().is_empty());
869
+
870
+ let Declaration::Namespace(Namespace::Module(bar)) =
871
+ context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
872
+ else {
873
+ panic!("Expected Bar to be a module");
874
+ };
875
+ assert!(!bar.descendants().contains(&DeclarationId::from("Foo")));
876
+ }
877
+
878
+ context.resolve();
879
+
880
+ let baz_declaration = context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap();
881
+ assert!(matches!(
882
+ baz_declaration.as_namespace().unwrap().ancestors(),
883
+ Ancestors::Complete(_)
884
+ ));
885
+
886
+ {
887
+ let Declaration::Namespace(Namespace::Class(foo)) =
888
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
889
+ else {
890
+ panic!("Expected Foo to be a class");
891
+ };
892
+ assert!(foo.descendants().contains(&DeclarationId::from("Baz")));
893
+ }
894
+ }
895
+
896
+ #[test]
897
+ fn name_count_increments_for_duplicates() {
898
+ let mut context = GraphTest::new();
899
+
900
+ context.index_uri("file:///foo.rb", "module Foo; end");
901
+ context.index_uri("file:///foo2.rb", "module Foo; end");
902
+ context.index_uri("file:///foo3.rb", "Foo");
903
+ context.resolve();
904
+
905
+ assert_eq!(context.graph().names().len(), 1);
906
+ let name_ref = context.graph().names().values().next().unwrap();
907
+ assert_eq!(name_ref.ref_count(), 3);
908
+ }
909
+
910
+ #[test]
911
+ fn string_ref_count_increments_for_duplicate_definitions() {
912
+ let mut context = GraphTest::new();
913
+
914
+ context.index_uri(
915
+ "file:///foo.rb",
916
+ "
917
+ def method_name; end
918
+ attr_accessor :accessor_name
919
+ attr_reader :reader_name
920
+ attr_writer :writer_name
921
+ $global_var = 1
922
+ @@class_var = 1
923
+ class Foo
924
+ def initialize
925
+ @instance_var = 1
926
+ end
927
+ end
928
+ def old_method; end
929
+ alias_method :new_method, :old_method
930
+ $old_global = 1
931
+ alias $new_global $old_global
932
+ ",
933
+ );
934
+
935
+ context.resolve();
936
+
937
+ let strings = context.graph().strings();
938
+ assert_eq!(strings.get(&StringId::from("method_name()")).unwrap().ref_count(), 1);
939
+ assert_eq!(strings.get(&StringId::from("accessor_name()")).unwrap().ref_count(), 1);
940
+ assert_eq!(strings.get(&StringId::from("reader_name()")).unwrap().ref_count(), 1);
941
+ assert_eq!(strings.get(&StringId::from("writer_name()")).unwrap().ref_count(), 1);
942
+ assert_eq!(strings.get(&StringId::from("$global_var")).unwrap().ref_count(), 1);
943
+ assert_eq!(strings.get(&StringId::from("@@class_var")).unwrap().ref_count(), 1);
944
+ assert_eq!(strings.get(&StringId::from("@instance_var")).unwrap().ref_count(), 1);
945
+ assert_eq!(strings.get(&StringId::from("old_method()")).unwrap().ref_count(), 2);
946
+ assert_eq!(strings.get(&StringId::from("new_method()")).unwrap().ref_count(), 1);
947
+ assert_eq!(strings.get(&StringId::from("$old_global")).unwrap().ref_count(), 2);
948
+ assert_eq!(strings.get(&StringId::from("$new_global")).unwrap().ref_count(), 1);
949
+ }
950
+
951
+ #[test]
952
+ fn updating_index_with_deleted_names() {
953
+ let mut context = GraphTest::new();
954
+
955
+ context.index_uri("file:///foo.rb", "module Foo; end");
956
+ context.index_uri("file:///bar.rb", "Foo");
957
+ context.resolve();
958
+
959
+ assert_eq!(context.graph().names().len(), 1);
960
+ assert_eq!(context.graph().names().values().next().unwrap().ref_count(), 2);
961
+
962
+ context.delete_uri("file:///foo.rb");
963
+ assert_eq!(context.graph().names().len(), 1);
964
+ assert_eq!(context.graph().names().values().next().unwrap().ref_count(), 1);
965
+
966
+ context.delete_uri("file:///bar.rb");
967
+ assert!(context.graph().names().is_empty());
968
+ }
969
+
970
+ #[test]
971
+ fn updating_index_with_deleted_strings() {
972
+ let mut context = GraphTest::new();
973
+
974
+ context.index_uri(
975
+ "file:///foo.rb",
976
+ "
977
+ Foo
978
+ foo.method_call
979
+ def method_name; end
980
+ ",
981
+ );
982
+ context.resolve();
983
+
984
+ let strings = context.graph().strings();
985
+ assert!(strings.get(&StringId::from("Foo")).is_some());
986
+ assert!(strings.get(&StringId::from("method_call")).is_some());
987
+ assert!(strings.get(&StringId::from("method_name()")).is_some());
988
+
989
+ context.delete_uri("file:///foo.rb");
990
+ let strings = context.graph().strings();
991
+ assert!(strings.get(&StringId::from("Foo")).is_none());
992
+ assert!(strings.get(&StringId::from("method_call")).is_none());
993
+ assert!(strings.get(&StringId::from("method_name()")).is_none());
994
+ }
995
+
996
+ #[test]
997
+ fn updating_index_with_new_definitions() {
998
+ let mut context = GraphTest::new();
999
+
1000
+ context.index_uri("file:///foo.rb", "module Foo; end");
1001
+ context.resolve();
1002
+
1003
+ assert_eq!(context.graph().definitions.len(), 1);
1004
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1005
+ assert_eq!(declaration.name(), "Foo");
1006
+ let document = context.graph().documents.get(&UriId::from("file:///foo.rb")).unwrap();
1007
+ assert_eq!(document.uri(), "file:///foo.rb");
1008
+ assert_eq!(declaration.definitions().len(), 1);
1009
+ assert_eq!(document.definitions().len(), 1);
1010
+ }
1011
+
1012
+ #[test]
1013
+ fn updating_existing_definitions() {
1014
+ let mut context = GraphTest::new();
1015
+
1016
+ context.index_uri("file:///foo.rb", "module Foo; end");
1017
+ // Update with the same definition but at a different position (with content before it)
1018
+ context.index_uri("file:///foo.rb", "\n\n\n\n\n\nmodule Foo; end");
1019
+ context.resolve();
1020
+
1021
+ assert_eq!(context.graph().definitions.len(), 1);
1022
+ let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1023
+ assert_eq!(declaration.name(), "Foo");
1024
+ assert_eq!(
1025
+ context
1026
+ .graph()
1027
+ .documents()
1028
+ .get(&UriId::from("file:///foo.rb"))
1029
+ .unwrap()
1030
+ .uri(),
1031
+ "file:///foo.rb"
1032
+ );
1033
+
1034
+ let definitions = context.graph().get("Foo").unwrap();
1035
+ assert_eq!(definitions.len(), 1);
1036
+ assert_eq!(definitions[0].offset().start(), 6);
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn adding_another_definition_from_a_different_uri() {
1041
+ let mut context = GraphTest::new();
1042
+
1043
+ context.index_uri("file:///foo.rb", "module Foo; end");
1044
+ context.index_uri("file:///foo2.rb", "\n\n\n\n\nmodule Foo; end");
1045
+ context.resolve();
1046
+
1047
+ let definitions = context.graph().get("Foo").unwrap();
1048
+ let mut offsets = definitions.iter().map(|d| d.offset().start()).collect::<Vec<_>>();
1049
+ offsets.sort_unstable();
1050
+ assert_eq!(definitions.len(), 2);
1051
+ assert_eq!(vec![0, 5], offsets);
1052
+ }
1053
+
1054
+ #[test]
1055
+ fn adding_a_second_definition_from_the_same_uri() {
1056
+ let mut context = GraphTest::new();
1057
+
1058
+ context.index_uri("file:///foo.rb", "module Foo; end");
1059
+
1060
+ // Update with multiple definitions of the same module in one file
1061
+ context.index_uri("file:///foo.rb", {
1062
+ "
1063
+ module Foo; end
1064
+
1065
+
1066
+ module Foo; end
1067
+ "
1068
+ });
1069
+
1070
+ context.resolve();
1071
+
1072
+ let definitions = context.graph().get("Foo").unwrap();
1073
+ assert_eq!(definitions.len(), 2);
1074
+
1075
+ let mut offsets = definitions
1076
+ .iter()
1077
+ .map(|d| [d.offset().start(), d.offset().end()])
1078
+ .collect::<Vec<_>>();
1079
+ offsets.sort_unstable();
1080
+ assert_eq!([0, 15], offsets[0]);
1081
+ assert_eq!([18, 33], offsets[1]);
1082
+ }
1083
+
1084
+ #[test]
1085
+ fn get_documentation() {
1086
+ let mut context = GraphTest::new();
1087
+
1088
+ context.index_uri("file:///foo.rb", {
1089
+ "
1090
+ # This is a class comment
1091
+ # Multi-line comment
1092
+ class CommentedClass; end
1093
+
1094
+ # Module comment
1095
+ module CommentedModule; end
1096
+
1097
+ class NoCommentClass; end
1098
+ "
1099
+ });
1100
+
1101
+ context.resolve();
1102
+
1103
+ let definitions = context.graph().get("CommentedClass").unwrap();
1104
+ let def = definitions.first().unwrap();
1105
+ assert_eq!(
1106
+ def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
1107
+ vec!["# This is a class comment", "# Multi-line comment"]
1108
+ );
1109
+
1110
+ let definitions = context.graph().get("CommentedModule").unwrap();
1111
+ let def = definitions.first().unwrap();
1112
+ assert_eq!(
1113
+ def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
1114
+ vec!["# Module comment"]
1115
+ );
1116
+
1117
+ let definitions = context.graph().get("NoCommentClass").unwrap();
1118
+ let def = definitions.first().unwrap();
1119
+ assert!(def.comments().is_empty());
1120
+ }
1121
+
1122
+ #[test]
1123
+ fn members_are_updated_when_definitions_get_deleted() {
1124
+ let mut context = GraphTest::new();
1125
+ // Initially, have `Foo` defined twice with a member called `Bar`
1126
+ context.index_uri("file:///foo.rb", {
1127
+ r"
1128
+ module Foo
1129
+ end
1130
+ "
1131
+ });
1132
+ context.index_uri("file:///foo2.rb", {
1133
+ r"
1134
+ module Foo
1135
+ class Bar; end
1136
+ end
1137
+ "
1138
+ });
1139
+ context.resolve();
1140
+
1141
+ {
1142
+ if let Declaration::Namespace(Namespace::Module(foo)) =
1143
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
1144
+ {
1145
+ assert!(foo.members().contains_key(&StringId::from("Bar")));
1146
+ } else {
1147
+ panic!("Expected Foo to be a module");
1148
+ }
1149
+ }
1150
+
1151
+ // Delete `Bar`
1152
+ context.index_uri("file:///foo2.rb", {
1153
+ r"
1154
+ module Foo
1155
+ end
1156
+ "
1157
+ });
1158
+ context.resolve();
1159
+
1160
+ if let Declaration::Namespace(Namespace::Module(foo)) =
1161
+ context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
1162
+ {
1163
+ assert!(!foo.members().contains_key(&StringId::from("Bar")));
1164
+ } else {
1165
+ panic!("Expected Foo to be a module");
1166
+ }
1167
+ }
1168
+
1169
+ #[test]
1170
+ fn updating_index_with_deleted_diagnostics() {
1171
+ let mut context = GraphTest::new();
1172
+
1173
+ // TODO: Add resolution error to test diagnostics attached to declarations
1174
+ context.index_uri("file:///foo.rb", "class Foo");
1175
+ assert!(!context.graph().all_diagnostics().is_empty());
1176
+
1177
+ context.index_uri("file:///foo.rb", "class Foo; end");
1178
+ assert!(context.graph().all_diagnostics().is_empty());
1179
+ }
1180
+
1181
+ #[test]
1182
+ fn diagnostics_are_collected() {
1183
+ let mut context = GraphTest::new();
1184
+
1185
+ context.index_uri("file:///foo1.rb", {
1186
+ r"
1187
+ class Foo
1188
+ "
1189
+ });
1190
+
1191
+ context.index_uri("file:///foo2.rb", {
1192
+ r"
1193
+ foo = 42
1194
+ "
1195
+ });
1196
+
1197
+ let mut diagnostics: Vec<String> = context
1198
+ .graph()
1199
+ .all_diagnostics()
1200
+ .iter()
1201
+ .map(|d| {
1202
+ format!(
1203
+ "{}: {} ({})",
1204
+ d.rule(),
1205
+ d.message(),
1206
+ context.graph().documents().get(d.uri_id()).unwrap().uri()
1207
+ )
1208
+ })
1209
+ .collect();
1210
+
1211
+ diagnostics.sort();
1212
+
1213
+ assert_eq!(
1214
+ vec![
1215
+ "parse-error: expected an `end` to close the `class` statement (file:///foo1.rb)",
1216
+ "parse-error: unexpected end-of-input, assuming it is closing the parent top level context (file:///foo1.rb)",
1217
+ "parse-warning: assigned but unused variable - foo (file:///foo2.rb)",
1218
+ ],
1219
+ diagnostics,
1220
+ );
1221
+ }
1222
+
1223
+ #[test]
1224
+ fn removing_method_def_with_conflicting_constant_name() {
1225
+ let mut context = GraphTest::new();
1226
+ context.index_uri("file:///foo.rb", {
1227
+ "
1228
+ class Foo
1229
+ class Array; end
1230
+ end
1231
+ "
1232
+ });
1233
+ context.index_uri("file:///foo2.rb", {
1234
+ "
1235
+ class Foo
1236
+ def Array; end
1237
+ end
1238
+ "
1239
+ });
1240
+
1241
+ context.resolve();
1242
+ // Removing the method should not remove the constant
1243
+ context.index_uri("file:///foo2.rb", "");
1244
+
1245
+ let foo = context
1246
+ .graph()
1247
+ .declarations()
1248
+ .get(&DeclarationId::from("Foo"))
1249
+ .unwrap()
1250
+ .as_namespace()
1251
+ .unwrap();
1252
+
1253
+ assert!(foo.member(&StringId::from("Array")).is_some());
1254
+ assert!(foo.member(&StringId::from("Array()")).is_none());
1255
+ }
1256
+
1257
+ #[test]
1258
+ fn removing_constant_with_conflicting_method_name() {
1259
+ let mut context = GraphTest::new();
1260
+ context.index_uri("file:///foo.rb", {
1261
+ "
1262
+ class Foo
1263
+ class Array; end
1264
+ end
1265
+ "
1266
+ });
1267
+ context.index_uri("file:///foo2.rb", {
1268
+ "
1269
+ class Foo
1270
+ def Array; end
1271
+ end
1272
+ "
1273
+ });
1274
+
1275
+ context.resolve();
1276
+ // Removing the method should not remove the constant
1277
+ context.index_uri("file:///foo.rb", "");
1278
+
1279
+ let foo = context
1280
+ .graph()
1281
+ .declarations()
1282
+ .get(&DeclarationId::from("Foo"))
1283
+ .unwrap()
1284
+ .as_namespace()
1285
+ .unwrap();
1286
+ assert!(foo.member(&StringId::from("Array()")).is_some());
1287
+ assert!(foo.member(&StringId::from("Array")).is_none());
1288
+ }
1289
+
1290
+ #[test]
1291
+ fn deleting_class_also_deletes_singleton_class() {
1292
+ let mut context = GraphTest::new();
1293
+
1294
+ context.index_uri("file:///foo.rb", {
1295
+ r"
1296
+ class Foo
1297
+ def self.hello; end
1298
+ end
1299
+ "
1300
+ });
1301
+ context.resolve();
1302
+
1303
+ assert!(context.graph().get("Foo").is_some());
1304
+ assert!(context.graph().get("Foo::<Foo>").is_some());
1305
+
1306
+ context.delete_uri("file:///foo.rb");
1307
+
1308
+ assert!(context.graph().get("Foo").is_none());
1309
+ assert!(context.graph().get("Foo::<Foo>").is_none());
1310
+ }
1311
+
1312
+ #[test]
1313
+ fn deleting_module_also_deletes_singleton_class() {
1314
+ let mut context = GraphTest::new();
1315
+
1316
+ context.index_uri("file:///bar.rb", {
1317
+ r"
1318
+ module Bar
1319
+ def self.greet; end
1320
+ end
1321
+ "
1322
+ });
1323
+ context.resolve();
1324
+
1325
+ assert!(context.graph().get("Bar").is_some());
1326
+ assert!(context.graph().get("Bar::<Bar>").is_some());
1327
+
1328
+ context.delete_uri("file:///bar.rb");
1329
+
1330
+ assert!(context.graph().get("Bar").is_none());
1331
+ assert!(context.graph().get("Bar::<Bar>").is_none());
1332
+ }
1333
+
1334
+ #[test]
1335
+ fn deleting_nested_class_also_deletes_singleton_class() {
1336
+ let mut context = GraphTest::new();
1337
+
1338
+ context.index_uri(
1339
+ "file:///nested.rb",
1340
+ r"
1341
+ class Outer
1342
+ class Inner
1343
+ def self.method; end
1344
+ end
1345
+ end
1346
+ ",
1347
+ );
1348
+ context.resolve();
1349
+
1350
+ assert!(context.graph().get("Outer").is_some());
1351
+ assert!(context.graph().get("Outer::Inner").is_some());
1352
+ assert!(context.graph().get("Outer::Inner::<Inner>").is_some());
1353
+
1354
+ context.delete_uri("file:///nested.rb");
1355
+
1356
+ assert!(context.graph().get("Outer").is_none());
1357
+ assert!(context.graph().get("Outer::Inner").is_none());
1358
+ assert!(context.graph().get("Outer::Inner::<Inner>").is_none());
1359
+ }
1360
+
1361
+ #[test]
1362
+ fn deleting_singleton_class_also_deletes_its_singleton_class() {
1363
+ let mut context = GraphTest::new();
1364
+
1365
+ context.index_uri(
1366
+ "file:///foo.rb",
1367
+ r"
1368
+ class Foo
1369
+ class << self
1370
+ def self.hello; end
1371
+ end
1372
+ end
1373
+ ",
1374
+ );
1375
+ context.resolve();
1376
+
1377
+ assert!(context.graph().get("Foo").is_some());
1378
+ assert!(context.graph().get("Foo::<Foo>").is_some());
1379
+ assert!(context.graph().get("Foo::<Foo>::<<Foo>>").is_some());
1380
+
1381
+ context.delete_uri("file:///foo.rb");
1382
+
1383
+ assert!(context.graph().get("Foo").is_none());
1384
+ assert!(context.graph().get("Foo::<Foo>").is_none());
1385
+ assert!(context.graph().get("Foo::<Foo>::<<Foo>>").is_none());
1386
+ }
1387
+ }