1use std::path::Path;
2
3use anyhow::{Context, Result};
4use graphy_core::{
5 EdgeKind, EdgeMetadata, GirEdge, GirNode, Language, NodeKind, ParseOutput, SymbolId,
6 Visibility,
7};
8use tree_sitter::{Node, Parser};
9
10use crate::frontend::LanguageFrontend;
11use crate::helpers::{is_noise_method_call, node_span, node_text};
12
13pub struct RustFrontend;
15
16impl RustFrontend {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl LanguageFrontend for RustFrontend {
23 fn parse(&self, path: &Path, source: &str) -> Result<ParseOutput> {
24 let mut parser = Parser::new();
25 parser
26 .set_language(&tree_sitter_rust::LANGUAGE.into())
27 .context("Failed to set Rust language")?;
28
29 let tree = parser
30 .parse(source, None)
31 .context("tree-sitter parse returned None")?;
32
33 let root = tree.root_node();
34 let mut output = ParseOutput::new();
35 let source_bytes = source.as_bytes();
36
37 let file_node = GirNode {
39 id: SymbolId::new(path, path.to_string_lossy().as_ref(), NodeKind::File, 0),
40 name: path
41 .file_stem()
42 .map(|s| s.to_string_lossy().into_owned())
43 .unwrap_or_else(|| path.to_string_lossy().into_owned()),
44 kind: NodeKind::File,
45 file_path: path.to_path_buf(),
46 span: node_span(&root),
47 visibility: Visibility::Public,
48 language: Language::Rust,
49 signature: None,
50 complexity: None,
51 confidence: 1.0,
52 doc: None,
53 coverage: None,
54 };
55 let file_id = file_node.id;
56 output.add_node(file_node);
57
58 let mut cursor = root.walk();
60 for child in root.children(&mut cursor) {
61 extract_node(&child, source_bytes, path, file_id, &mut output);
62 }
63
64 Ok(output)
65 }
66}
67
68fn extract_node(
69 node: &Node,
70 source: &[u8],
71 path: &Path,
72 parent_id: SymbolId,
73 output: &mut ParseOutput,
74) {
75 match node.kind() {
76 "function_item" => {
77 extract_function(node, source, path, parent_id, output, false);
78 }
79 "struct_item" => {
80 extract_struct(node, source, path, parent_id, output);
81 }
82 "enum_item" => {
83 extract_enum(node, source, path, parent_id, output);
84 }
85 "trait_item" => {
86 extract_trait(node, source, path, parent_id, output);
87 }
88 "impl_item" => {
89 extract_impl(node, source, path, parent_id, output);
90 }
91 "type_item" => {
92 extract_type_alias(node, source, path, parent_id, output);
93 }
94 "const_item" => {
95 extract_const(node, source, path, parent_id, output);
96 }
97 "static_item" => {
98 extract_static(node, source, path, parent_id, output);
99 }
100 "mod_item" => {
101 extract_mod(node, source, path, parent_id, output);
102 }
103 "use_declaration" => {
104 extract_use(node, source, path, parent_id, output);
105 }
106 "attribute_item" => {
107 }
109 "macro_definition" => {
110 extract_macro_def(node, source, path, parent_id, output);
111 }
112 _ => {}
113 }
114}
115
116fn extract_function(
119 node: &Node,
120 source: &[u8],
121 path: &Path,
122 parent_id: SymbolId,
123 output: &mut ParseOutput,
124 is_method: bool,
125) {
126 let Some(name_node) = node.child_by_field_name("name") else {
127 return;
128 };
129 let name = node_text(&name_node, source);
130 let span = node_span(node);
131
132 let kind = if is_method {
133 if name == "new" {
134 NodeKind::Constructor
135 } else {
136 NodeKind::Method
137 }
138 } else {
139 NodeKind::Function
140 };
141
142 let visibility = extract_visibility(node, source);
143 let sig = build_function_signature(node, source, &name);
144 let doc = extract_doc_comment(node, source);
145 let generics = extract_generics(node, source);
146
147 let signature = if let Some(g) = &generics {
148 Some(format!("{sig}{g}"))
149 } else {
150 Some(sig)
151 };
152
153 let func_node = GirNode {
154 id: SymbolId::new(path, &name, kind, span.start_line),
155 name: name.clone(),
156 kind,
157 file_path: path.to_path_buf(),
158 span,
159 visibility,
160 language: Language::Rust,
161 signature,
162 complexity: None,
163 confidence: 1.0,
164 doc,
165 coverage: None,
166 };
167 let func_id = func_node.id;
168 output.add_node(func_node);
169 output.add_edge(parent_id, func_id, GirEdge::new(EdgeKind::Contains));
170
171 if let Some(params) = node.child_by_field_name("parameters") {
173 extract_parameters(¶ms, source, path, func_id, output);
174 }
175
176 if let Some(ret) = node.child_by_field_name("return_type") {
178 let type_name = node_text(&ret, source);
179 let clean_name = type_name.trim_start_matches("->").trim().to_string();
181 if !clean_name.is_empty() {
182 let type_node = GirNode::new(
183 clean_name,
184 NodeKind::TypeAlias,
185 path.to_path_buf(),
186 node_span(&ret),
187 Language::Rust,
188 );
189 let type_id = type_node.id;
190 output.add_node(type_node);
191 output.add_edge(func_id, type_id, GirEdge::new(EdgeKind::ReturnsType));
192 }
193 }
194
195 if let Some(body) = node.child_by_field_name("body") {
197 extract_calls_from_body(&body, source, path, func_id, output);
198 }
199
200 extract_attributes_as_decorators(node, source, path, func_id, output);
202}
203
204fn extract_struct(
207 node: &Node,
208 source: &[u8],
209 path: &Path,
210 parent_id: SymbolId,
211 output: &mut ParseOutput,
212) {
213 let Some(name_node) = node.child_by_field_name("name") else {
214 return;
215 };
216 let name = node_text(&name_node, source);
217 let span = node_span(node);
218 let visibility = extract_visibility(node, source);
219 let doc = extract_doc_comment(node, source);
220
221 let struct_node = GirNode {
222 id: SymbolId::new(path, &name, NodeKind::Struct, span.start_line),
223 name: name.clone(),
224 kind: NodeKind::Struct,
225 file_path: path.to_path_buf(),
226 span,
227 visibility,
228 language: Language::Rust,
229 signature: Some(format!("struct {name}")),
230 complexity: None,
231 confidence: 1.0,
232 doc,
233 coverage: None,
234 };
235 let struct_id = struct_node.id;
236 output.add_node(struct_node);
237 output.add_edge(parent_id, struct_id, GirEdge::new(EdgeKind::Contains));
238
239 let body_node = node.child_by_field_name("body");
242 if let Some(body) = body_node {
243 extract_struct_fields(&body, source, path, struct_id, output);
244 } else {
245 let mut cursor = node.walk();
246 for child in node.children(&mut cursor) {
247 if child.kind() == "field_declaration_list" {
248 extract_struct_fields(&child, source, path, struct_id, output);
249 break;
250 }
251 }
252 }
253
254 extract_attributes_as_decorators(node, source, path, struct_id, output);
256}
257
258fn extract_struct_fields(
259 body: &Node,
260 source: &[u8],
261 path: &Path,
262 struct_id: SymbolId,
263 output: &mut ParseOutput,
264) {
265 let mut cursor = body.walk();
266 for child in body.children(&mut cursor) {
267 if child.kind() == "field_declaration" {
268 if let Some(name_node) = child.child_by_field_name("name") {
269 let field_name = node_text(&name_node, source);
270 let field_span = node_span(&child);
271 let field_vis = extract_visibility(&child, source);
272
273 let field_node = GirNode {
274 id: SymbolId::new(path, &field_name, NodeKind::Field, field_span.start_line),
275 name: field_name,
276 kind: NodeKind::Field,
277 file_path: path.to_path_buf(),
278 span: field_span,
279 visibility: field_vis,
280 language: Language::Rust,
281 signature: None,
282 complexity: None,
283 confidence: 1.0,
284 doc: None,
285 coverage: None,
286 };
287 let field_id = field_node.id;
288 output.add_node(field_node);
289 output.add_edge(struct_id, field_id, GirEdge::new(EdgeKind::Contains));
290
291 if let Some(type_node) = child.child_by_field_name("type") {
293 let type_name = node_text(&type_node, source);
294 let tn = GirNode::new(
295 type_name,
296 NodeKind::TypeAlias,
297 path.to_path_buf(),
298 node_span(&type_node),
299 Language::Rust,
300 );
301 let type_id = tn.id;
302 output.add_node(tn);
303 output.add_edge(field_id, type_id, GirEdge::new(EdgeKind::FieldType));
304 }
305 }
306 }
307 }
308}
309
310fn extract_enum(
313 node: &Node,
314 source: &[u8],
315 path: &Path,
316 parent_id: SymbolId,
317 output: &mut ParseOutput,
318) {
319 let Some(name_node) = node.child_by_field_name("name") else {
320 return;
321 };
322 let name = node_text(&name_node, source);
323 let span = node_span(node);
324 let visibility = extract_visibility(node, source);
325 let doc = extract_doc_comment(node, source);
326
327 let enum_node = GirNode {
328 id: SymbolId::new(path, &name, NodeKind::Enum, span.start_line),
329 name: name.clone(),
330 kind: NodeKind::Enum,
331 file_path: path.to_path_buf(),
332 span,
333 visibility,
334 language: Language::Rust,
335 signature: Some(format!("enum {name}")),
336 complexity: None,
337 confidence: 1.0,
338 doc,
339 coverage: None,
340 };
341 let enum_id = enum_node.id;
342 output.add_node(enum_node);
343 output.add_edge(parent_id, enum_id, GirEdge::new(EdgeKind::Contains));
344
345 if let Some(body) = node.child_by_field_name("body") {
347 let mut cursor = body.walk();
348 for child in body.children(&mut cursor) {
349 if child.kind() == "enum_variant" {
350 if let Some(vname) = child.child_by_field_name("name") {
351 let variant_name = node_text(&vname, source);
352 let variant_node = GirNode::new(
353 variant_name,
354 NodeKind::EnumVariant,
355 path.to_path_buf(),
356 node_span(&child),
357 Language::Rust,
358 );
359 let variant_id = variant_node.id;
360 output.add_node(variant_node);
361 output.add_edge(enum_id, variant_id, GirEdge::new(EdgeKind::Contains));
362 }
363 }
364 }
365 }
366
367 extract_attributes_as_decorators(node, source, path, enum_id, output);
369}
370
371fn extract_trait(
374 node: &Node,
375 source: &[u8],
376 path: &Path,
377 parent_id: SymbolId,
378 output: &mut ParseOutput,
379) {
380 let Some(name_node) = node.child_by_field_name("name") else {
381 return;
382 };
383 let name = node_text(&name_node, source);
384 let span = node_span(node);
385 let visibility = extract_visibility(node, source);
386 let doc = extract_doc_comment(node, source);
387
388 let trait_node = GirNode {
389 id: SymbolId::new(path, &name, NodeKind::Trait, span.start_line),
390 name: name.clone(),
391 kind: NodeKind::Trait,
392 file_path: path.to_path_buf(),
393 span,
394 visibility,
395 language: Language::Rust,
396 signature: Some(format!("trait {name}")),
397 complexity: None,
398 confidence: 1.0,
399 doc,
400 coverage: None,
401 };
402 let trait_id = trait_node.id;
403 output.add_node(trait_node);
404 output.add_edge(parent_id, trait_id, GirEdge::new(EdgeKind::Contains));
405
406 if let Some(body) = node.child_by_field_name("body") {
408 let mut cursor = body.walk();
409 for child in body.children(&mut cursor) {
410 match child.kind() {
411 "function_item" => {
412 extract_function(&child, source, path, trait_id, output, true);
413 }
414 "function_signature_item" => {
415 extract_trait_method_signature(&child, source, path, trait_id, output);
416 }
417 "type_item" => {
418 extract_type_alias(&child, source, path, trait_id, output);
419 }
420 "const_item" => {
421 extract_const(&child, source, path, trait_id, output);
422 }
423 _ => {}
424 }
425 }
426 }
427
428 let mut cursor = node.walk();
430 for child in node.children(&mut cursor) {
431 if child.kind() == "trait_bounds" {
432 let mut bounds_cursor = child.walk();
433 for bound in child.children(&mut bounds_cursor) {
434 if bound.kind() == "type_identifier" || bound.kind() == "scoped_type_identifier" || bound.kind() == "generic_type" {
435 let bound_name = node_text(&bound, source);
436 let bound_node = GirNode::new(
437 bound_name,
438 NodeKind::Trait,
439 path.to_path_buf(),
440 node_span(&bound),
441 Language::Rust,
442 );
443 let bound_id = bound_node.id;
444 output.add_node(bound_node);
445 output.add_edge(
446 trait_id,
447 bound_id,
448 GirEdge::new(EdgeKind::Inherits)
449 .with_metadata(EdgeMetadata::Inheritance { depth: 1 }),
450 );
451 }
452 }
453 }
454 }
455}
456
457fn extract_trait_method_signature(
458 node: &Node,
459 source: &[u8],
460 path: &Path,
461 trait_id: SymbolId,
462 output: &mut ParseOutput,
463) {
464 let Some(name_node) = node.child_by_field_name("name") else {
465 return;
466 };
467 let name = node_text(&name_node, source);
468 let span = node_span(node);
469 let sig = build_function_signature(node, source, &name);
470
471 let method_node = GirNode {
472 id: SymbolId::new(path, &name, NodeKind::Method, span.start_line),
473 name,
474 kind: NodeKind::Method,
475 file_path: path.to_path_buf(),
476 span,
477 visibility: Visibility::Public,
478 language: Language::Rust,
479 signature: Some(sig),
480 complexity: None,
481 confidence: 1.0,
482 doc: extract_doc_comment(node, source),
483 coverage: None,
484 };
485 let method_id = method_node.id;
486 output.add_node(method_node);
487 output.add_edge(trait_id, method_id, GirEdge::new(EdgeKind::Contains));
488
489 if let Some(params) = node.child_by_field_name("parameters") {
491 extract_parameters(¶ms, source, path, method_id, output);
492 }
493
494 if let Some(ret) = node.child_by_field_name("return_type") {
496 let type_name = node_text(&ret, source);
497 let clean_name = type_name.trim_start_matches("->").trim().to_string();
498 if !clean_name.is_empty() {
499 let type_node = GirNode::new(
500 clean_name,
501 NodeKind::TypeAlias,
502 path.to_path_buf(),
503 node_span(&ret),
504 Language::Rust,
505 );
506 let type_id = type_node.id;
507 output.add_node(type_node);
508 output.add_edge(method_id, type_id, GirEdge::new(EdgeKind::ReturnsType));
509 }
510 }
511}
512
513fn extract_impl(
516 node: &Node,
517 source: &[u8],
518 path: &Path,
519 _parent_id: SymbolId,
520 output: &mut ParseOutput,
521) {
522 let impl_type = node
524 .child_by_field_name("type")
525 .map(|n| node_text(&n, source))
526 .unwrap_or_default();
527
528 let impl_trait = node
529 .child_by_field_name("trait")
530 .map(|n| node_text(&n, source));
531
532 if impl_type.is_empty() {
533 return;
534 }
535
536 let span = node_span(node);
537
538 let type_id = if let Some(existing) = output.nodes.iter().find(|n| {
541 n.name == impl_type
542 && n.file_path == path
543 && matches!(
544 n.kind,
545 NodeKind::Struct | NodeKind::Enum | NodeKind::Trait | NodeKind::Class
546 )
547 }) {
548 existing.id
549 } else {
550 let type_node = GirNode::new(
552 impl_type.clone(),
553 NodeKind::Struct,
554 path.to_path_buf(),
555 span,
556 Language::Rust,
557 );
558 let id = type_node.id;
559 output.add_node(type_node);
560 id
561 };
562
563 if let Some(ref trait_name) = impl_trait {
565 let trait_node = GirNode::new(
566 trait_name.clone(),
567 NodeKind::Trait,
568 path.to_path_buf(),
569 span,
570 Language::Rust,
571 );
572 let trait_id = trait_node.id;
573 output.add_node(trait_node);
574 output.add_edge(type_id, trait_id, GirEdge::new(EdgeKind::Implements));
575 }
576
577 if let Some(body) = node.child_by_field_name("body") {
579 let mut cursor = body.walk();
580 for child in body.children(&mut cursor) {
581 match child.kind() {
582 "function_item" => {
583 extract_function(&child, source, path, type_id, output, true);
584 }
585 "type_item" => {
586 extract_type_alias(&child, source, path, type_id, output);
587 }
588 "const_item" => {
589 extract_const(&child, source, path, type_id, output);
590 }
591 _ => {}
592 }
593 }
594 }
595}
596
597fn extract_type_alias(
600 node: &Node,
601 source: &[u8],
602 path: &Path,
603 parent_id: SymbolId,
604 output: &mut ParseOutput,
605) {
606 let Some(name_node) = node.child_by_field_name("name") else {
607 return;
608 };
609 let name = node_text(&name_node, source);
610 let span = node_span(node);
611 let visibility = extract_visibility(node, source);
612 let full_text = node_text(node, source);
613
614 let type_node = GirNode {
615 id: SymbolId::new(path, &name, NodeKind::TypeAlias, span.start_line),
616 name,
617 kind: NodeKind::TypeAlias,
618 file_path: path.to_path_buf(),
619 span,
620 visibility,
621 language: Language::Rust,
622 signature: Some(full_text),
623 complexity: None,
624 confidence: 1.0,
625 doc: extract_doc_comment(node, source),
626 coverage: None,
627 };
628 let type_id = type_node.id;
629 output.add_node(type_node);
630 output.add_edge(parent_id, type_id, GirEdge::new(EdgeKind::Contains));
631}
632
633fn extract_const(
636 node: &Node,
637 source: &[u8],
638 path: &Path,
639 parent_id: SymbolId,
640 output: &mut ParseOutput,
641) {
642 let Some(name_node) = node.child_by_field_name("name") else {
643 return;
644 };
645 let name = node_text(&name_node, source);
646 let span = node_span(node);
647 let visibility = extract_visibility(node, source);
648
649 let const_node = GirNode {
650 id: SymbolId::new(path, &name, NodeKind::Constant, span.start_line),
651 name,
652 kind: NodeKind::Constant,
653 file_path: path.to_path_buf(),
654 span,
655 visibility,
656 language: Language::Rust,
657 signature: Some(node_text(node, source)),
658 complexity: None,
659 confidence: 1.0,
660 doc: extract_doc_comment(node, source),
661 coverage: None,
662 };
663 let const_id = const_node.id;
664 output.add_node(const_node);
665 output.add_edge(parent_id, const_id, GirEdge::new(EdgeKind::Contains));
666
667 if let Some(type_ann) = node.child_by_field_name("type") {
669 let type_name = node_text(&type_ann, source);
670 let tn = GirNode::new(
671 type_name,
672 NodeKind::TypeAlias,
673 path.to_path_buf(),
674 node_span(&type_ann),
675 Language::Rust,
676 );
677 let type_id = tn.id;
678 output.add_node(tn);
679 output.add_edge(const_id, type_id, GirEdge::new(EdgeKind::FieldType));
680 }
681}
682
683fn extract_static(
686 node: &Node,
687 source: &[u8],
688 path: &Path,
689 parent_id: SymbolId,
690 output: &mut ParseOutput,
691) {
692 let Some(name_node) = node.child_by_field_name("name") else {
693 return;
694 };
695 let name = node_text(&name_node, source);
696 let span = node_span(node);
697 let visibility = extract_visibility(node, source);
698
699 let static_node = GirNode {
700 id: SymbolId::new(path, &name, NodeKind::Variable, span.start_line),
701 name,
702 kind: NodeKind::Variable,
703 file_path: path.to_path_buf(),
704 span,
705 visibility,
706 language: Language::Rust,
707 signature: Some(node_text(node, source)),
708 complexity: None,
709 confidence: 1.0,
710 doc: extract_doc_comment(node, source),
711 coverage: None,
712 };
713 let static_id = static_node.id;
714 output.add_node(static_node);
715 output.add_edge(parent_id, static_id, GirEdge::new(EdgeKind::Contains));
716}
717
718fn extract_mod(
721 node: &Node,
722 source: &[u8],
723 path: &Path,
724 parent_id: SymbolId,
725 output: &mut ParseOutput,
726) {
727 let Some(name_node) = node.child_by_field_name("name") else {
728 return;
729 };
730 let name = node_text(&name_node, source);
731 let span = node_span(node);
732 let visibility = extract_visibility(node, source);
733 let doc = extract_doc_comment(node, source);
734
735 let mod_node = GirNode {
736 id: SymbolId::new(path, &name, NodeKind::Module, span.start_line),
737 name: name.clone(),
738 kind: NodeKind::Module,
739 file_path: path.to_path_buf(),
740 span,
741 visibility,
742 language: Language::Rust,
743 signature: Some(format!("mod {name}")),
744 complexity: None,
745 confidence: 1.0,
746 doc,
747 coverage: None,
748 };
749 let mod_id = mod_node.id;
750 output.add_node(mod_node);
751 output.add_edge(parent_id, mod_id, GirEdge::new(EdgeKind::Contains));
752
753 extract_attributes_as_decorators(node, source, path, mod_id, output);
755
756 if let Some(body) = node.child_by_field_name("body") {
758 let mut cursor = body.walk();
759 for child in body.children(&mut cursor) {
760 extract_node(&child, source, path, mod_id, output);
761 }
762 }
763}
764
765fn extract_use(
768 node: &Node,
769 source: &[u8],
770 path: &Path,
771 parent_id: SymbolId,
772 output: &mut ParseOutput,
773) {
774 let text = node_text(node, source);
775 let span = node_span(node);
776 let visibility = extract_visibility(node, source);
777
778 let mut items = Vec::new();
780 collect_use_items(node, source, &mut items);
781
782 let import_node = GirNode {
783 id: SymbolId::new(path, &text, NodeKind::Import, span.start_line),
784 name: text.clone(),
785 kind: NodeKind::Import,
786 file_path: path.to_path_buf(),
787 span,
788 visibility,
789 language: Language::Rust,
790 signature: Some(text),
791 complexity: None,
792 confidence: 1.0,
793 doc: None,
794 coverage: None,
795 };
796 let import_id = import_node.id;
797 output.add_node(import_node);
798 output.add_edge(
799 parent_id,
800 import_id,
801 GirEdge::new(EdgeKind::ImportsFrom).with_metadata(EdgeMetadata::Import {
802 alias: None,
803 items,
804 }),
805 );
806}
807
808fn collect_use_items(node: &Node, source: &[u8], items: &mut Vec<String>) {
809 let mut cursor = node.walk();
810 for child in node.children(&mut cursor) {
811 match child.kind() {
812 "use_as_clause" => {
813 if let Some(path_node) = child.child_by_field_name("path") {
814 items.push(node_text(&path_node, source));
815 }
816 }
817 "use_list" => {
818 let mut list_cursor = child.walk();
819 for list_child in child.children(&mut list_cursor) {
820 if list_child.kind() == "identifier" || list_child.kind() == "scoped_identifier" {
821 items.push(node_text(&list_child, source));
822 } else if list_child.kind() == "use_as_clause" {
823 if let Some(path_node) = list_child.child_by_field_name("path") {
824 items.push(node_text(&path_node, source));
825 }
826 }
827 }
828 }
829 "scoped_use_list" => {
830 collect_use_items(&child, source, items);
831 }
832 "identifier" => {
833 items.push(node_text(&child, source));
834 }
835 "scoped_identifier" => {
836 items.push(node_text(&child, source));
837 }
838 _ => {
839 if child.child_count() > 0 {
841 collect_use_items(&child, source, items);
842 }
843 }
844 }
845 }
846}
847
848fn extract_macro_def(
851 node: &Node,
852 source: &[u8],
853 path: &Path,
854 parent_id: SymbolId,
855 output: &mut ParseOutput,
856) {
857 let Some(name_node) = node.child_by_field_name("name") else {
858 return;
859 };
860 let name = node_text(&name_node, source);
861 let span = node_span(node);
862
863 let macro_node = GirNode {
864 id: SymbolId::new(path, &name, NodeKind::Function, span.start_line),
865 name: format!("{name}!"),
866 kind: NodeKind::Function,
867 file_path: path.to_path_buf(),
868 span,
869 visibility: extract_visibility(node, source),
870 language: Language::Rust,
871 signature: Some(format!("macro_rules! {name}")),
872 complexity: None,
873 confidence: 1.0,
874 doc: extract_doc_comment(node, source),
875 coverage: None,
876 };
877 let macro_id = macro_node.id;
878 output.add_node(macro_node);
879 output.add_edge(parent_id, macro_id, GirEdge::new(EdgeKind::Contains));
880}
881
882fn extract_parameters(
885 params_node: &Node,
886 source: &[u8],
887 path: &Path,
888 func_id: SymbolId,
889 output: &mut ParseOutput,
890) {
891 let mut cursor = params_node.walk();
892 for param in params_node.children(&mut cursor) {
893 let name = match param.kind() {
894 "parameter" => {
895 param
896 .child_by_field_name("pattern")
897 .map(|n| node_text(&n, source))
898 .unwrap_or_default()
899 }
900 "self_parameter" => {
901 node_text(¶m, source)
902 }
903 "identifier" => node_text(¶m, source),
904 _ => continue,
905 };
906
907 if name.is_empty() || name == "," || name == "(" || name == ")" {
908 continue;
909 }
910
911 let param_node = GirNode::new(
912 name,
913 NodeKind::Parameter,
914 path.to_path_buf(),
915 node_span(¶m),
916 Language::Rust,
917 );
918 let param_id = param_node.id;
919 output.add_node(param_node);
920 output.add_edge(func_id, param_id, GirEdge::new(EdgeKind::Contains));
921
922 if let Some(type_ann) = param.child_by_field_name("type") {
924 let type_name = node_text(&type_ann, source);
925 let tn = GirNode::new(
926 type_name,
927 NodeKind::TypeAlias,
928 path.to_path_buf(),
929 node_span(&type_ann),
930 Language::Rust,
931 );
932 let type_id = tn.id;
933 output.add_node(tn);
934 output.add_edge(param_id, type_id, GirEdge::new(EdgeKind::ParamType));
935 }
936 }
937}
938
939fn extract_attributes_as_decorators(
942 node: &Node,
943 source: &[u8],
944 path: &Path,
945 target_id: SymbolId,
946 output: &mut ParseOutput,
947) {
948 let mut prev = node.prev_sibling();
950 while let Some(sibling) = prev {
951 if sibling.kind() == "attribute_item" {
952 let attr_text = node_text(&sibling, source);
953 let inner = attr_text
955 .trim_start_matches("#![")
956 .trim_start_matches("#[")
957 .trim_end_matches(']')
958 .trim()
959 .to_string();
960
961 if inner.starts_with("derive(") {
963 let derives = inner
964 .trim_start_matches("derive(")
965 .trim_end_matches(')')
966 .split(',')
967 .map(|s| s.trim().to_string())
968 .filter(|s| !s.is_empty());
969
970 for derive_name in derives {
971 let dec_node = GirNode::new(
972 format!("derive({derive_name})"),
973 NodeKind::Decorator,
974 path.to_path_buf(),
975 node_span(&sibling),
976 Language::Rust,
977 );
978 let dec_id = dec_node.id;
979 output.add_node(dec_node);
980 output.add_edge(target_id, dec_id, GirEdge::new(EdgeKind::AnnotatedWith));
981 }
982 } else {
983 let dec_node = GirNode::new(
984 inner,
985 NodeKind::Decorator,
986 path.to_path_buf(),
987 node_span(&sibling),
988 Language::Rust,
989 );
990 let dec_id = dec_node.id;
991 output.add_node(dec_node);
992 output.add_edge(target_id, dec_id, GirEdge::new(EdgeKind::AnnotatedWith));
993 }
994 prev = sibling.prev_sibling();
995 } else if sibling.kind() == "line_comment" || sibling.kind() == "block_comment" {
996 prev = sibling.prev_sibling();
998 } else {
999 break;
1000 }
1001 }
1002}
1003
1004fn extract_calls_from_body(
1007 body: &Node,
1008 source: &[u8],
1009 path: &Path,
1010 func_id: SymbolId,
1011 output: &mut ParseOutput,
1012) {
1013 let mut stack = vec![*body];
1014 while let Some(node) = stack.pop() {
1015 if node.kind() == "call_expression" {
1016 if let Some(func_node) = node.child_by_field_name("function") {
1017 let call_name = node_text(&func_node, source);
1018
1019 if !is_noise_call(&call_name) && !is_noise_method_call(&call_name) {
1020 let call_target = GirNode::new(
1021 call_name.clone(),
1022 NodeKind::Function,
1023 path.to_path_buf(),
1024 node_span(&func_node),
1025 Language::Rust,
1026 );
1027 let target_id = call_target.id;
1028 output.add_node(call_target);
1029
1030 let is_dynamic = call_name.contains("::");
1031 let confidence = if is_dynamic { 0.7 } else { 0.9 };
1032 output.add_edge(
1033 func_id,
1034 target_id,
1035 GirEdge::new(EdgeKind::Calls)
1036 .with_confidence(confidence)
1037 .with_metadata(EdgeMetadata::Call { is_dynamic }),
1038 );
1039 }
1040 }
1041 }
1042
1043 if node.kind() == "macro_invocation" {
1045 if let Some(macro_node) = node.child_by_field_name("macro") {
1046 let macro_name = node_text(¯o_node, source);
1047 if !is_noise_macro(¯o_name) {
1048 let call_target = GirNode::new(
1049 format!("{macro_name}!"),
1050 NodeKind::Function,
1051 path.to_path_buf(),
1052 node_span(¯o_node),
1053 Language::Rust,
1054 );
1055 let target_id = call_target.id;
1056 output.add_node(call_target);
1057 output.add_edge(
1058 func_id,
1059 target_id,
1060 GirEdge::new(EdgeKind::Calls)
1061 .with_confidence(0.8)
1062 .with_metadata(EdgeMetadata::Call { is_dynamic: false }),
1063 );
1064 }
1065 }
1066 }
1067
1068 let is_nested = node.kind() == "function_item";
1072 if !is_nested || node == *body {
1073 let mut cursor = node.walk();
1074 for child in node.children(&mut cursor) {
1075 stack.push(child);
1076 }
1077 }
1078 }
1079}
1080
1081fn extract_visibility(node: &Node, source: &[u8]) -> Visibility {
1084 let mut cursor = node.walk();
1086 for child in node.children(&mut cursor) {
1087 if child.kind() == "visibility_modifier" {
1088 let vis_text = node_text(&child, source);
1089 return match vis_text.as_str() {
1090 "pub" => Visibility::Public,
1091 "pub(crate)" => Visibility::Internal,
1092 "pub(super)" => Visibility::Internal,
1093 _ if vis_text.starts_with("pub(") => Visibility::Internal,
1094 _ => Visibility::Private,
1095 };
1096 }
1097 }
1098 Visibility::Private
1099}
1100
1101fn extract_generics(node: &Node, source: &[u8]) -> Option<String> {
1102 node.child_by_field_name("type_parameters")
1103 .map(|n| node_text(&n, source))
1104}
1105
1106fn build_function_signature(node: &Node, source: &[u8], name: &str) -> String {
1107 let params = node
1108 .child_by_field_name("parameters")
1109 .map(|p| node_text(&p, source))
1110 .unwrap_or_else(|| "()".to_string());
1111
1112 let ret = node
1113 .child_by_field_name("return_type")
1114 .map(|r| format!(" {}", node_text(&r, source)))
1115 .unwrap_or_default();
1116
1117 let generics = node
1118 .child_by_field_name("type_parameters")
1119 .map(|g| node_text(&g, source))
1120 .unwrap_or_default();
1121
1122 format!("fn {name}{generics}{params}{ret}")
1123}
1124
1125fn extract_doc_comment(node: &Node, source: &[u8]) -> Option<String> {
1126 let mut doc_lines = Vec::new();
1128 let mut prev = node.prev_sibling();
1129
1130 while let Some(sibling) = prev {
1131 if sibling.kind() == "line_comment" {
1132 let text = node_text(&sibling, source);
1133 if text.starts_with("///") {
1134 let content = text.trim_start_matches("///").trim();
1135 doc_lines.push(content.to_string());
1136 prev = sibling.prev_sibling();
1137 continue;
1138 } else if text.starts_with("//!") {
1139 let content = text.trim_start_matches("//!").trim();
1140 doc_lines.push(content.to_string());
1141 prev = sibling.prev_sibling();
1142 continue;
1143 }
1144 } else if sibling.kind() == "attribute_item" {
1145 prev = sibling.prev_sibling();
1147 continue;
1148 }
1149 break;
1150 }
1151
1152 if doc_lines.is_empty() {
1153 if let Some(sibling) = node.prev_sibling() {
1155 if sibling.kind() == "block_comment" {
1156 let text = node_text(&sibling, source);
1157 if text.starts_with("/**") || text.starts_with("/*!") {
1158 let cleaned = text
1159 .trim_start_matches("/**")
1160 .trim_start_matches("/*!")
1161 .trim_end_matches("*/")
1162 .lines()
1163 .map(|line| line.trim().trim_start_matches('*').trim())
1164 .filter(|line| !line.is_empty())
1165 .collect::<Vec<_>>()
1166 .join("\n");
1167 if !cleaned.is_empty() {
1168 return Some(cleaned);
1169 }
1170 }
1171 }
1172 }
1173 return None;
1174 }
1175
1176 doc_lines.reverse();
1178 Some(doc_lines.join("\n"))
1179}
1180
1181fn is_noise_call(name: &str) -> bool {
1184 matches!(
1185 name,
1186 "println" | "print" | "eprintln" | "eprint"
1187 | "format" | "write" | "writeln"
1188 | "dbg" | "todo" | "unimplemented" | "unreachable"
1189 | "assert" | "assert_eq" | "assert_ne"
1190 | "debug_assert" | "debug_assert_eq" | "debug_assert_ne"
1191 | "panic" | "Ok" | "Err" | "Some" | "None"
1192 )
1193}
1194
1195fn is_noise_macro(name: &str) -> bool {
1196 matches!(
1197 name,
1198 "println" | "print" | "eprintln" | "eprint"
1199 | "format" | "write" | "writeln"
1200 | "dbg" | "todo" | "unimplemented" | "unreachable"
1201 | "assert" | "assert_eq" | "assert_ne"
1202 | "debug_assert" | "debug_assert_eq" | "debug_assert_ne"
1203 | "panic" | "vec" | "cfg" | "include"
1204 | "env" | "concat" | "stringify"
1205 )
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211 use graphy_core::NodeKind;
1212
1213 #[test]
1214 fn parse_simple_function() {
1215 let source = r#"
1216/// Adds two numbers.
1217pub fn add(a: i32, b: i32) -> i32 {
1218 a + b
1219}
1220"#;
1221 let output = RustFrontend::new()
1222 .parse(Path::new("test.rs"), source)
1223 .unwrap();
1224
1225 let funcs: Vec<_> = output
1226 .nodes
1227 .iter()
1228 .filter(|n| n.kind == NodeKind::Function)
1229 .collect();
1230 assert_eq!(funcs.len(), 1);
1231 assert_eq!(funcs[0].name, "add");
1232 assert_eq!(funcs[0].visibility, Visibility::Public);
1233 assert!(funcs[0].doc.as_deref().unwrap().contains("Adds two numbers"));
1234 }
1235
1236 #[test]
1237 fn parse_struct_with_fields() {
1238 let source = r#"
1239#[derive(Debug, Clone)]
1240pub struct Point {
1241 pub x: f64,
1242 pub y: f64,
1243}
1244"#;
1245 let output = RustFrontend::new()
1246 .parse(Path::new("test.rs"), source)
1247 .unwrap();
1248
1249 let structs: Vec<_> = output
1250 .nodes
1251 .iter()
1252 .filter(|n| n.kind == NodeKind::Struct)
1253 .collect();
1254 assert_eq!(structs.len(), 1);
1255 assert_eq!(structs[0].name, "Point");
1256
1257 let fields: Vec<_> = output
1258 .nodes
1259 .iter()
1260 .filter(|n| n.kind == NodeKind::Field)
1261 .collect();
1262 assert_eq!(fields.len(), 2);
1263
1264 let decorators: Vec<_> = output
1266 .nodes
1267 .iter()
1268 .filter(|n| n.kind == NodeKind::Decorator)
1269 .collect();
1270 assert!(decorators.len() >= 2);
1271 assert!(decorators.iter().any(|d| d.name.contains("Debug")));
1272 assert!(decorators.iter().any(|d| d.name.contains("Clone")));
1273 }
1274
1275 #[test]
1276 fn parse_enum_with_variants() {
1277 let source = r#"
1278pub enum Color {
1279 Red,
1280 Green,
1281 Blue,
1282 Custom(u8, u8, u8),
1283}
1284"#;
1285 let output = RustFrontend::new()
1286 .parse(Path::new("test.rs"), source)
1287 .unwrap();
1288
1289 let enums: Vec<_> = output
1290 .nodes
1291 .iter()
1292 .filter(|n| n.kind == NodeKind::Enum)
1293 .collect();
1294 assert_eq!(enums.len(), 1);
1295 assert_eq!(enums[0].name, "Color");
1296
1297 let variants: Vec<_> = output
1298 .nodes
1299 .iter()
1300 .filter(|n| n.kind == NodeKind::EnumVariant)
1301 .collect();
1302 assert_eq!(variants.len(), 4);
1303 }
1304
1305 #[test]
1306 fn parse_trait_definition() {
1307 let source = r#"
1308pub trait Drawable {
1309 fn draw(&self);
1310 fn area(&self) -> f64;
1311}
1312"#;
1313 let output = RustFrontend::new()
1314 .parse(Path::new("test.rs"), source)
1315 .unwrap();
1316
1317 let traits: Vec<_> = output
1318 .nodes
1319 .iter()
1320 .filter(|n| n.kind == NodeKind::Trait)
1321 .collect();
1322 assert_eq!(traits.len(), 1);
1323 assert_eq!(traits[0].name, "Drawable");
1324
1325 let methods: Vec<_> = output
1326 .nodes
1327 .iter()
1328 .filter(|n| n.kind == NodeKind::Method)
1329 .collect();
1330 assert!(methods.len() >= 2);
1331 }
1332
1333 #[test]
1334 fn parse_impl_block() {
1335 let source = r#"
1336struct Circle {
1337 radius: f64,
1338}
1339
1340impl Circle {
1341 pub fn new(radius: f64) -> Self {
1342 Circle { radius }
1343 }
1344
1345 pub fn area(&self) -> f64 {
1346 std::f64::consts::PI * self.radius * self.radius
1347 }
1348}
1349"#;
1350 let output = RustFrontend::new()
1351 .parse(Path::new("test.rs"), source)
1352 .unwrap();
1353
1354 let constructors: Vec<_> = output
1355 .nodes
1356 .iter()
1357 .filter(|n| n.kind == NodeKind::Constructor)
1358 .collect();
1359 assert_eq!(constructors.len(), 1);
1360 assert_eq!(constructors[0].name, "new");
1361
1362 let methods: Vec<_> = output
1363 .nodes
1364 .iter()
1365 .filter(|n| n.kind == NodeKind::Method)
1366 .collect();
1367 assert_eq!(methods.len(), 1);
1368 assert_eq!(methods[0].name, "area");
1369 }
1370
1371 #[test]
1372 fn parse_trait_impl() {
1373 let source = r#"
1374struct Square {
1375 side: f64,
1376}
1377
1378impl Drawable for Square {
1379 fn draw(&self) {
1380 todo!()
1381 }
1382
1383 fn area(&self) -> f64 {
1384 self.side * self.side
1385 }
1386}
1387"#;
1388 let output = RustFrontend::new()
1389 .parse(Path::new("test.rs"), source)
1390 .unwrap();
1391
1392 let impl_edges: Vec<_> = output
1394 .edges
1395 .iter()
1396 .filter(|(_, _, e)| e.kind == EdgeKind::Implements)
1397 .collect();
1398 assert!(!impl_edges.is_empty());
1399 }
1400
1401 #[test]
1402 fn parse_use_declarations() {
1403 let source = r#"
1404use std::collections::HashMap;
1405use std::path::{Path, PathBuf};
1406use crate::utils;
1407"#;
1408 let output = RustFrontend::new()
1409 .parse(Path::new("test.rs"), source)
1410 .unwrap();
1411
1412 let imports: Vec<_> = output
1413 .nodes
1414 .iter()
1415 .filter(|n| n.kind == NodeKind::Import)
1416 .collect();
1417 assert_eq!(imports.len(), 3);
1418 }
1419
1420 #[test]
1421 fn parse_mod_declaration() {
1422 let source = r#"
1423pub mod utils;
1424mod internal;
1425"#;
1426 let output = RustFrontend::new()
1427 .parse(Path::new("test.rs"), source)
1428 .unwrap();
1429
1430 let modules: Vec<_> = output
1431 .nodes
1432 .iter()
1433 .filter(|n| n.kind == NodeKind::Module)
1434 .collect();
1435 assert_eq!(modules.len(), 2);
1436
1437 let pub_mod = modules.iter().find(|m| m.name == "utils").unwrap();
1438 assert_eq!(pub_mod.visibility, Visibility::Public);
1439
1440 let priv_mod = modules.iter().find(|m| m.name == "internal").unwrap();
1441 assert_eq!(priv_mod.visibility, Visibility::Private);
1442 }
1443
1444 #[test]
1445 fn parse_visibility() {
1446 let source = r#"
1447pub fn public_fn() {}
1448pub(crate) fn crate_fn() {}
1449fn private_fn() {}
1450"#;
1451 let output = RustFrontend::new()
1452 .parse(Path::new("test.rs"), source)
1453 .unwrap();
1454
1455 let funcs: Vec<_> = output
1456 .nodes
1457 .iter()
1458 .filter(|n| n.kind == NodeKind::Function)
1459 .collect();
1460
1461 let pub_fn = funcs.iter().find(|f| f.name == "public_fn").unwrap();
1462 assert_eq!(pub_fn.visibility, Visibility::Public);
1463
1464 let crate_fn = funcs.iter().find(|f| f.name == "crate_fn").unwrap();
1465 assert_eq!(crate_fn.visibility, Visibility::Internal);
1466
1467 let priv_fn = funcs.iter().find(|f| f.name == "private_fn").unwrap();
1468 assert_eq!(priv_fn.visibility, Visibility::Private);
1469 }
1470
1471 #[test]
1472 fn parse_const_and_static() {
1473 let source = r#"
1474pub const MAX_SIZE: usize = 1024;
1475static COUNTER: u32 = 0;
1476"#;
1477 let output = RustFrontend::new()
1478 .parse(Path::new("test.rs"), source)
1479 .unwrap();
1480
1481 let consts: Vec<_> = output
1482 .nodes
1483 .iter()
1484 .filter(|n| n.kind == NodeKind::Constant)
1485 .collect();
1486 assert_eq!(consts.len(), 1);
1487 assert_eq!(consts[0].name, "MAX_SIZE");
1488
1489 let vars: Vec<_> = output
1490 .nodes
1491 .iter()
1492 .filter(|n| n.kind == NodeKind::Variable)
1493 .collect();
1494 assert_eq!(vars.len(), 1);
1495 }
1496
1497 #[test]
1498 fn parse_type_alias() {
1499 let source = r#"
1500pub type Result<T> = std::result::Result<T, MyError>;
1501"#;
1502 let output = RustFrontend::new()
1503 .parse(Path::new("test.rs"), source)
1504 .unwrap();
1505
1506 let types: Vec<_> = output
1507 .nodes
1508 .iter()
1509 .filter(|n| n.kind == NodeKind::TypeAlias && n.name == "Result")
1510 .collect();
1511 assert_eq!(types.len(), 1);
1512 }
1513
1514 #[test]
1517 fn parse_empty_file() {
1518 let output = RustFrontend::new()
1519 .parse(Path::new("empty.rs"), "")
1520 .unwrap();
1521 assert!(output.nodes.iter().any(|n| n.kind == NodeKind::File));
1522 }
1523
1524 #[test]
1525 fn parse_async_function() {
1526 let source = r#"
1527pub async fn fetch(url: &str) -> Result<String> {
1528 todo!()
1529}
1530"#;
1531 let output = RustFrontend::new()
1532 .parse(Path::new("test.rs"), source)
1533 .unwrap();
1534 let funcs: Vec<_> = output.nodes.iter()
1535 .filter(|n| n.kind == NodeKind::Function)
1536 .collect();
1537 assert_eq!(funcs.len(), 1);
1538 assert_eq!(funcs[0].name, "fetch");
1539 }
1540
1541 #[test]
1542 fn parse_lifetime_annotations() {
1543 let source = r#"
1544pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
1545 if x.len() > y.len() { x } else { y }
1546}
1547"#;
1548 let output = RustFrontend::new()
1549 .parse(Path::new("test.rs"), source)
1550 .unwrap();
1551 let funcs: Vec<_> = output.nodes.iter()
1552 .filter(|n| n.kind == NodeKind::Function)
1553 .collect();
1554 assert_eq!(funcs.len(), 1);
1555 assert_eq!(funcs[0].name, "longest");
1556 }
1557
1558 #[test]
1559 fn parse_inline_mod_with_items() {
1560 let source = r#"
1561mod inner {
1562 pub fn inner_fn() {}
1563 pub struct InnerStruct;
1564}
1565"#;
1566 let output = RustFrontend::new()
1567 .parse(Path::new("test.rs"), source)
1568 .unwrap();
1569 let modules: Vec<_> = output.nodes.iter()
1570 .filter(|n| n.kind == NodeKind::Module)
1571 .collect();
1572 assert!(modules.iter().any(|m| m.name == "inner"));
1573 let funcs: Vec<_> = output.nodes.iter()
1574 .filter(|n| n.kind == NodeKind::Function)
1575 .collect();
1576 assert!(funcs.iter().any(|f| f.name == "inner_fn"));
1577 }
1578
1579 #[test]
1580 fn parse_cfg_test_module() {
1581 let source = r#"
1582#[cfg(test)]
1583mod tests {
1584 #[test]
1585 fn it_works() {
1586 assert_eq!(2 + 2, 4);
1587 }
1588}
1589"#;
1590 let output = RustFrontend::new()
1591 .parse(Path::new("test.rs"), source)
1592 .unwrap();
1593 let modules: Vec<_> = output.nodes.iter()
1595 .filter(|n| n.kind == NodeKind::Module)
1596 .collect();
1597 assert!(modules.iter().any(|m| m.name == "tests"));
1598 }
1599
1600 #[test]
1601 fn parse_function_with_where_clause() {
1602 let source = r#"
1603pub fn process<T>(item: T) -> String
1604where
1605 T: std::fmt::Display + Clone,
1606{
1607 format!("{}", item)
1608}
1609"#;
1610 let output = RustFrontend::new()
1611 .parse(Path::new("test.rs"), source)
1612 .unwrap();
1613 let funcs: Vec<_> = output.nodes.iter()
1614 .filter(|n| n.kind == NodeKind::Function)
1615 .collect();
1616 assert_eq!(funcs.len(), 1);
1617 assert_eq!(funcs[0].name, "process");
1618 }
1619
1620 #[test]
1621 fn parse_call_expressions() {
1622 let source = r#"
1623fn main() {
1624 let x = foo();
1625 bar::baz();
1626 obj.method();
1627}
1628"#;
1629 let output = RustFrontend::new()
1630 .parse(Path::new("test.rs"), source)
1631 .unwrap();
1632 let calls: Vec<_> = output.edges.iter()
1633 .filter(|e| e.2.kind == EdgeKind::Calls)
1634 .collect();
1635 assert!(calls.len() >= 1);
1636 }
1637
1638 #[test]
1639 fn parse_async_fn_with_lifetime_and_generics() {
1640 let source = r#"
1641pub async fn process<'a, T: Send + Sync>(data: &'a [T]) -> Result<&'a T, Box<dyn std::error::Error>> {
1642 compute(data)
1643}
1644"#;
1645 let output = RustFrontend::new()
1646 .parse(Path::new("test.rs"), source)
1647 .unwrap();
1648
1649 let process_fn = output.nodes.iter()
1651 .find(|n| n.kind == NodeKind::Function && n.name == "process")
1652 .expect("process function not found");
1653 assert_eq!(process_fn.visibility, Visibility::Public);
1654
1655 let sig = process_fn.signature.as_ref().unwrap();
1657 assert!(sig.contains("<'a, T: Send + Sync>"), "Signature missing generics: {sig}");
1658 assert!(sig.contains("&'a [T]"), "Signature missing lifetime param: {sig}");
1659 }
1660}