rubydex 0.2.4-aarch64-linux → 0.2.6-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +6 -6
  4. data/exe/rubydex_mcp +17 -0
  5. data/ext/rubydex/definition.c +89 -2
  6. data/ext/rubydex/document.c +36 -0
  7. data/ext/rubydex/extconf.rb +8 -0
  8. data/ext/rubydex/graph.c +32 -18
  9. data/ext/rubydex/handle.h +21 -5
  10. data/lib/rubydex/3.2/rubydex.so +0 -0
  11. data/lib/rubydex/3.3/rubydex.so +0 -0
  12. data/lib/rubydex/3.4/rubydex.so +0 -0
  13. data/lib/rubydex/4.0/rubydex.so +0 -0
  14. data/lib/rubydex/bin/rubydex_mcp +0 -0
  15. data/lib/rubydex/declaration.rb +3 -3
  16. data/lib/rubydex/errors.rb +8 -0
  17. data/lib/rubydex/graph.rb +3 -1
  18. data/lib/rubydex/librubydex_sys.so +0 -0
  19. data/lib/rubydex/location.rb +24 -0
  20. data/lib/rubydex/version.rb +1 -1
  21. data/lib/rubydex.rb +1 -0
  22. data/rbi/rubydex.rbi +37 -14
  23. data/rust/Cargo.lock +3 -3
  24. data/rust/rubydex/Cargo.toml +7 -1
  25. data/rust/rubydex/src/dot.rs +609 -0
  26. data/rust/rubydex/src/indexing/local_graph.rs +38 -0
  27. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  28. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -0
  29. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +5 -1
  30. data/rust/rubydex/src/indexing.rs +38 -12
  31. data/rust/rubydex/src/lib.rs +2 -1
  32. data/rust/rubydex/src/listing.rs +14 -3
  33. data/rust/rubydex/src/main.rs +35 -7
  34. data/rust/rubydex/src/model/built_in.rs +5 -2
  35. data/rust/rubydex/src/model/comment.rs +2 -0
  36. data/rust/rubydex/src/model/declaration.rs +1 -0
  37. data/rust/rubydex/src/model/definitions.rs +20 -19
  38. data/rust/rubydex/src/model/document.rs +2 -0
  39. data/rust/rubydex/src/model/encoding.rs +2 -0
  40. data/rust/rubydex/src/model/graph.rs +51 -13
  41. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  42. data/rust/rubydex/src/model/ids.rs +27 -1
  43. data/rust/rubydex/src/model/keywords.rs +3 -0
  44. data/rust/rubydex/src/model/name.rs +2 -0
  45. data/rust/rubydex/src/model/string_ref.rs +2 -0
  46. data/rust/rubydex/src/model/visibility.rs +3 -0
  47. data/rust/rubydex/src/operation/applier.rs +520 -0
  48. data/rust/rubydex/src/operation/mod.rs +285 -0
  49. data/rust/rubydex/src/operation/printer.rs +260 -0
  50. data/rust/rubydex/src/operation/ruby_builder.rs +2919 -0
  51. data/rust/rubydex/src/query.rs +114 -33
  52. data/rust/rubydex/src/resolution.rs +22 -9
  53. data/rust/rubydex/src/resolution_tests.rs +349 -209
  54. data/rust/rubydex/src/test_utils/graph_test.rs +19 -4
  55. data/rust/rubydex/src/test_utils/local_graph_test.rs +7 -6
  56. data/rust/rubydex/tests/cli.rs +17 -61
  57. data/rust/rubydex-mcp/Cargo.toml +9 -3
  58. data/rust/rubydex-mcp/src/server.rs +5 -1
  59. data/rust/rubydex-sys/Cargo.toml +9 -2
  60. data/rust/rubydex-sys/src/definition_api.rs +96 -2
  61. data/rust/rubydex-sys/src/document_api.rs +28 -0
  62. data/rust/rubydex-sys/src/graph_api.rs +2 -4
  63. metadata +11 -4
  64. data/rust/rubydex/src/visualization/dot.rs +0 -192
  65. data/rust/rubydex/src/visualization.rs +0 -6
