rubydex 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +69 -3
- data/THIRD_PARTY_LICENSES.html +34 -1
- data/exe/rdx +131 -55
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/graph.c +14 -3
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +22 -0
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/main.rs +2 -124
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +40 -28
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +158 -14
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- metadata +8 -2
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
//! Maps the rubydex [`Graph`] onto a property-graph schema for Cypher execution.
|
|
2
|
+
//!
|
|
3
|
+
//! Node labels:
|
|
4
|
+
//! - `Document` — a source file.
|
|
5
|
+
//! - `Definition` — a per-file occurrence of a Ruby construct.
|
|
6
|
+
//! - `Declaration` — the global, merged concept of a named entity. Declarations also carry
|
|
7
|
+
//! kind sub-labels (`Class`, `Module`, `SingletonClass`, `Method`, `Constant`, `ConstantAlias`,
|
|
8
|
+
//! `GlobalVariable`, `InstanceVariable`, `ClassVariable`) plus the grouping label `Namespace`
|
|
9
|
+
//! (any of `Class`/`Module`/`SingletonClass`).
|
|
10
|
+
//!
|
|
11
|
+
//! Relationship types mirror `dot.rs`:
|
|
12
|
+
//! - `DEFINES`: `Document` → `Definition`
|
|
13
|
+
//! - `DECLARES`: `Definition` → `Declaration`
|
|
14
|
+
//! - `CONTAINS`: `Definition` → `Definition` (lexical nesting in one file, e.g. a class written
|
|
15
|
+
//! inside a module; the source-level counterpart of declaration-level `OWNS`)
|
|
16
|
+
//! - `HAS_PARENT`: `Declaration` → `Declaration` (direct superclass; reverse-traverse for direct
|
|
17
|
+
//! subclasses, `*` for the full chain)
|
|
18
|
+
//! - `INCLUDES` / `PREPENDS` / `EXTENDS`: `Declaration` → `Declaration` (mixins)
|
|
19
|
+
//! - `OWNS`: `Declaration` → `Declaration` (declaration-level membership, e.g. a namespace's methods
|
|
20
|
+
//! and nested constants, merged across all files)
|
|
21
|
+
//! - `HAS_ANCESTOR`: `Declaration` → `Declaration` (linearized ancestor chain, incl. modules)
|
|
22
|
+
//! - `HAS_DESCENDANT`: `Declaration` → `Declaration` (reverse of `HAS_ANCESTOR`)
|
|
23
|
+
//! - `REFERENCES`: `Document` → `Declaration` (constant references)
|
|
24
|
+
|
|
25
|
+
use std::collections::{HashSet, VecDeque};
|
|
26
|
+
|
|
27
|
+
use crate::model::declaration::Declaration;
|
|
28
|
+
use crate::model::definitions::{Definition, Mixin};
|
|
29
|
+
use crate::model::graph::Graph;
|
|
30
|
+
use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, UriId};
|
|
31
|
+
|
|
32
|
+
use cypher_parser::{CypherValue, GraphProvider};
|
|
33
|
+
|
|
34
|
+
/// A handle to a node in the graph.
|
|
35
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
36
|
+
pub enum NodeRef {
|
|
37
|
+
Declaration(DeclarationId),
|
|
38
|
+
Definition(DefinitionId),
|
|
39
|
+
Document(UriId),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A relationship type.
|
|
43
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
44
|
+
pub enum RelType {
|
|
45
|
+
/// `Document` → `Definition`: a file defines a construct occurrence.
|
|
46
|
+
Defines,
|
|
47
|
+
/// `Definition` → `Declaration`: a per-file occurrence declares the global merged entity.
|
|
48
|
+
Declares,
|
|
49
|
+
/// `Definition` → `Definition`: lexical nesting within a single file, e.g. a class written
|
|
50
|
+
/// inside a module. This is the source-level structure; the declaration-level (merged across
|
|
51
|
+
/// files) counterpart is [`RelType::Owns`].
|
|
52
|
+
Contains,
|
|
53
|
+
/// `Declaration` → `Declaration`: the direct superclass (a single hop). Reverse-traverse for
|
|
54
|
+
/// direct subclasses, or use `*` for the full class chain. For the transitive relation
|
|
55
|
+
/// (including modules) see [`RelType::HasAncestor`].
|
|
56
|
+
HasParent,
|
|
57
|
+
/// `Declaration` → `Declaration`: an included module mixin.
|
|
58
|
+
Includes,
|
|
59
|
+
/// `Declaration` → `Declaration`: a prepended module mixin.
|
|
60
|
+
Prepends,
|
|
61
|
+
/// `Declaration` → `Declaration`: an extended module mixin.
|
|
62
|
+
Extends,
|
|
63
|
+
/// `Declaration` → `Declaration`: declaration-level membership (a namespace's methods and
|
|
64
|
+
/// nested constants), merged across all files. The per-file source counterpart is
|
|
65
|
+
/// [`RelType::Contains`].
|
|
66
|
+
Owns,
|
|
67
|
+
/// `Declaration` → `Declaration`: an entry in the linearized ancestor chain (transitive
|
|
68
|
+
/// superclasses plus included/prepended modules).
|
|
69
|
+
HasAncestor,
|
|
70
|
+
/// `Declaration` → `Declaration`: the reverse of [`RelType::HasAncestor`].
|
|
71
|
+
HasDescendant,
|
|
72
|
+
/// `Document` → `Declaration`: a constant reference in the file resolves to a declaration.
|
|
73
|
+
References,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Catalog metadata for a relationship type: the type itself, its canonical name, endpoint labels,
|
|
77
|
+
/// and a short description. Entries live in [`REL_SCHEMAS`], the single source of truth that
|
|
78
|
+
/// `RelType`'s accessors and the `--schema` catalog both derive from.
|
|
79
|
+
pub struct RelSchema {
|
|
80
|
+
pub rel: RelType,
|
|
81
|
+
pub name: &'static str,
|
|
82
|
+
pub from: &'static str,
|
|
83
|
+
pub to: &'static str,
|
|
84
|
+
pub description: &'static str,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The single source of truth for every relationship type: its name, endpoints, and description.
|
|
88
|
+
/// `RelType::all`/`name`/`parse`/`schema` and the `--schema` catalog all derive from this table.
|
|
89
|
+
const REL_SCHEMAS: &[RelSchema] = &[
|
|
90
|
+
RelSchema {
|
|
91
|
+
rel: RelType::Defines,
|
|
92
|
+
name: "DEFINES",
|
|
93
|
+
from: "Document",
|
|
94
|
+
to: "Definition",
|
|
95
|
+
description: "A file defines a construct occurrence",
|
|
96
|
+
},
|
|
97
|
+
RelSchema {
|
|
98
|
+
rel: RelType::Declares,
|
|
99
|
+
name: "DECLARES",
|
|
100
|
+
from: "Definition",
|
|
101
|
+
to: "Declaration",
|
|
102
|
+
description: "An occurrence contributes to a declaration",
|
|
103
|
+
},
|
|
104
|
+
RelSchema {
|
|
105
|
+
rel: RelType::Contains,
|
|
106
|
+
name: "CONTAINS",
|
|
107
|
+
from: "Definition",
|
|
108
|
+
to: "Definition",
|
|
109
|
+
description: "Lexical nesting of definitions",
|
|
110
|
+
},
|
|
111
|
+
RelSchema {
|
|
112
|
+
rel: RelType::HasParent,
|
|
113
|
+
name: "HAS_PARENT",
|
|
114
|
+
from: "Class",
|
|
115
|
+
to: "Class",
|
|
116
|
+
description: "Direct superclass (use `*` for the full chain)",
|
|
117
|
+
},
|
|
118
|
+
RelSchema {
|
|
119
|
+
rel: RelType::Includes,
|
|
120
|
+
name: "INCLUDES",
|
|
121
|
+
from: "Declaration",
|
|
122
|
+
to: "Declaration",
|
|
123
|
+
description: "`include` mixin",
|
|
124
|
+
},
|
|
125
|
+
RelSchema {
|
|
126
|
+
rel: RelType::Prepends,
|
|
127
|
+
name: "PREPENDS",
|
|
128
|
+
from: "Declaration",
|
|
129
|
+
to: "Declaration",
|
|
130
|
+
description: "`prepend` mixin",
|
|
131
|
+
},
|
|
132
|
+
RelSchema {
|
|
133
|
+
rel: RelType::Extends,
|
|
134
|
+
name: "EXTENDS",
|
|
135
|
+
from: "Declaration",
|
|
136
|
+
to: "Declaration",
|
|
137
|
+
description: "`extend` mixin",
|
|
138
|
+
},
|
|
139
|
+
RelSchema {
|
|
140
|
+
rel: RelType::Owns,
|
|
141
|
+
name: "OWNS",
|
|
142
|
+
from: "Declaration",
|
|
143
|
+
to: "Declaration",
|
|
144
|
+
description: "A namespace owns a member declaration",
|
|
145
|
+
},
|
|
146
|
+
RelSchema {
|
|
147
|
+
rel: RelType::HasAncestor,
|
|
148
|
+
name: "HAS_ANCESTOR",
|
|
149
|
+
from: "Declaration",
|
|
150
|
+
to: "Declaration",
|
|
151
|
+
description: "An entry in the linearized ancestor chain (incl. modules)",
|
|
152
|
+
},
|
|
153
|
+
RelSchema {
|
|
154
|
+
rel: RelType::HasDescendant,
|
|
155
|
+
name: "HAS_DESCENDANT",
|
|
156
|
+
from: "Declaration",
|
|
157
|
+
to: "Declaration",
|
|
158
|
+
description: "A declaration that descends from this one",
|
|
159
|
+
},
|
|
160
|
+
RelSchema {
|
|
161
|
+
rel: RelType::References,
|
|
162
|
+
name: "REFERENCES",
|
|
163
|
+
from: "Document",
|
|
164
|
+
to: "Declaration",
|
|
165
|
+
description: "A file references a constant declaration",
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
impl RelType {
|
|
170
|
+
/// All relationship types, in catalog order. Used when a pattern leaves the type unspecified.
|
|
171
|
+
pub fn all() -> impl Iterator<Item = RelType> {
|
|
172
|
+
REL_SCHEMAS.iter().map(|schema| schema.rel)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// The catalog metadata for this relationship type, looked up in the [`REL_SCHEMAS`] table.
|
|
176
|
+
///
|
|
177
|
+
/// # Panics
|
|
178
|
+
///
|
|
179
|
+
/// Panics if a `RelType` variant has no `REL_SCHEMAS` entry. The table is exhaustive by
|
|
180
|
+
/// construction (guarded by a test), so this cannot happen in practice.
|
|
181
|
+
#[must_use]
|
|
182
|
+
pub fn schema(self) -> &'static RelSchema {
|
|
183
|
+
REL_SCHEMAS
|
|
184
|
+
.iter()
|
|
185
|
+
.find(|schema| schema.rel == self)
|
|
186
|
+
.expect("every RelType has a REL_SCHEMAS entry")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// The canonical uppercase name of this relationship type.
|
|
190
|
+
#[must_use]
|
|
191
|
+
pub fn name(self) -> &'static str {
|
|
192
|
+
self.schema().name
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Parses a relationship type name (case-insensitive). Returns `None` if unknown.
|
|
196
|
+
#[must_use]
|
|
197
|
+
pub fn parse(name: &str) -> Option<Self> {
|
|
198
|
+
let upper = name.to_ascii_uppercase();
|
|
199
|
+
REL_SCHEMAS
|
|
200
|
+
.iter()
|
|
201
|
+
.find(|schema| schema.name == upper)
|
|
202
|
+
.map(|schema| schema.rel)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// A node label and what it matches, for the `--schema` catalog.
|
|
207
|
+
pub struct LabelSchema {
|
|
208
|
+
pub label: &'static str,
|
|
209
|
+
pub matches: &'static str,
|
|
210
|
+
pub description: &'static str,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// A property exposed on a node type, for the `--schema` catalog.
|
|
214
|
+
pub struct PropertySchema {
|
|
215
|
+
pub node_type: &'static str,
|
|
216
|
+
pub property: &'static str,
|
|
217
|
+
pub description: &'static str,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// The node labels queries can match against. Single source of truth for the `--schema` catalog;
|
|
221
|
+
/// the matching behavior lives in [`scan_label`] and [`declaration_matches_label`].
|
|
222
|
+
pub const NODE_LABELS: &[LabelSchema] = &[
|
|
223
|
+
LabelSchema {
|
|
224
|
+
label: "Document",
|
|
225
|
+
matches: "source files",
|
|
226
|
+
description: "A source file in the workspace",
|
|
227
|
+
},
|
|
228
|
+
LabelSchema {
|
|
229
|
+
label: "Definition",
|
|
230
|
+
matches: "per-file occurrences",
|
|
231
|
+
description: "A single occurrence of a Ruby construct in one file",
|
|
232
|
+
},
|
|
233
|
+
LabelSchema {
|
|
234
|
+
label: "Declaration",
|
|
235
|
+
matches: "merged entities",
|
|
236
|
+
description: "The global, merged concept of a named entity",
|
|
237
|
+
},
|
|
238
|
+
LabelSchema {
|
|
239
|
+
label: "Namespace",
|
|
240
|
+
matches: "Class | Module | SingletonClass declarations",
|
|
241
|
+
description: "Grouping label for namespace-like declarations",
|
|
242
|
+
},
|
|
243
|
+
LabelSchema {
|
|
244
|
+
label: "Class",
|
|
245
|
+
matches: "declarations of kind Class",
|
|
246
|
+
description: "A class declaration",
|
|
247
|
+
},
|
|
248
|
+
LabelSchema {
|
|
249
|
+
label: "Module",
|
|
250
|
+
matches: "declarations of kind Module",
|
|
251
|
+
description: "A module declaration",
|
|
252
|
+
},
|
|
253
|
+
LabelSchema {
|
|
254
|
+
label: "SingletonClass",
|
|
255
|
+
matches: "declarations of kind SingletonClass",
|
|
256
|
+
description: "A singleton class declaration",
|
|
257
|
+
},
|
|
258
|
+
LabelSchema {
|
|
259
|
+
label: "Method",
|
|
260
|
+
matches: "declarations of kind Method",
|
|
261
|
+
description: "A method declaration",
|
|
262
|
+
},
|
|
263
|
+
LabelSchema {
|
|
264
|
+
label: "Constant",
|
|
265
|
+
matches: "declarations of kind Constant",
|
|
266
|
+
description: "A constant declaration",
|
|
267
|
+
},
|
|
268
|
+
LabelSchema {
|
|
269
|
+
label: "ConstantAlias",
|
|
270
|
+
matches: "declarations of kind ConstantAlias",
|
|
271
|
+
description: "A constant alias declaration",
|
|
272
|
+
},
|
|
273
|
+
LabelSchema {
|
|
274
|
+
label: "GlobalVariable",
|
|
275
|
+
matches: "declarations of kind GlobalVariable",
|
|
276
|
+
description: "A global variable declaration",
|
|
277
|
+
},
|
|
278
|
+
LabelSchema {
|
|
279
|
+
label: "InstanceVariable",
|
|
280
|
+
matches: "declarations of kind InstanceVariable",
|
|
281
|
+
description: "An instance variable declaration",
|
|
282
|
+
},
|
|
283
|
+
LabelSchema {
|
|
284
|
+
label: "ClassVariable",
|
|
285
|
+
matches: "declarations of kind ClassVariable",
|
|
286
|
+
description: "A class variable declaration",
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
/// The node properties queries can read. Single source of truth for the `--schema` catalog; the
|
|
291
|
+
/// value resolution lives in [`property`] and its per-node-type helpers.
|
|
292
|
+
pub const NODE_PROPERTIES: &[PropertySchema] = &[
|
|
293
|
+
PropertySchema {
|
|
294
|
+
node_type: "(any)",
|
|
295
|
+
property: "label",
|
|
296
|
+
description: "The node's top-level label / kind",
|
|
297
|
+
},
|
|
298
|
+
PropertySchema {
|
|
299
|
+
node_type: "(any)",
|
|
300
|
+
property: "kind",
|
|
301
|
+
description: "Alias of `label`",
|
|
302
|
+
},
|
|
303
|
+
PropertySchema {
|
|
304
|
+
node_type: "Declaration",
|
|
305
|
+
property: "name",
|
|
306
|
+
description: "Fully qualified name",
|
|
307
|
+
},
|
|
308
|
+
PropertySchema {
|
|
309
|
+
node_type: "Declaration",
|
|
310
|
+
property: "unqualified_name",
|
|
311
|
+
description: "Name without its namespace prefix",
|
|
312
|
+
},
|
|
313
|
+
PropertySchema {
|
|
314
|
+
node_type: "Declaration",
|
|
315
|
+
property: "visibility",
|
|
316
|
+
description: "public / protected / private (when applicable)",
|
|
317
|
+
},
|
|
318
|
+
PropertySchema {
|
|
319
|
+
node_type: "Declaration",
|
|
320
|
+
property: "definition_count",
|
|
321
|
+
description: "Number of definitions that compose the declaration",
|
|
322
|
+
},
|
|
323
|
+
PropertySchema {
|
|
324
|
+
node_type: "Definition",
|
|
325
|
+
property: "name",
|
|
326
|
+
description: "Name of the declaration this definition contributes to",
|
|
327
|
+
},
|
|
328
|
+
PropertySchema {
|
|
329
|
+
node_type: "Definition",
|
|
330
|
+
property: "file",
|
|
331
|
+
description: "URI of the file containing the definition",
|
|
332
|
+
},
|
|
333
|
+
PropertySchema {
|
|
334
|
+
node_type: "Definition",
|
|
335
|
+
property: "line",
|
|
336
|
+
description: "1-indexed start line of the definition",
|
|
337
|
+
},
|
|
338
|
+
PropertySchema {
|
|
339
|
+
node_type: "Document",
|
|
340
|
+
property: "uri",
|
|
341
|
+
description: "Full document URI",
|
|
342
|
+
},
|
|
343
|
+
PropertySchema {
|
|
344
|
+
node_type: "Document",
|
|
345
|
+
property: "path",
|
|
346
|
+
description: "File system path of the document",
|
|
347
|
+
},
|
|
348
|
+
PropertySchema {
|
|
349
|
+
node_type: "Document",
|
|
350
|
+
property: "name",
|
|
351
|
+
description: "Base file name of the document",
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
/// Exposes the rubydex [`Graph`] to the `cypher-parser` executor as a property graph. This is the
|
|
356
|
+
/// rubydex-specific mapping; the executor itself is generic over this trait.
|
|
357
|
+
impl GraphProvider for Graph {
|
|
358
|
+
type NodeId = NodeRef;
|
|
359
|
+
|
|
360
|
+
fn scan(&self, labels: &[String]) -> Vec<NodeRef> {
|
|
361
|
+
scan(self, labels)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn matches_label(&self, node: NodeRef, label: &str) -> bool {
|
|
365
|
+
matches_label(self, node, label)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn relationship_types(&self) -> Vec<String> {
|
|
369
|
+
RelType::all().map(|rel| rel.name().to_string()).collect()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fn expand(&self, node: NodeRef, rel_type: &str) -> Vec<NodeRef> {
|
|
373
|
+
RelType::parse(rel_type).map_or_else(Vec::new, |rel| expand_out(self, node, rel))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn rel_sources(&self, rel_type: &str) -> Vec<NodeRef> {
|
|
377
|
+
RelType::parse(rel_type).map_or_else(Vec::new, |rel| rel_source_nodes(self, rel))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fn property(&self, node: NodeRef, prop: &str) -> CypherValue {
|
|
381
|
+
property(self, node, prop)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn label(&self, node: NodeRef) -> String {
|
|
385
|
+
node_label(self, node)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn name(&self, node: NodeRef) -> String {
|
|
389
|
+
node_name(self, node)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// Returns all nodes matching the given labels. An empty slice matches every node; otherwise a node
|
|
394
|
+
/// is returned if it matches **any** of the labels (label disjunction, e.g. `(:Class|Module)`).
|
|
395
|
+
#[must_use]
|
|
396
|
+
pub fn scan(graph: &Graph, labels: &[String]) -> Vec<NodeRef> {
|
|
397
|
+
if labels.is_empty() {
|
|
398
|
+
let mut nodes = Vec::new();
|
|
399
|
+
nodes.extend(graph.documents().keys().map(|id| NodeRef::Document(*id)));
|
|
400
|
+
nodes.extend(graph.definitions().keys().map(|id| NodeRef::Definition(*id)));
|
|
401
|
+
nodes.extend(graph.declarations().keys().map(|id| NodeRef::Declaration(*id)));
|
|
402
|
+
return nodes;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let mut seen = HashSet::new();
|
|
406
|
+
let mut nodes = Vec::new();
|
|
407
|
+
for label in labels {
|
|
408
|
+
for node in scan_label(graph, label) {
|
|
409
|
+
if seen.insert(node) {
|
|
410
|
+
nodes.push(node);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
nodes
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// Returns all nodes matching a single label.
|
|
418
|
+
fn scan_label(graph: &Graph, label: &str) -> Vec<NodeRef> {
|
|
419
|
+
match label {
|
|
420
|
+
"Document" => graph.documents().keys().map(|id| NodeRef::Document(*id)).collect(),
|
|
421
|
+
"Definition" => graph.definitions().keys().map(|id| NodeRef::Definition(*id)).collect(),
|
|
422
|
+
other => graph
|
|
423
|
+
.declarations()
|
|
424
|
+
.iter()
|
|
425
|
+
.filter(|(_, declaration)| declaration_matches_label(declaration, other))
|
|
426
|
+
.map(|(id, _)| NodeRef::Declaration(*id))
|
|
427
|
+
.collect(),
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/// Returns whether a node matches a single label.
|
|
432
|
+
#[must_use]
|
|
433
|
+
pub fn matches_label(graph: &Graph, node: NodeRef, label: &str) -> bool {
|
|
434
|
+
match node {
|
|
435
|
+
NodeRef::Document(_) => label == "Document",
|
|
436
|
+
NodeRef::Definition(_) => label == "Definition",
|
|
437
|
+
NodeRef::Declaration(id) => graph
|
|
438
|
+
.declarations()
|
|
439
|
+
.get(&id)
|
|
440
|
+
.is_some_and(|declaration| declaration_matches_label(declaration, label)),
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fn declaration_matches_label(declaration: &Declaration, label: &str) -> bool {
|
|
445
|
+
match label {
|
|
446
|
+
"Declaration" => true,
|
|
447
|
+
"Namespace" => declaration.as_namespace().is_some(),
|
|
448
|
+
other => declaration.kind() == other,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Returns the top-level label name of a node, used for display and JSON output.
|
|
453
|
+
#[must_use]
|
|
454
|
+
pub fn node_label(graph: &Graph, node: NodeRef) -> String {
|
|
455
|
+
match node {
|
|
456
|
+
NodeRef::Document(_) => "Document".to_string(),
|
|
457
|
+
NodeRef::Definition(id) => graph
|
|
458
|
+
.definitions()
|
|
459
|
+
.get(&id)
|
|
460
|
+
.map_or_else(|| "Definition".to_string(), |definition| definition.kind().to_string()),
|
|
461
|
+
NodeRef::Declaration(id) => graph.declarations().get(&id).map_or_else(
|
|
462
|
+
|| "Declaration".to_string(),
|
|
463
|
+
|declaration| declaration.kind().to_string(),
|
|
464
|
+
),
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// The primary display name of a node (FQN for declarations, URI basename for documents).
|
|
469
|
+
#[must_use]
|
|
470
|
+
pub fn node_name(graph: &Graph, node: NodeRef) -> String {
|
|
471
|
+
match node {
|
|
472
|
+
NodeRef::Declaration(id) => graph
|
|
473
|
+
.declarations()
|
|
474
|
+
.get(&id)
|
|
475
|
+
.map_or_else(String::new, |declaration| declaration.name().to_string()),
|
|
476
|
+
NodeRef::Definition(id) => graph
|
|
477
|
+
.definitions()
|
|
478
|
+
.get(&id)
|
|
479
|
+
.and_then(|definition| graph.definition_to_declaration_id(definition))
|
|
480
|
+
.and_then(|decl_id| graph.declarations().get(decl_id))
|
|
481
|
+
.map_or_else(String::new, |declaration| declaration.name().to_string()),
|
|
482
|
+
NodeRef::Document(id) => graph.documents().get(&id).map_or_else(String::new, |document| {
|
|
483
|
+
document.file_name().unwrap_or_else(|| document.uri().to_string())
|
|
484
|
+
}),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Resolves a node property to a value, where `prop` is the property name read off the node (the
|
|
489
|
+
/// `x` in `RETURN n.x` / `WHERE n.x = ...`). Unknown properties yield `NULL`.
|
|
490
|
+
#[must_use]
|
|
491
|
+
pub fn property(graph: &Graph, node: NodeRef, prop: &str) -> CypherValue {
|
|
492
|
+
match prop {
|
|
493
|
+
"label" | "kind" => CypherValue::Str(node_label(graph, node)),
|
|
494
|
+
_ => match node {
|
|
495
|
+
NodeRef::Declaration(id) => declaration_property(graph, id, prop),
|
|
496
|
+
NodeRef::Definition(id) => definition_property(graph, id, prop),
|
|
497
|
+
NodeRef::Document(id) => document_property(graph, id, prop),
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn declaration_property(graph: &Graph, id: DeclarationId, prop: &str) -> CypherValue {
|
|
503
|
+
let Some(declaration) = graph.declarations().get(&id) else {
|
|
504
|
+
return CypherValue::Null;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
match prop {
|
|
508
|
+
"name" => CypherValue::Str(declaration.name().to_string()),
|
|
509
|
+
"unqualified_name" => CypherValue::Str(declaration.unqualified_name()),
|
|
510
|
+
"visibility" => graph
|
|
511
|
+
.visibility(&id)
|
|
512
|
+
.map_or(CypherValue::Null, |visibility| CypherValue::Str(visibility.to_string())),
|
|
513
|
+
"definition_count" => CypherValue::Int(i64::try_from(declaration.definitions().len()).unwrap_or(i64::MAX)),
|
|
514
|
+
_ => CypherValue::Null,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fn definition_property(graph: &Graph, id: DefinitionId, prop: &str) -> CypherValue {
|
|
519
|
+
let Some(definition) = graph.definitions().get(&id) else {
|
|
520
|
+
return CypherValue::Null;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
match prop {
|
|
524
|
+
"name" => CypherValue::Str(node_name(graph, NodeRef::Definition(id))),
|
|
525
|
+
"file" => graph
|
|
526
|
+
.documents()
|
|
527
|
+
.get(definition.uri_id())
|
|
528
|
+
.map_or(CypherValue::Null, |document| {
|
|
529
|
+
CypherValue::Str(document.uri().to_string())
|
|
530
|
+
}),
|
|
531
|
+
"line" => graph
|
|
532
|
+
.documents()
|
|
533
|
+
.get(definition.uri_id())
|
|
534
|
+
.map_or(CypherValue::Null, |document| {
|
|
535
|
+
let location = definition.offset().to_location(document).to_presentation();
|
|
536
|
+
CypherValue::Int(i64::from(location.start_line()))
|
|
537
|
+
}),
|
|
538
|
+
_ => CypherValue::Null,
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fn document_property(graph: &Graph, id: UriId, prop: &str) -> CypherValue {
|
|
543
|
+
let Some(document) = graph.documents().get(&id) else {
|
|
544
|
+
return CypherValue::Null;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Non-`file://` URIs (the synthetic built-in document) have no file path, so `path`/`name` fall
|
|
548
|
+
// back to the raw URI.
|
|
549
|
+
match prop {
|
|
550
|
+
// Full document URI, e.g. `file:///app/models/user.rb`.
|
|
551
|
+
"uri" => CypherValue::Str(document.uri().to_string()),
|
|
552
|
+
// File-system path, e.g. `/app/models/user.rb`.
|
|
553
|
+
"path" => CypherValue::Str(document.file_path().map_or_else(
|
|
554
|
+
|| document.uri().to_string(),
|
|
555
|
+
|path| path.to_string_lossy().into_owned(),
|
|
556
|
+
)),
|
|
557
|
+
// Base file name, e.g. `user.rb`.
|
|
558
|
+
"name" => CypherValue::Str(document.file_name().unwrap_or_else(|| document.uri().to_string())),
|
|
559
|
+
_ => CypherValue::Null,
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/// Returns the candidate source nodes for a relationship type, used to build reverse adjacency.
|
|
564
|
+
#[must_use]
|
|
565
|
+
pub fn rel_source_nodes(graph: &Graph, rel: RelType) -> Vec<NodeRef> {
|
|
566
|
+
match rel {
|
|
567
|
+
RelType::Defines | RelType::References => graph.documents().keys().map(|id| NodeRef::Document(*id)).collect(),
|
|
568
|
+
RelType::Declares | RelType::Contains => {
|
|
569
|
+
graph.definitions().keys().map(|id| NodeRef::Definition(*id)).collect()
|
|
570
|
+
}
|
|
571
|
+
RelType::HasParent
|
|
572
|
+
| RelType::Includes
|
|
573
|
+
| RelType::Prepends
|
|
574
|
+
| RelType::Extends
|
|
575
|
+
| RelType::Owns
|
|
576
|
+
| RelType::HasAncestor
|
|
577
|
+
| RelType::HasDescendant => graph
|
|
578
|
+
.declarations()
|
|
579
|
+
.keys()
|
|
580
|
+
.map(|id| NodeRef::Declaration(*id))
|
|
581
|
+
.collect(),
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/// Expands the outgoing edges of `node` for the given relationship type.
|
|
586
|
+
#[must_use]
|
|
587
|
+
pub fn expand_out(graph: &Graph, node: NodeRef, rel: RelType) -> Vec<NodeRef> {
|
|
588
|
+
match (node, rel) {
|
|
589
|
+
(NodeRef::Document(uri_id), RelType::Defines) => graph
|
|
590
|
+
.documents()
|
|
591
|
+
.get(&uri_id)
|
|
592
|
+
.map(|document| {
|
|
593
|
+
document
|
|
594
|
+
.definitions()
|
|
595
|
+
.iter()
|
|
596
|
+
.map(|id| NodeRef::Definition(*id))
|
|
597
|
+
.collect()
|
|
598
|
+
})
|
|
599
|
+
.unwrap_or_default(),
|
|
600
|
+
(NodeRef::Document(uri_id), RelType::References) => document_references(graph, uri_id),
|
|
601
|
+
(NodeRef::Definition(def_id), RelType::Declares) => graph
|
|
602
|
+
.definitions()
|
|
603
|
+
.get(&def_id)
|
|
604
|
+
.and_then(|definition| graph.definition_to_declaration_id(definition))
|
|
605
|
+
.map(|decl_id| vec![NodeRef::Declaration(*decl_id)])
|
|
606
|
+
.unwrap_or_default(),
|
|
607
|
+
(NodeRef::Definition(def_id), RelType::Contains) => definition_children(graph, def_id),
|
|
608
|
+
(NodeRef::Declaration(decl_id), RelType::HasParent) => superclasses(graph, decl_id),
|
|
609
|
+
(NodeRef::Declaration(decl_id), RelType::Includes) => mixin_targets(graph, decl_id, MixinKind::Include),
|
|
610
|
+
(NodeRef::Declaration(decl_id), RelType::Prepends) => mixin_targets(graph, decl_id, MixinKind::Prepend),
|
|
611
|
+
(NodeRef::Declaration(decl_id), RelType::Extends) => mixin_targets(graph, decl_id, MixinKind::Extend),
|
|
612
|
+
(NodeRef::Declaration(decl_id), RelType::Owns) => members(graph, decl_id),
|
|
613
|
+
(NodeRef::Declaration(decl_id), RelType::HasAncestor) => ancestors(graph, decl_id),
|
|
614
|
+
(NodeRef::Declaration(decl_id), RelType::HasDescendant) => descendants(graph, decl_id),
|
|
615
|
+
_ => Vec::new(),
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
fn document_references(graph: &Graph, uri_id: UriId) -> Vec<NodeRef> {
|
|
620
|
+
let Some(document) = graph.documents().get(&uri_id) else {
|
|
621
|
+
return Vec::new();
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
let mut seen = HashSet::new();
|
|
625
|
+
let mut targets = Vec::new();
|
|
626
|
+
for ref_id in document.constant_references() {
|
|
627
|
+
if let Some(decl_id) = resolve_ref(graph, *ref_id)
|
|
628
|
+
&& seen.insert(decl_id)
|
|
629
|
+
{
|
|
630
|
+
targets.push(NodeRef::Declaration(decl_id));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
targets
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
fn definition_children(graph: &Graph, def_id: DefinitionId) -> Vec<NodeRef> {
|
|
637
|
+
let Some(definition) = graph.definitions().get(&def_id) else {
|
|
638
|
+
return Vec::new();
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
let children: &[DefinitionId] = match definition {
|
|
642
|
+
Definition::Class(d) => d.members(),
|
|
643
|
+
Definition::Module(d) => d.members(),
|
|
644
|
+
Definition::SingletonClass(d) => d.members(),
|
|
645
|
+
_ => &[],
|
|
646
|
+
};
|
|
647
|
+
children.iter().map(|id| NodeRef::Definition(*id)).collect()
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fn superclasses(graph: &Graph, decl_id: DeclarationId) -> Vec<NodeRef> {
|
|
651
|
+
let Some(declaration) = graph.declarations().get(&decl_id) else {
|
|
652
|
+
return Vec::new();
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
let mut seen = HashSet::new();
|
|
656
|
+
let mut targets = Vec::new();
|
|
657
|
+
for definition_id in declaration.definitions() {
|
|
658
|
+
if let Some(Definition::Class(class_def)) = graph.definitions().get(definition_id)
|
|
659
|
+
&& let Some(superclass_ref) = class_def.superclass_ref()
|
|
660
|
+
&& let Some(target) = resolve_ref_to_namespace(graph, *superclass_ref)
|
|
661
|
+
&& seen.insert(target)
|
|
662
|
+
{
|
|
663
|
+
targets.push(NodeRef::Declaration(target));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
targets
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#[derive(Clone, Copy)]
|
|
670
|
+
enum MixinKind {
|
|
671
|
+
Include,
|
|
672
|
+
Prepend,
|
|
673
|
+
Extend,
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn mixin_targets(graph: &Graph, decl_id: DeclarationId, kind: MixinKind) -> Vec<NodeRef> {
|
|
677
|
+
let Some(declaration) = graph.declarations().get(&decl_id) else {
|
|
678
|
+
return Vec::new();
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
let mut seen = HashSet::new();
|
|
682
|
+
let mut targets = Vec::new();
|
|
683
|
+
for definition_id in declaration.definitions() {
|
|
684
|
+
let mixins: &[Mixin] = match graph.definitions().get(definition_id) {
|
|
685
|
+
Some(Definition::Class(d)) => d.mixins(),
|
|
686
|
+
Some(Definition::Module(d)) => d.mixins(),
|
|
687
|
+
Some(Definition::SingletonClass(d)) => d.mixins(),
|
|
688
|
+
_ => &[],
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
for mixin in mixins {
|
|
692
|
+
let matches = matches!(
|
|
693
|
+
(kind, mixin),
|
|
694
|
+
(MixinKind::Include, Mixin::Include(_))
|
|
695
|
+
| (MixinKind::Prepend, Mixin::Prepend(_))
|
|
696
|
+
| (MixinKind::Extend, Mixin::Extend(_))
|
|
697
|
+
);
|
|
698
|
+
if matches
|
|
699
|
+
&& let Some(target) = resolve_ref_to_namespace(graph, *mixin.constant_reference_id())
|
|
700
|
+
&& seen.insert(target)
|
|
701
|
+
{
|
|
702
|
+
targets.push(NodeRef::Declaration(target));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
targets
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
fn members(graph: &Graph, decl_id: DeclarationId) -> Vec<NodeRef> {
|
|
710
|
+
graph
|
|
711
|
+
.declarations()
|
|
712
|
+
.get(&decl_id)
|
|
713
|
+
.and_then(Declaration::as_namespace)
|
|
714
|
+
.map(|namespace| {
|
|
715
|
+
namespace
|
|
716
|
+
.members()
|
|
717
|
+
.values()
|
|
718
|
+
.map(|id| NodeRef::Declaration(*id))
|
|
719
|
+
.collect()
|
|
720
|
+
})
|
|
721
|
+
.unwrap_or_default()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fn ancestors(graph: &Graph, decl_id: DeclarationId) -> Vec<NodeRef> {
|
|
725
|
+
use crate::model::declaration::Ancestor;
|
|
726
|
+
|
|
727
|
+
graph
|
|
728
|
+
.declarations()
|
|
729
|
+
.get(&decl_id)
|
|
730
|
+
.and_then(Declaration::as_namespace)
|
|
731
|
+
.map(|namespace| {
|
|
732
|
+
namespace
|
|
733
|
+
.ancestors()
|
|
734
|
+
.iter()
|
|
735
|
+
.filter_map(|ancestor| match ancestor {
|
|
736
|
+
Ancestor::Complete(id) if *id != decl_id => Some(NodeRef::Declaration(*id)),
|
|
737
|
+
_ => None,
|
|
738
|
+
})
|
|
739
|
+
.collect()
|
|
740
|
+
})
|
|
741
|
+
.unwrap_or_default()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
fn descendants(graph: &Graph, decl_id: DeclarationId) -> Vec<NodeRef> {
|
|
745
|
+
graph
|
|
746
|
+
.declarations()
|
|
747
|
+
.get(&decl_id)
|
|
748
|
+
.and_then(Declaration::as_namespace)
|
|
749
|
+
.map(|namespace| {
|
|
750
|
+
namespace
|
|
751
|
+
.descendants()
|
|
752
|
+
.iter()
|
|
753
|
+
.map(|id| NodeRef::Declaration(*id))
|
|
754
|
+
.collect()
|
|
755
|
+
})
|
|
756
|
+
.unwrap_or_default()
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/// Resolves a constant reference to the declaration of the name it points to.
|
|
760
|
+
fn resolve_ref(graph: &Graph, ref_id: ConstantReferenceId) -> Option<DeclarationId> {
|
|
761
|
+
let constant_ref = graph.constant_references().get(&ref_id)?;
|
|
762
|
+
graph.name_id_to_declaration_id(*constant_ref.name_id()).copied()
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/// Resolves a constant reference to a namespace declaration, following constant aliases.
|
|
766
|
+
fn resolve_ref_to_namespace(graph: &Graph, ref_id: ConstantReferenceId) -> Option<DeclarationId> {
|
|
767
|
+
resolve_to_namespace(graph, resolve_ref(graph, ref_id)?)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// Walks constant-alias chains until reaching a namespace declaration.
|
|
771
|
+
fn resolve_to_namespace(graph: &Graph, declaration_id: DeclarationId) -> Option<DeclarationId> {
|
|
772
|
+
let mut queue = VecDeque::from([declaration_id]);
|
|
773
|
+
let mut seen = HashSet::new();
|
|
774
|
+
|
|
775
|
+
while let Some(current_id) = queue.pop_front() {
|
|
776
|
+
if !seen.insert(current_id) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
match graph.declarations().get(¤t_id)? {
|
|
781
|
+
Declaration::Namespace(_) => return Some(current_id),
|
|
782
|
+
Declaration::ConstantAlias(_) => {
|
|
783
|
+
queue.extend(graph.alias_targets(¤t_id)?);
|
|
784
|
+
}
|
|
785
|
+
_ => {}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
None
|
|
790
|
+
}
|