rubydex 0.2.5 → 0.2.6

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +6 -6
  4. data/ext/rubydex/definition.c +33 -2
  5. data/ext/rubydex/document.c +36 -0
  6. data/ext/rubydex/graph.c +32 -18
  7. data/ext/rubydex/handle.h +21 -5
  8. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  9. data/lib/rubydex/errors.rb +8 -0
  10. data/lib/rubydex/location.rb +24 -0
  11. data/lib/rubydex/version.rb +1 -1
  12. data/lib/rubydex.rb +1 -0
  13. data/rbi/rubydex.rbi +29 -12
  14. data/rust/Cargo.lock +3 -3
  15. data/rust/rubydex/Cargo.toml +7 -1
  16. data/rust/rubydex/src/dot.rs +609 -0
  17. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  18. data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
  19. data/rust/rubydex/src/lib.rs +1 -1
  20. data/rust/rubydex/src/main.rs +8 -5
  21. data/rust/rubydex/src/model/built_in.rs +5 -2
  22. data/rust/rubydex/src/model/comment.rs +2 -0
  23. data/rust/rubydex/src/model/declaration.rs +1 -0
  24. data/rust/rubydex/src/model/definitions.rs +13 -1
  25. data/rust/rubydex/src/model/document.rs +2 -0
  26. data/rust/rubydex/src/model/encoding.rs +2 -0
  27. data/rust/rubydex/src/model/graph.rs +51 -13
  28. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  29. data/rust/rubydex/src/model/keywords.rs +3 -0
  30. data/rust/rubydex/src/model/name.rs +2 -0
  31. data/rust/rubydex/src/model/string_ref.rs +2 -0
  32. data/rust/rubydex/src/model/visibility.rs +3 -0
  33. data/rust/rubydex/src/operation/applier.rs +1 -0
  34. data/rust/rubydex/src/operation/mod.rs +1 -0
  35. data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
  36. data/rust/rubydex/src/query.rs +114 -33
  37. data/rust/rubydex/src/resolution.rs +16 -8
  38. data/rust/rubydex/src/resolution_tests.rs +132 -0
  39. data/rust/rubydex/tests/cli.rs +17 -61
  40. data/rust/rubydex-mcp/Cargo.toml +9 -3
  41. data/rust/rubydex-sys/Cargo.toml +9 -2
  42. data/rust/rubydex-sys/src/definition_api.rs +72 -2
  43. data/rust/rubydex-sys/src/document_api.rs +28 -0
  44. data/rust/rubydex-sys/src/graph_api.rs +1 -3
  45. metadata +4 -4
  46. data/rust/rubydex/src/visualization/dot.rs +0 -192
  47. 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
+ }
@@ -338,6 +338,7 @@ impl<'a> RBSIndexer<'a> {
338
338
  &mut self,
339
339
  str_id: StringId,
340
340
  offset: Offset,
341
+ name_offset: Offset,
341
342
  comments: Box<[Comment]>,
342
343
  flags: DefinitionFlags,
343
344
  lexical_nesting_id: Option<DefinitionId>,
@@ -347,6 +348,7 @@ impl<'a> RBSIndexer<'a> {
347
348
  str_id,
348
349
  self.uri_id,
349
350
  offset.clone(),
351
+ name_offset.clone(),
350
352
  comments.clone(),
351
353
  flags.clone(),
352
354
  lexical_nesting_id,
@@ -360,6 +362,7 @@ impl<'a> RBSIndexer<'a> {
360
362
  str_id,
361
363
  self.uri_id,
362
364
  offset,
365
+ name_offset,
363
366
  comments,
364
367
  flags,
365
368
  lexical_nesting_id,
@@ -562,13 +565,22 @@ impl Visit for RBSIndexer<'_> {
562
565
  fn visit_method_definition_node(&mut self, def_node: &node::MethodDefinitionNode) {
563
566
  let str_id = self.local_graph.intern_string(format!("{}()", def_node.name()));
564
567
  let offset = Offset::from_rbs_location(&def_node.location());
568
+ let name_offset = Offset::from_rbs_location(&def_node.name_location());
565
569
  let comments = self.collect_comments(def_node.comment());
566
570
  let flags = Self::flags(&def_node.annotations());
567
571
  let lexical_nesting_id = self.parent_lexical_scope_id();
568
572
  let signatures = self.collect_overload_signatures(def_node);
569
573
 
570
574
  if def_node.kind() == node::MethodDefinitionKind::SingletonInstance {
571
- self.register_singleton_instance_method(str_id, offset, comments, flags, lexical_nesting_id, signatures);
575
+ self.register_singleton_instance_method(
576
+ str_id,
577
+ offset,
578
+ name_offset,
579
+ comments,
580
+ flags,
581
+ lexical_nesting_id,
582
+ signatures,
583
+ );
572
584
  return;
573
585
  }
574
586
 
@@ -602,6 +614,7 @@ impl Visit for RBSIndexer<'_> {
602
614
  str_id,
603
615
  self.uri_id,
604
616
  offset,
617
+ name_offset,
605
618
  comments,
606
619
  flags,
607
620
  lexical_nesting_id,
@@ -1197,6 +1210,7 @@ mod tests {
1197
1210
 
1198
1211
  assert_definition_at!(&context, "2:3-2:22", Method, |def| {
1199
1212
  assert_def_str_eq!(&context, def, "foo()");
1213
+ assert_def_name_offset_eq!(&context, def, "2:7-2:10");
1200
1214
  assert!(def.receiver().is_none());
1201
1215
  assert_eq!(def.visibility(), &Visibility::Public);
1202
1216
  assert_eq!(class_def.id(), def.lexical_nesting_id().unwrap());
@@ -1205,6 +1219,7 @@ mod tests {
1205
1219
 
1206
1220
  assert_definition_at!(&context, "4:3-4:23", Method, |def| {
1207
1221
  assert_def_str_eq!(&context, def, "bar()");
1222
+ assert_def_name_offset_eq!(&context, def, "4:7-4:10");
1208
1223
  assert!(def.receiver().is_none());
1209
1224
  assert_eq!(def.visibility(), &Visibility::Public);
1210
1225
  assert_eq!(class_def.id(), def.lexical_nesting_id().unwrap());
@@ -1398,6 +1413,7 @@ mod tests {
1398
1413
  panic!()
1399
1414
  };
1400
1415
  assert_def_str_eq!(&context, instance_method, "foo()");
1416
+ assert_def_name_offset_eq!(&context, instance_method, "2:13-2:16");
1401
1417
  assert_eq!(instance_method.visibility(), &Visibility::Private);
1402
1418
 
1403
1419
  let singleton_method = definitions
@@ -1408,6 +1424,7 @@ mod tests {
1408
1424
  panic!()
1409
1425
  };
1410
1426
  assert_def_str_eq!(&context, singleton_method, "foo()");
1427
+ assert_def_name_offset_eq!(&context, singleton_method, "2:13-2:16");
1411
1428
  assert_eq!(singleton_method.visibility(), &Visibility::Public);
1412
1429
  }
1413
1430
 
@@ -1425,6 +1442,7 @@ mod tests {
1425
1442
 
1426
1443
  assert_definition_at!(&context, "2:3-2:27", Method, |def| {
1427
1444
  assert_def_str_eq!(&context, def, "foo()");
1445
+ assert_def_name_offset_eq!(&context, def, "2:12-2:15");
1428
1446
  let sigs = def.signatures().as_slice();
1429
1447
  assert_eq!(sigs.len(), 1);
1430
1448
  assert_eq!(sigs[0].len(), 0);