@@ -0,0 +1,609 @@
1
+ use std::collections::{HashSet, VecDeque};
2
+ use std::fmt::Write;
3
+
4
+ use crate::model::{
5
+ built_in,
6
+ declaration::Declaration,
7
+ definitions::{Definition, Mixin},
8
+ document::Document,
9
+ graph::Graph,
10
+ ids::{DeclarationId, DefinitionId, UriId},
11
+ };
12
+
13
+ const DOC_COLOR: &str = "#4a90d9";
14
+ const DOC_FILL: &str = "#dce8f5";
15
+ const DEF_COLOR: &str = "#e8912d";
16
+ const DEF_FILL: &str = "#fdf0e0";
17
+ const DECL_COLOR: &str = "#5ba55b";
18
+ const DECL_FILL: &str = "#e0f0e0";
19
+ const NESTS_COLOR: &str = "#f0c08a";
20
+ const MEMBER_COLOR: &str = "#a3d9a3";
21
+ const SUPERCLASS_COLOR: &str = "#d94a7a";
22
+ const MIXIN_COLOR: &str = "#8b5fc7";
23
+
24
+ pub struct DotBuilder<'a> {
25
+ output: String,
26
+ graph: &'a Graph,
27
+ }
28
+
29
+ impl<'a> DotBuilder<'a> {
30
+ fn new(graph: &'a Graph) -> Self {
31
+ Self {
32
+ output: String::new(),
33
+ graph,
34
+ }
35
+ }
36
+
37
+ fn graph(&self) -> &'a Graph {
38
+ self.graph
39
+ }
40
+
41
+ fn writeln(&mut self, s: &str) {
42
+ self.output.push_str(s);
43
+ self.output.push('\n');
44
+ }
45
+
46
+ fn html_escape(s: &str) -> String {
47
+ s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
48
+ }
49
+
50
+ fn label(type_name: &str, name: &str, color: &str) -> String {
51
+ let escaped = Self::html_escape(name);
52
+ format!(
53
+ concat!(
54
+ "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" align=\"center\">",
55
+ "<tr><td align=\"center\"><font point-size=\"8\" color=\"{}\">{}</font></td></tr>",
56
+ "<tr><td align=\"center\"><b>{}</b></td></tr>",
57
+ "</table>>",
58
+ ),
59
+ color, type_name, escaped,
60
+ )
61
+ }
62
+
63
+ #[must_use]
64
+ pub fn generate(graph: &'a Graph, show_builtins: bool) -> String {
65
+ let mut builder = Self::new(graph);
66
+
67
+ builder.write_header();
68
+
69
+ let documents = builder.visible_documents(show_builtins);
70
+ let def_ids = builder.write_document_nodes(&documents);
71
+ let definitions = builder.visible_definitions(&def_ids);
72
+ let visible_def_ids: HashSet<_> = definitions.iter().map(|(_, definition)| definition.id()).collect();
73
+ let decl_ids = builder.write_definition_nodes(&definitions);
74
+ let declarations = builder.visible_declarations(&decl_ids);
75
+ builder.write_declaration_nodes(&declarations);
76
+
77
+ builder.write_document_definition_edges(&documents, &visible_def_ids);
78
+ builder.write_definition_declaration_edges(&definitions);
79
+ builder.write_definition_nesting_edges(&definitions, &visible_def_ids);
80
+ builder.write_superclass_edges(&definitions, &decl_ids);
81
+ builder.write_mixin_edges(&definitions, &decl_ids);
82
+ builder.write_member_edges(&declarations, &decl_ids);
83
+
84
+ builder.writeln("}");
85
+ builder.output
86
+ }
87
+
88
+ fn write_header(&mut self) {
89
+ self.writeln("digraph rubydex {");
90
+ self.writeln(" rankdir=LR");
91
+ self.writeln(" graph [ranksep=0.30 nodesep=0.08 concentrate=true]");
92
+ self.writeln(" node [fontname=\"Courier\" fontsize=10 shape=box]");
93
+ self.writeln(" edge [fontsize=9 fontname=\"Courier\"]");
94
+ self.output.push('\n');
95
+ }
96
+
97
+ fn visible_documents(&self, show_builtins: bool) -> Vec<&'a Document> {
98
+ let mut documents: Vec<_> = self
99
+ .graph
100
+ .documents()
101
+ .values()
102
+ .filter(|d| show_builtins || d.uri() != built_in::BUILT_IN_URI)
103
+ .collect();
104
+ documents.sort_by(|a, b| a.uri().cmp(b.uri()));
105
+ documents
106
+ }
107
+
108
+ fn write_document_nodes(&mut self, documents: &[&'a Document]) -> HashSet<DefinitionId> {
109
+ let mut def_ids = HashSet::new();
110
+ for document in documents {
111
+ document.to_dot(self);
112
+ for def_id in document.definitions() {
113
+ def_ids.insert(*def_id);
114
+ }
115
+ }
116
+ self.output.push('\n');
117
+ def_ids
118
+ }
119
+
120
+ fn visible_definitions(&self, def_ids: &HashSet<DefinitionId>) -> Vec<(String, &'a Definition)> {
121
+ let mut definitions: Vec<_> = self
122
+ .graph
123
+ .definitions()
124
+ .iter()
125
+ .filter(|(id, _)| def_ids.contains(*id))
126
+ .filter_map(|(_, definition)| {
127
+ let decl_id = self.graph.definition_to_declaration_id(definition)?;
128
+ let declaration = self.graph.declarations().get(decl_id)?;
129
+ let sort_key = format!("{}({})", definition.kind(), declaration.name());
130
+ Some((sort_key, definition))
131
+ })
132
+ .collect();
133
+ definitions.sort_by(|a, b| a.0.cmp(&b.0));
134
+ definitions
135
+ }
136
+
137
+ fn write_definition_nodes(&mut self, definitions: &[(String, &'a Definition)]) -> HashSet<DeclarationId> {
138
+ let mut decl_ids = HashSet::new();
139
+ for (_, definition) in definitions {
140
+ definition.to_dot(self);
141
+ if let Some(decl_id) = self.graph.definition_to_declaration_id(definition) {
142
+ decl_ids.insert(*decl_id);
143
+ }
144
+ }
145
+ self.output.push('\n');
146
+ decl_ids
147
+ }
148
+
149
+ fn visible_declarations(&self, decl_ids: &HashSet<DeclarationId>) -> Vec<(&'a DeclarationId, &'a Declaration)> {
150
+ let mut declarations: Vec<_> = self
151
+ .graph
152
+ .declarations()
153
+ .iter()
154
+ .filter(|(id, _)| decl_ids.contains(*id))
155
+ .collect();
156
+ declarations.sort_by(|(_, a), (_, b)| a.name().cmp(b.name()));
157
+ declarations
158
+ }
159
+
160
+ fn write_declaration_nodes(&mut self, declarations: &[(&DeclarationId, &Declaration)]) {
161
+ for (_, declaration) in declarations {
162
+ declaration.to_dot(self);
163
+ }
164
+ self.output.push('\n');
165
+ }
166
+
167
+ fn write_document_definition_edges(&mut self, documents: &[&Document], def_ids: &HashSet<DefinitionId>) {
168
+ for document in documents {
169
+ let uri = document.uri();
170
+ let doc_id = Self::doc_node_id(uri);
171
+ for def_id in document.definitions() {
172
+ if def_ids.contains(def_id) {
173
+ let _ = writeln!(
174
+ self.output,
175
+ " {doc_id} -> \"def_{def_id}\" [label=\"defines\" color=\"{DEF_COLOR}\" fontcolor=\"{DEF_COLOR}\"]"
176
+ );
177
+ }
178
+ }
179
+ }
180
+ self.output.push('\n');
181
+ }
182
+
183
+ fn write_definition_declaration_edges(&mut self, definitions: &[(String, &'a Definition)]) {
184
+ for (_, definition) in definitions {
185
+ let def_id = definition.id();
186
+ if let Some(decl_id) = self.graph.definition_to_declaration_id(definition) {
187
+ let decl_node = Self::decl_node_id(*decl_id);
188
+ let _ = writeln!(
189
+ self.output,
190
+ " \"def_{def_id}\" -> {decl_node} [label=\"declares\" color=\"{DECL_COLOR}\" fontcolor=\"{DECL_COLOR}\"]"
191
+ );
192
+ }
193
+ }
194
+ self.output.push('\n');
195
+ }
196
+
197
+ fn write_definition_nesting_edges(
198
+ &mut self,
199
+ definitions: &[(String, &'a Definition)],
200
+ def_ids: &HashSet<DefinitionId>,
201
+ ) {
202
+ for (_, definition) in definitions {
203
+ let parent_id = definition.id();
204
+ let children: &[DefinitionId] = match definition {
205
+ Definition::Class(d) => d.members(),
206
+ Definition::Module(d) => d.members(),
207
+ Definition::SingletonClass(d) => d.members(),
208
+ _ => &[],
209
+ };
210
+ for child_id in children {
211
+ if def_ids.contains(child_id) {
212
+ let _ = writeln!(
213
+ self.output,
214
+ " \"def_{parent_id}\" -> \"def_{child_id}\" [label=\"contains\" style=dashed arrowhead=onormal color=\"{NESTS_COLOR}\" fontcolor=\"{NESTS_COLOR}\"]"
215
+ );
216
+ }
217
+ }
218
+ }
219
+ self.output.push('\n');
220
+ }
221
+
222
+ fn write_superclass_edges(&mut self, definitions: &[(String, &'a Definition)], decl_ids: &HashSet<DeclarationId>) {
223
+ for (_, definition) in definitions {
224
+ let Definition::Class(class_def) = definition else {
225
+ continue;
226
+ };
227
+ let Some(superclass_ref_id) = class_def.superclass_ref() else {
228
+ continue;
229
+ };
230
+ let Some(decl_id) = self.resolve_ref_to_namespace(*superclass_ref_id) else {
231
+ continue;
232
+ };
233
+ if !decl_ids.contains(&decl_id) {
234
+ continue;
235
+ }
236
+ let Some(child_decl_id) = self.graph.definition_to_declaration_id(definition) else {
237
+ continue;
238
+ };
239
+
240
+ let child_node = Self::decl_node_id(*child_decl_id);
241
+ let parent_node = Self::decl_node_id(decl_id);
242
+ let _ = writeln!(
243
+ self.output,
244
+ " {child_node} -> {parent_node} [label=\"inherits\" color=\"{SUPERCLASS_COLOR}\" fontcolor=\"{SUPERCLASS_COLOR}\"]"
245
+ );
246
+ }
247
+ self.output.push('\n');
248
+ }
249
+
250
+ fn write_mixin_edges(&mut self, definitions: &[(String, &'a Definition)], decl_ids: &HashSet<DeclarationId>) {
251
+ for (_, definition) in definitions {
252
+ let mixins: &[Mixin] = match definition {
253
+ Definition::Class(d) => d.mixins(),
254
+ Definition::Module(d) => d.mixins(),
255
+ Definition::SingletonClass(d) => d.mixins(),
256
+ _ => &[],
257
+ };
258
+ if mixins.is_empty() {
259
+ continue;
260
+ }
261
+ let Some(decl_id) = self.graph.definition_to_declaration_id(definition) else {
262
+ continue;
263
+ };
264
+ let src_node = Self::decl_node_id(*decl_id);
265
+ for mixin in mixins {
266
+ self.write_mixin_edge(mixin, &src_node, decl_ids);
267
+ }
268
+ }
269
+ self.output.push('\n');
270
+ }
271
+
272
+ fn write_mixin_edge(&mut self, mixin: &Mixin, src_node: &str, decl_ids: &HashSet<DeclarationId>) {
273
+ let mixin_label = match mixin {
274
+ Mixin::Include(_) => "includes",
275
+ Mixin::Prepend(_) => "prepends",
276
+ Mixin::Extend(_) => "extends",
277
+ };
278
+ let Some(target_decl_id) = self.resolve_ref_to_namespace(*mixin.constant_reference_id()) else {
279
+ return;
280
+ };
281
+ if !decl_ids.contains(&target_decl_id) {
282
+ return;
283
+ }
284
+ let target_node = Self::decl_node_id(target_decl_id);
285
+ let _ = writeln!(
286
+ self.output,
287
+ " {src_node} -> {target_node} [label=\"{mixin_label}\" color=\"{MIXIN_COLOR}\" fontcolor=\"{MIXIN_COLOR}\"]"
288
+ );
289
+ }
290
+
291
+ fn write_member_edges(
292
+ &mut self,
293
+ declarations: &[(&DeclarationId, &Declaration)],
294
+ decl_ids: &HashSet<DeclarationId>,
295
+ ) {
296
+ for (declaration_id, declaration) in declarations {
297
+ if let Some(namespace) = declaration.as_namespace() {
298
+ let owner_node = Self::decl_node_id(**declaration_id);
299
+ let mut members: Vec<_> = namespace
300
+ .members()
301
+ .values()
302
+ .filter(|id| decl_ids.contains(*id))
303
+ .collect();
304
+ members.sort();
305
+ for member_id in members {
306
+ let member_node = Self::decl_node_id(*member_id);
307
+ let _ = writeln!(
308
+ self.output,
309
+ " {owner_node} -> {member_node} [label=\"owns\" style=dashed arrowhead=onormal color=\"{MEMBER_COLOR}\" fontcolor=\"{MEMBER_COLOR}\"]"
310
+ );
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ fn resolve_ref(&self, ref_id: crate::model::ids::ConstantReferenceId) -> Option<&'a DeclarationId> {
317
+ let constant_ref = self.graph.constant_references().get(&ref_id)?;
318
+ self.graph.name_id_to_declaration_id(*constant_ref.name_id())
319
+ }
320
+
321
+ fn resolve_ref_to_namespace(&self, ref_id: crate::model::ids::ConstantReferenceId) -> Option<DeclarationId> {
322
+ self.resolve_to_namespace(*self.resolve_ref(ref_id)?)
323
+ }
324
+
325
+ fn resolve_to_namespace(&self, declaration_id: DeclarationId) -> Option<DeclarationId> {
326
+ let mut queue = VecDeque::from([declaration_id]);
327
+ let mut seen = HashSet::new();
328
+
329
+ while let Some(current_id) = queue.pop_front() {
330
+ if !seen.insert(current_id) {
331
+ continue;
332
+ }
333
+
334
+ match self.graph.declarations().get(&current_id)? {
335
+ Declaration::Namespace(_) => return Some(current_id),
336
+ Declaration::ConstantAlias(_) => {
337
+ queue.extend(self.graph.alias_targets(&current_id)?);
338
+ }
339
+ _ => {}
340
+ }
341
+ }
342
+
343
+ None
344
+ }
345
+
346
+ fn doc_node_id(uri: &str) -> String {
347
+ format!("\"doc_{}\"", UriId::from(uri))
348
+ }
349
+
350
+ fn decl_node_id(id: DeclarationId) -> String {
351
+ format!("\"decl_{id}\"")
352
+ }
353
+ }
354
+
355
+ pub trait ToDot {
356
+ fn to_dot(&self, builder: &mut DotBuilder);
357
+ }
358
+
359
+ impl ToDot for Document {
360
+ fn to_dot(&self, builder: &mut DotBuilder) {
361
+ let uri = self.uri();
362
+ let label = uri.rsplit('/').next().unwrap_or(uri);
363
+ let node_id = DotBuilder::doc_node_id(uri);
364
+ let html_label = DotBuilder::label("Document", label, DOC_COLOR);
365
+ let _ = writeln!(
366
+ builder.output,
367
+ " {node_id} [label={html_label} shape=note color=\"{DOC_COLOR}\" fillcolor=\"{DOC_FILL}\" style=filled]"
368
+ );
369
+ }
370
+ }
371
+
372
+ impl ToDot for Definition {
373
+ fn to_dot(&self, builder: &mut DotBuilder) {
374
+ let def_id = self.id();
375
+ let Some(decl_id) = builder.graph().definition_to_declaration_id(self) else {
376
+ return;
377
+ };
378
+ let Some(declaration) = builder.graph().declarations().get(decl_id) else {
379
+ return;
380
+ };
381
+
382
+ let type_label = format!("{}Def", self.kind());
383
+ let html_label = DotBuilder::label(&type_label, declaration.name(), DEF_COLOR);
384
+ let _ = writeln!(
385
+ builder.output,
386
+ " \"def_{def_id}\" [label={html_label} style=rounded color=\"{DEF_COLOR}\" fillcolor=\"{DEF_FILL}\" style=\"rounded,filled\"]"
387
+ );
388
+ }
389
+ }
390
+
391
+ impl ToDot for Declaration {
392
+ fn to_dot(&self, builder: &mut DotBuilder) {
393
+ let type_label = format!("{}Decl", self.kind());
394
+ let declaration_id = DeclarationId::from(self.name());
395
+ let node_id = DotBuilder::decl_node_id(declaration_id);
396
+ let html_label = DotBuilder::label(&type_label, self.name(), DECL_COLOR);
397
+ let _ = writeln!(
398
+ builder.output,
399
+ " {node_id} [label={html_label} color=\"{DECL_COLOR}\" fillcolor=\"{DECL_FILL}\" style=filled]"
400
+ );
401
+ }
402
+ }
403
+
404
+ #[cfg(test)]
405
+ mod tests {
406
+ use super::*;
407
+ use crate::model::ids::DeclarationId;
408
+ use crate::test_utils::GraphTest;
409
+
410
+ #[test]
411
+ fn test_dot_generation() {
412
+ let mut context = GraphTest::new();
413
+ context.index_uri(
414
+ "file:///test.rb",
415
+ "
416
+ class TestClass
417
+ end
418
+
419
+ module TestModule
420
+ end
421
+ ",
422
+ );
423
+ context.resolve();
424
+ let dot_output = DotBuilder::generate(context.graph(), true);
425
+
426
+ assert!(dot_output.contains("digraph rubydex"));
427
+ assert!(dot_output.contains(" rankdir=LR"));
428
+ assert!(dot_output.contains(" graph [ranksep=0.30 nodesep=0.08 concentrate=true]"));
429
+
430
+ // Document nodes
431
+ assert!(dot_output.contains("Document"));
432
+ assert!(dot_output.contains("test.rb"));
433
+
434
+ // Definition nodes
435
+ assert!(dot_output.contains("ClassDef"));
436
+ assert!(dot_output.contains("ModuleDef"));
437
+
438
+ // Declaration nodes
439
+ assert!(dot_output.contains("ClassDecl"));
440
+ assert!(dot_output.contains("ModuleDecl"));
441
+
442
+ // Edges
443
+ assert!(dot_output.contains("defines"));
444
+ assert!(dot_output.contains("declares"));
445
+ assert!(dot_output.contains("owns"));
446
+ }
447
+
448
+ #[test]
449
+ fn test_dot_nesting_edges() {
450
+ let mut context = GraphTest::new();
451
+ context.index_uri(
452
+ "file:///test.rb",
453
+ "
454
+ module Outer
455
+ class Inner
456
+ end
457
+ end
458
+ ",
459
+ );
460
+ context.resolve();
461
+ let dot_output = DotBuilder::generate(context.graph(), false);
462
+ assert!(dot_output.contains("contains"));
463
+ }
464
+
465
+ #[test]
466
+ fn test_dot_superclass_edges() {
467
+ let mut context = GraphTest::new();
468
+ context.index_uri(
469
+ "file:///test.rb",
470
+ "
471
+ class Parent
472
+ end
473
+
474
+ class Child < Parent
475
+ end
476
+ ",
477
+ );
478
+ context.resolve();
479
+ let dot_output = DotBuilder::generate(context.graph(), false);
480
+ assert!(dot_output.contains("inherits"));
481
+ }
482
+
483
+ #[test]
484
+ fn test_dot_superclass_edge_resolves_alias_target() {
485
+ let mut context = GraphTest::new();
486
+ context.index_uri(
487
+ "file:///test.rb",
488
+ "
489
+ class Base
490
+ end
491
+
492
+ AliasedBase = Base
493
+
494
+ class Child < AliasedBase
495
+ end
496
+ ",
497
+ );
498
+ context.resolve();
499
+ let dot_output = DotBuilder::generate(context.graph(), false);
500
+
501
+ let child_node = format!("\"decl_{}\"", DeclarationId::from("Child"));
502
+ let base_node = format!("\"decl_{}\"", DeclarationId::from("Base"));
503
+ let alias_node = format!("\"decl_{}\"", DeclarationId::from("AliasedBase"));
504
+
505
+ assert!(dot_output.contains(&format!("{child_node} -> {base_node} [label=\"inherits\"")));
506
+ assert!(!dot_output.contains(&format!("{child_node} -> {alias_node} [label=\"inherits\"")));
507
+ }
508
+
509
+ #[test]
510
+ fn test_dot_mixin_edges() {
511
+ let mut context = GraphTest::new();
512
+ context.index_uri(
513
+ "file:///test.rb",
514
+ "
515
+ module Mixin
516
+ end
517
+
518
+ class Klass
519
+ include Mixin
520
+ end
521
+ ",
522
+ );
523
+ context.resolve();
524
+ let dot_output = DotBuilder::generate(context.graph(), false);
525
+ assert!(dot_output.contains("includes"));
526
+ }
527
+
528
+ #[test]
529
+ fn test_dot_mixin_edge_resolves_alias_target() {
530
+ let mut context = GraphTest::new();
531
+ context.index_uri(
532
+ "file:///test.rb",
533
+ "
534
+ module Mixin
535
+ end
536
+
537
+ AliasMixin = Mixin
538
+
539
+ class Klass
540
+ include AliasMixin
541
+ end
542
+ ",
543
+ );
544
+ context.resolve();
545
+ let dot_output = DotBuilder::generate(context.graph(), false);
546
+
547
+ let klass_node = format!("\"decl_{}\"", DeclarationId::from("Klass"));
548
+ let mixin_node = format!("\"decl_{}\"", DeclarationId::from("Mixin"));
549
+ let alias_node = format!("\"decl_{}\"", DeclarationId::from("AliasMixin"));
550
+
551
+ assert!(dot_output.contains(&format!("{klass_node} -> {mixin_node} [label=\"includes\"")));
552
+ assert!(!dot_output.contains(&format!("{klass_node} -> {alias_node} [label=\"includes\"")));
553
+ }
554
+
555
+ #[test]
556
+ fn test_dot_declaration_node_ids_do_not_collapse_similar_names() {
557
+ let mut context = GraphTest::new();
558
+ context.index_uri(
559
+ "file:///test.rb",
560
+ "
561
+ module A
562
+ class B
563
+ end
564
+ end
565
+
566
+ class A__B
567
+ end
568
+ ",
569
+ );
570
+ context.resolve();
571
+ let dot_output = DotBuilder::generate(context.graph(), false);
572
+
573
+ let nested_node = format!("\"decl_{}\"", DeclarationId::from("A::B"));
574
+ let underscored_node = format!("\"decl_{}\"", DeclarationId::from("A__B"));
575
+
576
+ assert_ne!(nested_node, underscored_node);
577
+ assert!(dot_output.contains(&format!("{nested_node} [")));
578
+ assert!(dot_output.contains(&format!("{underscored_node} [")));
579
+ assert!(!dot_output.contains("\"decl_A__B\" ["));
580
+ }
581
+
582
+ #[test]
583
+ fn test_dot_does_not_emit_document_edges_to_hidden_definition_nodes() {
584
+ let mut context = GraphTest::new();
585
+ context.index_uri("file:///test.rb", "def Missing.foo; end");
586
+ context.resolve();
587
+ let dot_output = DotBuilder::generate(context.graph(), false);
588
+
589
+ assert!(!dot_output.contains("[label=\"defines\""));
590
+ }
591
+
592
+ #[test]
593
+ fn test_dot_reopened_builtin_not_hidden() {
594
+ let mut context = GraphTest::new();
595
+ context.index_uri(
596
+ "file:///test.rb",
597
+ "
598
+ class Object
599
+ def test; end
600
+ end
601
+ ",
602
+ );
603
+ context.resolve();
604
+ let dot_output = DotBuilder::generate(context.graph(), false);
605
+
606
+ assert!(dot_output.contains("ClassDecl"));
607
+ assert!(dot_output.contains("Object"));
608
+ }
609
+ }
@@ -206,6 +206,44 @@ impl LocalGraph {
206
206
  &self.name_dependents
207
207
  }
208
208
 
209
+ /// Creates a `LocalGraph` from pre-built parts (used by the operation applier pipeline).
210
+ #[must_use]
211
+ pub fn from_parts(
212
+ uri_id: UriId,
213
+ document: Document,
214
+ strings: IdentityHashMap<StringId, StringRef>,
215
+ names: IdentityHashMap<NameId, NameRef>,
216
+ ) -> Self {
217
+ let mut name_dependents: IdentityHashMap<NameId, Vec<NameDependent>> = IdentityHashMap::default();
218
+ for (name_id, name_ref) in &names {
219
+ if let NameRef::Unresolved(name) = name_ref {
220
+ if let Some(&parent_scope) = name.parent_scope().as_ref() {
221
+ name_dependents
222
+ .entry(parent_scope)
223
+ .or_default()
224
+ .push(NameDependent::ChildName(*name_id));
225
+ }
226
+ if let Some(&nesting_id) = name.nesting().as_ref() {
227
+ name_dependents
228
+ .entry(nesting_id)
229
+ .or_default()
230
+ .push(NameDependent::NestedName(*name_id));
231
+ }
232
+ }
233
+ }
234
+
235
+ Self {
236
+ uri_id,
237
+ document,
238
+ definitions: IdentityHashMap::default(),
239
+ strings,
240
+ names,
241
+ constant_references: IdentityHashMap::default(),
242
+ method_references: IdentityHashMap::default(),
243
+ name_dependents,
244
+ }
245
+ }
246
+
209
247
  // Into parts
210
248
 
211
249
  #[must_use]