Skip to main content

graphy_parser/
rust_lang.rs

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
13/// Frontend for Rust (.rs) files.
14pub 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        // Create the file node
38        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        // Walk top-level children
59        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            // Top-level attributes (like #![...]) — skip for now
108        }
109        "macro_definition" => {
110            extract_macro_def(node, source, path, parent_id, output);
111        }
112        _ => {}
113    }
114}
115
116// ── Functions ───────────────────────────────────────────────
117
118fn 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    // Extract parameters
172    if let Some(params) = node.child_by_field_name("parameters") {
173        extract_parameters(&params, source, path, func_id, output);
174    }
175
176    // Extract return type
177    if let Some(ret) = node.child_by_field_name("return_type") {
178        let type_name = node_text(&ret, source);
179        // Strip leading `-> ` if present
180        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    // Walk function body for calls
196    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
201    extract_attributes_as_decorators(node, source, path, func_id, output);
202}
203
204// ── Structs ─────────────────────────────────────────────────
205
206fn 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    // Extract struct fields — prefer the `body` field name, fallback to
240    // searching for field_declaration_list among children
241    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 derive macros and other attributes as decorators
255    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                // Extract field type
292                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
310// ── Enums ───────────────────────────────────────────────────
311
312fn 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    // Extract enum variants
346    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 derive macros
368    extract_attributes_as_decorators(node, source, path, enum_id, output);
369}
370
371// ── Traits ──────────────────────────────────────────────────
372
373fn 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    // Extract trait methods (function signatures in body)
407    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    // Extract supertraits (trait bounds after colon)
429    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    // Extract parameters
490    if let Some(params) = node.child_by_field_name("parameters") {
491        extract_parameters(&params, source, path, method_id, output);
492    }
493
494    // Extract return type
495    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
513// ── Impl Blocks ─────────────────────────────────────────────
514
515fn extract_impl(
516    node: &Node,
517    source: &[u8],
518    path: &Path,
519    _parent_id: SymbolId,
520    output: &mut ParseOutput,
521) {
522    // Determine the type being implemented and optional trait
523    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    // Try to find the existing struct/enum/trait definition in the output
539    // so impl block methods attach to the real definition node, not duplicates.
540    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        // No definition found (e.g., impl for external type) — create a synthetic node
551        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 this is a trait impl, create the Implements edge
564    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    // Walk impl body for methods
578    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
597// ── Type Aliases ────────────────────────────────────────────
598
599fn 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
633// ── Constants ───────────────────────────────────────────────
634
635fn 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    // Extract type annotation
668    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
683// ── Statics ─────────────────────────────────────────────────
684
685fn 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
718// ── Modules ─────────────────────────────────────────────────
719
720fn 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 (e.g. #[cfg(test)]) as decorators on the module
754    extract_attributes_as_decorators(node, source, path, mod_id, output);
755
756    // If this is an inline module (has a body), walk its children
757    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
765// ── Use Declarations ────────────────────────────────────────
766
767fn 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    // Extract the path being imported
779    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                // Recurse for nested structures
840                if child.child_count() > 0 {
841                    collect_use_items(&child, source, items);
842                }
843            }
844        }
845    }
846}
847
848// ── Macro definitions ───────────────────────────────────────
849
850fn 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
882// ── Parameters ──────────────────────────────────────────────
883
884fn 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(&param, source)
902            }
903            "identifier" => node_text(&param, 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(&param),
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        // Extract parameter type
923        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
939// ── Attributes / Decorators ─────────────────────────────────
940
941fn extract_attributes_as_decorators(
942    node: &Node,
943    source: &[u8],
944    path: &Path,
945    target_id: SymbolId,
946    output: &mut ParseOutput,
947) {
948    // Look for attribute_item siblings preceding this node
949    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            // Strip outer #[...] or #![...]
954            let inner = attr_text
955                .trim_start_matches("#![")
956                .trim_start_matches("#[")
957                .trim_end_matches(']')
958                .trim()
959                .to_string();
960
961            // If it's a derive, expand into individual derive decorators
962            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            // Skip doc comments (they precede attributes sometimes)
997            prev = sibling.prev_sibling();
998        } else {
999            break;
1000        }
1001    }
1002}
1003
1004// ── Call extraction ─────────────────────────────────────────
1005
1006fn 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        // Also handle macro invocations
1044        if node.kind() == "macro_invocation" {
1045            if let Some(macro_node) = node.child_by_field_name("macro") {
1046                let macro_name = node_text(&macro_node, source);
1047                if !is_noise_macro(&macro_name) {
1048                    let call_target = GirNode::new(
1049                        format!("{macro_name}!"),
1050                        NodeKind::Function,
1051                        path.to_path_buf(),
1052                        node_span(&macro_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        // Don't recurse into nested function definitions.
1069        // Closures ARE traversed because they don't get their own GirNode —
1070        // their calls should be attributed to the enclosing function.
1071        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
1081// ── Helpers ─────────────────────────────────────────────────
1082
1083fn extract_visibility(node: &Node, source: &[u8]) -> Visibility {
1084    // Look for a visibility_modifier child
1085    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    // Rust doc comments are `///` or `//!` comment lines preceding the item
1127    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            // Skip attributes between doc comments and the item
1146            prev = sibling.prev_sibling();
1147            continue;
1148        }
1149        break;
1150    }
1151
1152    if doc_lines.is_empty() {
1153        // Also check for block doc comments: /** ... */
1154        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    // Reverse because we collected from bottom to top
1177    doc_lines.reverse();
1178    Some(doc_lines.join("\n"))
1179}
1180
1181/// Bare function/macro names that are language builtins (no receiver).
1182/// Method chains on variables are handled by `is_noise_method_call()`.
1183fn 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        // Check derive decorators
1265        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        // Check that there's an Implements edge
1393        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    // ── Edge case tests ───────────────────────────────────
1515
1516    #[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        // Should parse the test module and the test function
1594        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        // The real function definition + phantom call target for compute()
1650        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        // Signature should contain the generics and lifetime
1656        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}