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.
- checksums.yaml +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +125 -0
- data/THIRD_PARTY_LICENSES.html +4562 -0
- data/exe/rdx +47 -0
- data/ext/rubydex/declaration.c +453 -0
- data/ext/rubydex/declaration.h +23 -0
- data/ext/rubydex/definition.c +284 -0
- data/ext/rubydex/definition.h +28 -0
- data/ext/rubydex/diagnostic.c +6 -0
- data/ext/rubydex/diagnostic.h +11 -0
- data/ext/rubydex/document.c +97 -0
- data/ext/rubydex/document.h +10 -0
- data/ext/rubydex/extconf.rb +138 -0
- data/ext/rubydex/graph.c +681 -0
- data/ext/rubydex/graph.h +10 -0
- data/ext/rubydex/handle.h +44 -0
- data/ext/rubydex/location.c +22 -0
- data/ext/rubydex/location.h +15 -0
- data/ext/rubydex/reference.c +123 -0
- data/ext/rubydex/reference.h +15 -0
- data/ext/rubydex/rubydex.c +22 -0
- data/ext/rubydex/utils.c +108 -0
- data/ext/rubydex/utils.h +34 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/comment.rb +17 -0
- data/lib/rubydex/diagnostic.rb +21 -0
- data/lib/rubydex/failures.rb +15 -0
- data/lib/rubydex/graph.rb +98 -0
- data/lib/rubydex/keyword.rb +17 -0
- data/lib/rubydex/keyword_parameter.rb +13 -0
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/location.rb +90 -0
- data/lib/rubydex/mixin.rb +22 -0
- data/lib/rubydex/version.rb +5 -0
- data/lib/rubydex.rb +23 -0
- data/rbi/rubydex.rbi +422 -0
- data/rust/Cargo.lock +1851 -0
- data/rust/Cargo.toml +29 -0
- data/rust/about.hbs +78 -0
- data/rust/about.toml +10 -0
- data/rust/rubydex/Cargo.toml +42 -0
- data/rust/rubydex/src/compile_assertions.rs +13 -0
- data/rust/rubydex/src/diagnostic.rs +110 -0
- data/rust/rubydex/src/errors.rs +28 -0
- data/rust/rubydex/src/indexing/local_graph.rs +224 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
- data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
- data/rust/rubydex/src/indexing.rs +210 -0
- data/rust/rubydex/src/integrity.rs +279 -0
- data/rust/rubydex/src/job_queue.rs +205 -0
- data/rust/rubydex/src/lib.rs +17 -0
- data/rust/rubydex/src/listing.rs +371 -0
- data/rust/rubydex/src/main.rs +160 -0
- data/rust/rubydex/src/model/built_in.rs +83 -0
- data/rust/rubydex/src/model/comment.rs +24 -0
- data/rust/rubydex/src/model/declaration.rs +671 -0
- data/rust/rubydex/src/model/definitions.rs +1682 -0
- data/rust/rubydex/src/model/document.rs +222 -0
- data/rust/rubydex/src/model/encoding.rs +22 -0
- data/rust/rubydex/src/model/graph.rs +3754 -0
- data/rust/rubydex/src/model/id.rs +110 -0
- data/rust/rubydex/src/model/identity_maps.rs +58 -0
- data/rust/rubydex/src/model/ids.rs +60 -0
- data/rust/rubydex/src/model/keywords.rs +256 -0
- data/rust/rubydex/src/model/name.rs +298 -0
- data/rust/rubydex/src/model/references.rs +111 -0
- data/rust/rubydex/src/model/string_ref.rs +50 -0
- data/rust/rubydex/src/model/visibility.rs +41 -0
- data/rust/rubydex/src/model.rs +15 -0
- data/rust/rubydex/src/offset.rs +147 -0
- data/rust/rubydex/src/position.rs +6 -0
- data/rust/rubydex/src/query.rs +1841 -0
- data/rust/rubydex/src/resolution.rs +6517 -0
- data/rust/rubydex/src/stats/memory.rs +71 -0
- data/rust/rubydex/src/stats/orphan_report.rs +264 -0
- data/rust/rubydex/src/stats/timer.rs +127 -0
- data/rust/rubydex/src/stats.rs +11 -0
- data/rust/rubydex/src/test_utils/context.rs +226 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
- data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
- data/rust/rubydex/src/test_utils.rs +52 -0
- data/rust/rubydex/src/visualization/dot.rs +192 -0
- data/rust/rubydex/src/visualization.rs +6 -0
- data/rust/rubydex/tests/cli.rs +185 -0
- data/rust/rubydex-mcp/Cargo.toml +28 -0
- data/rust/rubydex-mcp/src/main.rs +48 -0
- data/rust/rubydex-mcp/src/server.rs +1145 -0
- data/rust/rubydex-mcp/src/tools.rs +49 -0
- data/rust/rubydex-mcp/tests/mcp.rs +302 -0
- data/rust/rubydex-sys/Cargo.toml +20 -0
- data/rust/rubydex-sys/build.rs +14 -0
- data/rust/rubydex-sys/cbindgen.toml +12 -0
- data/rust/rubydex-sys/src/declaration_api.rs +485 -0
- data/rust/rubydex-sys/src/definition_api.rs +443 -0
- data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
- data/rust/rubydex-sys/src/document_api.rs +85 -0
- data/rust/rubydex-sys/src/graph_api.rs +948 -0
- data/rust/rubydex-sys/src/lib.rs +79 -0
- data/rust/rubydex-sys/src/location_api.rs +79 -0
- data/rust/rubydex-sys/src/name_api.rs +135 -0
- data/rust/rubydex-sys/src/reference_api.rs +267 -0
- data/rust/rubydex-sys/src/utils.rs +70 -0
- data/rust/rustfmt.toml +2 -0
- 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(¤t_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
|