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.
- checksums.yaml +4 -4
- data/README.md +17 -16
- data/THIRD_PARTY_LICENSES.html +6 -6
- data/ext/rubydex/definition.c +33 -2
- data/ext/rubydex/document.c +36 -0
- data/ext/rubydex/graph.c +32 -18
- data/ext/rubydex/handle.h +21 -5
- data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
- data/lib/rubydex/errors.rb +8 -0
- data/lib/rubydex/location.rb +24 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +1 -0
- data/rbi/rubydex.rbi +29 -12
- data/rust/Cargo.lock +3 -3
- data/rust/rubydex/Cargo.toml +7 -1
- data/rust/rubydex/src/dot.rs +609 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
- data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
- data/rust/rubydex/src/lib.rs +1 -1
- data/rust/rubydex/src/main.rs +8 -5
- data/rust/rubydex/src/model/built_in.rs +5 -2
- data/rust/rubydex/src/model/comment.rs +2 -0
- data/rust/rubydex/src/model/declaration.rs +1 -0
- data/rust/rubydex/src/model/definitions.rs +13 -1
- data/rust/rubydex/src/model/document.rs +2 -0
- data/rust/rubydex/src/model/encoding.rs +2 -0
- data/rust/rubydex/src/model/graph.rs +51 -13
- data/rust/rubydex/src/model/identity_maps.rs +3 -0
- data/rust/rubydex/src/model/keywords.rs +3 -0
- data/rust/rubydex/src/model/name.rs +2 -0
- data/rust/rubydex/src/model/string_ref.rs +2 -0
- data/rust/rubydex/src/model/visibility.rs +3 -0
- data/rust/rubydex/src/operation/applier.rs +1 -0
- data/rust/rubydex/src/operation/mod.rs +1 -0
- data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
- data/rust/rubydex/src/query.rs +114 -33
- data/rust/rubydex/src/resolution.rs +16 -8
- data/rust/rubydex/src/resolution_tests.rs +132 -0
- data/rust/rubydex/tests/cli.rs +17 -61
- data/rust/rubydex-mcp/Cargo.toml +9 -3
- data/rust/rubydex-sys/Cargo.toml +9 -2
- data/rust/rubydex-sys/src/definition_api.rs +72 -2
- data/rust/rubydex-sys/src/document_api.rs +28 -0
- data/rust/rubydex-sys/src/graph_api.rs +1 -3
- metadata +4 -4
- data/rust/rubydex/src/visualization/dot.rs +0 -192
- 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('&', "&").replace('<', "<").replace('>', ">")
|
|
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(¤t_id)? {
|
|
335
|
+
Declaration::Namespace(_) => return Some(current_id),
|
|
336
|
+
Declaration::ConstantAlias(_) => {
|
|
337
|
+
queue.extend(self.graph.alias_targets(¤t_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(
|
|
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);
|