Skip to main content

gitcortex_indexer/parser/
rust.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4};
5
6use gitcortex_core::{
7    error::{GitCortexError, Result},
8    graph::{Edge, Node, NodeId, NodeMetadata, Span},
9    schema::{EdgeKind, NodeKind, Visibility},
10};
11use tree_sitter::{Node as TsNode, Parser};
12
13use super::{LanguageParser, ParseResult};
14
15// ── Public parser ─────────────────────────────────────────────────────────────
16
17pub struct RustParser {
18    language: tree_sitter::Language,
19}
20
21impl RustParser {
22    pub fn new() -> Self {
23        Self {
24            language: tree_sitter_rust::LANGUAGE.into(),
25        }
26    }
27}
28
29impl Default for RustParser {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl LanguageParser for RustParser {
36    fn extensions(&self) -> &[&str] {
37        &["rs"]
38    }
39
40    fn parse(&self, path: &Path, source: &str) -> Result<ParseResult> {
41        let mut parser = Parser::new();
42        parser
43            .set_language(&self.language)
44            .map_err(|e| GitCortexError::Parse {
45                file: path.to_owned(),
46                message: e.to_string(),
47            })?;
48
49        let tree = parser
50            .parse(source, None)
51            .ok_or_else(|| GitCortexError::Parse {
52                file: path.to_owned(),
53                message: "tree-sitter returned no parse tree".into(),
54            })?;
55
56        let mut visitor = FileVisitor::new(path, source);
57        // Pass 1 — pre-allocate NodeIds for all named items so forward
58        // references (impl blocks, call sites) can resolve correctly.
59        visitor.collect_names(tree.root_node());
60        // Pass 2 — full walk: create nodes and edges.
61        visitor.visit_items(tree.root_node(), &[], None);
62        // Pass 3 — collect use declarations for Imports edges.
63        visitor.collect_imports(tree.root_node());
64
65        Ok(ParseResult {
66            nodes: visitor.nodes,
67            edges: visitor.edges,
68            deferred_calls: visitor.deferred_calls,
69            deferred_uses: visitor.deferred_uses,
70            deferred_implements: visitor.deferred_implements,
71            deferred_imports: visitor.deferred_imports,
72        })
73    }
74}
75
76// ── Internal visitor ──────────────────────────────────────────────────────────
77
78struct FileVisitor<'src> {
79    source: &'src [u8],
80    file: PathBuf,
81    nodes: Vec<Node>,
82    edges: Vec<Edge>,
83    type_index: HashMap<String, NodeId>,
84    fn_index: HashMap<String, NodeId>,
85    deferred_calls: Vec<(NodeId, String)>,
86    deferred_uses: Vec<(NodeId, String)>,
87    deferred_implements: Vec<(NodeId, String)>,
88    deferred_imports: Vec<(NodeId, String)>,
89}
90
91impl<'src> FileVisitor<'src> {
92    fn new(file: &Path, source: &'src str) -> Self {
93        Self {
94            source: source.as_bytes(),
95            file: file.to_owned(),
96            nodes: Vec::new(),
97            edges: Vec::new(),
98            type_index: HashMap::new(),
99            fn_index: HashMap::new(),
100            deferred_calls: Vec::new(),
101            deferred_uses: Vec::new(),
102            deferred_implements: Vec::new(),
103            deferred_imports: Vec::new(),
104        }
105    }
106
107    // ── Helpers ───────────────────────────────────────────────────────────────
108
109    fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
110        node.utf8_text(self.source).unwrap_or("")
111    }
112
113    fn field_text(&self, node: TsNode<'_>, field: &str) -> Option<String> {
114        node.child_by_field_name(field)
115            .and_then(|n| n.utf8_text(self.source).ok())
116            .map(str::to_owned)
117    }
118
119    fn span(node: TsNode<'_>) -> Span {
120        Span {
121            start_line: node.start_position().row as u32 + 1,
122            end_line: node.end_position().row as u32 + 1,
123        }
124    }
125
126    fn visibility(&self, node: TsNode<'_>) -> Visibility {
127        let mut cursor = node.walk();
128        for child in node.children(&mut cursor) {
129            if child.kind() == "visibility_modifier" {
130                let t = self.text(child);
131                return if t.contains("crate") {
132                    Visibility::PubCrate
133                } else {
134                    Visibility::Pub
135                };
136            }
137        }
138        Visibility::Private
139    }
140
141    fn is_async(&self, node: TsNode<'_>) -> bool {
142        let mut cursor = node.walk();
143        let result = node.children(&mut cursor).any(|c| c.kind() == "async");
144        result
145    }
146
147    fn is_unsafe(&self, node: TsNode<'_>) -> bool {
148        let mut cursor = node.walk();
149        let result = node.children(&mut cursor).any(|c| c.kind() == "unsafe");
150        result
151    }
152
153    fn qualified(scope: &[String], name: &str) -> String {
154        if scope.is_empty() {
155            format!("crate::{name}")
156        } else {
157            format!("crate::{}::{name}", scope.join("::"))
158        }
159    }
160
161    fn make_node(
162        &self,
163        id: NodeId,
164        kind: NodeKind,
165        name: String,
166        scope: &[String],
167        ts_node: TsNode<'_>,
168    ) -> Node {
169        Node {
170            id,
171            qualified_name: Self::qualified(scope, &name),
172            kind,
173            name,
174            file: self.file.clone(),
175            span: Self::span(ts_node),
176            metadata: NodeMetadata {
177                loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
178                visibility: self.visibility(ts_node),
179                is_async: self.is_async(ts_node),
180                is_unsafe: self.is_unsafe(ts_node),
181                ..Default::default()
182            },
183        }
184    }
185
186    fn type_name(&self, node: TsNode<'_>) -> Option<String> {
187        match node.kind() {
188            "type_identifier" => Some(self.text(node).to_owned()),
189            "generic_type" => node
190                .child_by_field_name("type")
191                .map(|n| self.text(n).to_owned()),
192            "scoped_type_identifier" => node
193                .child_by_field_name("name")
194                .map(|n| self.text(n).to_owned()),
195            "reference_type" => node
196                .child_by_field_name("type")
197                .and_then(|n| self.type_name(n)),
198            "mutable_specifier" => None,
199            _ => Some(self.text(node).to_owned()),
200        }
201    }
202
203    // ── Pass 1: pre-allocate NodeIds for all named items ─────────────────────
204
205    fn collect_names(&mut self, node: TsNode<'_>) {
206        let mut cursor = node.walk();
207        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
208        for child in children {
209            match child.kind() {
210                "struct_item" | "enum_item" | "trait_item" => {
211                    if let Some(name) = self.field_text(child, "name") {
212                        self.type_index.entry(name).or_default();
213                    }
214                }
215                "function_item" => {
216                    if let Some(name) = self.field_text(child, "name") {
217                        self.fn_index.entry(name).or_default();
218                    }
219                }
220                "impl_item" => {
221                    // Methods are not pre-allocated — they can share names across
222                    // multiple impl blocks (e.g. fmt in Display and Debug).
223                    // Methods are never targets of bare call_expression resolution.
224                }
225                "mod_item" => {
226                    if let Some(body) = child.child_by_field_name("body") {
227                        self.collect_names(body);
228                    }
229                }
230                _ => {}
231            }
232        }
233    }
234
235    // ── Pass 2: full AST walk ─────────────────────────────────────────────────
236
237    fn visit_items(&mut self, parent: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
238        let mut cursor = parent.walk();
239        let children: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
240        for child in children {
241            self.visit_item(child, scope, container_id.clone());
242        }
243    }
244
245    fn visit_item(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
246        match node.kind() {
247            "function_item" => self.visit_function(node, scope, container_id, NodeKind::Function),
248            "struct_item" => self.visit_type_item(node, scope, container_id, NodeKind::Struct),
249            "enum_item" => self.visit_type_item(node, scope, container_id, NodeKind::Enum),
250            "trait_item" => self.visit_trait(node, scope, container_id),
251            "impl_item" => self.visit_impl(node, scope),
252            "mod_item" => self.visit_mod(node, scope, container_id),
253            "const_item" | "static_item" => self.visit_const(node, scope, container_id),
254            "type_item" => self.visit_type_alias(node, scope, container_id),
255            "macro_definition" => self.visit_macro_def(node, scope, container_id),
256            _ => {}
257        }
258    }
259
260    fn visit_function(
261        &mut self,
262        node: TsNode<'_>,
263        scope: &[String],
264        container_id: Option<NodeId>,
265        kind: NodeKind,
266    ) {
267        let Some(name) = self.field_text(node, "name") else {
268            return;
269        };
270        // Methods always get a fresh ID — same method name can appear in multiple
271        // impl blocks (e.g. `fmt` in Display and Debug) and bare-name call resolution
272        // doesn't apply to methods. Free functions use the fn_index for deferred calls.
273        let id = if kind == NodeKind::Method {
274            NodeId::new()
275        } else {
276            self.fn_index
277                .get(&name)
278                .cloned()
279                .unwrap_or_else(NodeId::new)
280        };
281        let graph_node = self.make_node(id.clone(), kind, name, scope, node);
282
283        if let Some(cid) = container_id {
284            self.edges.push(Edge {
285                src: cid,
286                dst: id.clone(),
287                kind: EdgeKind::Contains,
288            });
289        }
290
291        // Uses edges: parameter types and return type referencing same-file types.
292        self.collect_uses_edges(node, &id);
293
294        self.nodes.push(graph_node);
295
296        // Walk the function body for call sites.
297        if let Some(body) = node.child_by_field_name("body") {
298            self.collect_calls(body, &id);
299        }
300    }
301
302    /// Create `Uses` edges for each parameter/return type. Intra-file types
303    /// resolve immediately; cross-file types go into `deferred_uses`.
304    fn collect_uses_edges(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
305        let mut type_names: Vec<String> = Vec::new();
306
307        if let Some(params) = fn_node.child_by_field_name("parameters") {
308            let mut cursor = params.walk();
309            for param in params.named_children(&mut cursor) {
310                if param.kind() == "parameter" {
311                    if let Some(type_node) = param.child_by_field_name("type") {
312                        if let Some(tname) = self.type_name(type_node) {
313                            type_names.push(tname);
314                        }
315                    }
316                }
317            }
318        }
319
320        if let Some(ret_type) = fn_node.child_by_field_name("return_type") {
321            if let Some(tname) = self.type_name(ret_type) {
322                type_names.push(tname);
323            }
324        }
325
326        for tname in type_names {
327            if let Some(tid) = self.type_index.get(&tname).cloned() {
328                self.edges.push(Edge {
329                    src: fn_id.clone(),
330                    dst: tid,
331                    kind: EdgeKind::Uses,
332                });
333            } else if !tname.is_empty()
334                && !is_primitive(&tname)
335                && !self
336                    .deferred_uses
337                    .iter()
338                    .any(|(id, n)| id == fn_id && n == &tname)
339            {
340                self.deferred_uses.push((fn_id.clone(), tname));
341            }
342        }
343    }
344
345    /// Recursively walk an expression node collecting call sites.
346    ///
347    /// Called both on container nodes (blocks, statements) and directly on
348    /// call/method-call nodes when recursing into chained receivers. We match
349    /// on `node.kind()` first so the node itself is never silently skipped.
350    fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
351        match node.kind() {
352            "call_expression" => {
353                if let Some(callee) = self.callee_name(node) {
354                    self.record_call(caller_id.clone(), callee);
355                }
356                if let Some(args) = node.child_by_field_name("arguments") {
357                    self.collect_calls(args, caller_id);
358                }
359                // For chained calls like `store.apply_diff(...).context("x")`,
360                // tree-sitter represents .context as a call_expression whose
361                // `function` is a field_expression whose `value` is the inner
362                // call_expression for .apply_diff. Recurse into that value.
363                if let Some(func) = node.child_by_field_name("function") {
364                    if let Some(value) = func.child_by_field_name("value") {
365                        self.collect_calls(value, caller_id);
366                    }
367                }
368            }
369            "method_call_expression" => {
370                // tree-sitter-rust uses call_expression for dot-call syntax,
371                // but method_call_expression may appear for other constructs.
372                if let Some(name_node) = node.child_by_field_name("name") {
373                    let method = self.text(name_node).to_owned();
374                    self.record_call(caller_id.clone(), method);
375                }
376                if let Some(args) = node.child_by_field_name("arguments") {
377                    self.collect_calls(args, caller_id);
378                }
379                if let Some(recv) = node.child_by_field_name("receiver") {
380                    self.collect_calls(recv, caller_id);
381                }
382            }
383            _ => {
384                let mut cursor = node.walk();
385                let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
386                for child in children {
387                    self.collect_calls(child, caller_id);
388                }
389            }
390        }
391    }
392
393    /// Extract the simple callee name from a `call_expression` function field.
394    fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
395        let func = call_expr.child_by_field_name("function")?;
396        match func.kind() {
397            "identifier" => Some(self.text(func).to_owned()),
398            "scoped_identifier" => func
399                .child_by_field_name("name")
400                .and_then(|n| n.utf8_text(self.source).ok())
401                .map(str::to_owned),
402            "field_expression" => func
403                .child_by_field_name("field")
404                .and_then(|n| n.utf8_text(self.source).ok())
405                .map(str::to_owned),
406            _ => None,
407        }
408    }
409
410    /// Resolve a call: create an intra-file edge or push to deferred list.
411    fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
412        if callee_name.is_empty() {
413            return;
414        }
415        if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
416            let edge = Edge {
417                src: caller_id,
418                dst: callee_id,
419                kind: EdgeKind::Calls,
420            };
421            if !self.edges.contains(&edge) {
422                self.edges.push(edge);
423            }
424        } else if !self
425            .deferred_calls
426            .iter()
427            .any(|(c, n)| c == &caller_id && n == &callee_name)
428        {
429            self.deferred_calls.push((caller_id, callee_name));
430        }
431    }
432
433    fn visit_type_item(
434        &mut self,
435        node: TsNode<'_>,
436        scope: &[String],
437        container_id: Option<NodeId>,
438        kind: NodeKind,
439    ) {
440        let Some(name) = self.field_text(node, "name") else {
441            return;
442        };
443        let id = self
444            .type_index
445            .get(&name)
446            .cloned()
447            .unwrap_or_else(NodeId::new);
448        let graph_node = self.make_node(id.clone(), kind, name, scope, node);
449        if let Some(cid) = container_id {
450            self.edges.push(Edge {
451                src: cid,
452                dst: id.clone(),
453                kind: EdgeKind::Contains,
454            });
455        }
456        self.nodes.push(graph_node);
457    }
458
459    fn visit_trait(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
460        let Some(name) = self.field_text(node, "name") else {
461            return;
462        };
463        let id = self
464            .type_index
465            .get(&name)
466            .cloned()
467            .unwrap_or_else(NodeId::new);
468        let graph_node = self.make_node(id.clone(), NodeKind::Trait, name.clone(), scope, node);
469        if let Some(cid) = container_id {
470            self.edges.push(Edge {
471                src: cid,
472                dst: id.clone(),
473                kind: EdgeKind::Contains,
474            });
475        }
476        self.nodes.push(graph_node);
477
478        if let Some(body) = node.child_by_field_name("body") {
479            let mut new_scope = scope.to_vec();
480            new_scope.push(name);
481            self.visit_items(body, &new_scope, Some(id));
482        }
483    }
484
485    fn visit_impl(&mut self, node: TsNode<'_>, scope: &[String]) {
486        let type_node = node.child_by_field_name("type");
487        let type_name = type_node.and_then(|n| self.type_name(n));
488        let Some(type_name) = type_name else { return };
489        let type_id = self.type_index.get(&type_name).cloned();
490
491        if let Some(trait_node) = node.child_by_field_name("trait") {
492            if let Some(trait_name) = self.type_name(trait_node) {
493                let trait_id = self.type_index.get(&trait_name).cloned();
494                match (type_id.clone(), trait_id) {
495                    (Some(tid), Some(trid)) => {
496                        self.edges.push(Edge {
497                            src: tid,
498                            dst: trid,
499                            kind: EdgeKind::Implements,
500                        });
501                    }
502                    (Some(tid), None)
503                        if !is_primitive(&trait_name)
504                            && !self
505                                .deferred_implements
506                                .iter()
507                                .any(|(id, n)| id == &tid && n == &trait_name) =>
508                    {
509                        self.deferred_implements.push((tid, trait_name));
510                    }
511                    _ => {}
512                }
513            }
514        }
515
516        if let Some(body) = node.child_by_field_name("body") {
517            let mut cursor = body.walk();
518            let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
519            let mut impl_scope = scope.to_vec();
520            impl_scope.push(type_name);
521
522            for child in children {
523                if child.kind() == "function_item" {
524                    self.visit_function(child, &impl_scope, type_id.clone(), NodeKind::Method);
525                }
526            }
527        }
528    }
529
530    fn visit_mod(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
531        let Some(name) = self.field_text(node, "name") else {
532            return;
533        };
534        let id = NodeId::new();
535        let graph_node = self.make_node(id.clone(), NodeKind::Module, name.clone(), scope, node);
536        if let Some(cid) = container_id {
537            self.edges.push(Edge {
538                src: cid,
539                dst: id.clone(),
540                kind: EdgeKind::Contains,
541            });
542        }
543        self.nodes.push(graph_node);
544
545        if let Some(body) = node.child_by_field_name("body") {
546            let mut new_scope = scope.to_vec();
547            new_scope.push(name);
548            self.visit_items(body, &new_scope, Some(id));
549        }
550    }
551
552    fn visit_const(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
553        let Some(name) = self.field_text(node, "name") else {
554            return;
555        };
556        let id = NodeId::new();
557        let graph_node = self.make_node(id.clone(), NodeKind::Constant, name, scope, node);
558        if let Some(cid) = container_id {
559            self.edges.push(Edge {
560                src: cid,
561                dst: id.clone(),
562                kind: EdgeKind::Contains,
563            });
564        }
565        self.nodes.push(graph_node);
566    }
567
568    fn visit_type_alias(
569        &mut self,
570        node: TsNode<'_>,
571        scope: &[String],
572        container_id: Option<NodeId>,
573    ) {
574        let Some(name) = self.field_text(node, "name") else {
575            return;
576        };
577        let id = NodeId::new();
578        let graph_node = self.make_node(id.clone(), NodeKind::TypeAlias, name, scope, node);
579        if let Some(cid) = container_id {
580            self.edges.push(Edge {
581                src: cid,
582                dst: id.clone(),
583                kind: EdgeKind::Contains,
584            });
585        }
586        self.nodes.push(graph_node);
587    }
588
589    fn visit_macro_def(
590        &mut self,
591        node: TsNode<'_>,
592        scope: &[String],
593        container_id: Option<NodeId>,
594    ) {
595        let Some(name) = self.field_text(node, "name") else {
596            return;
597        };
598        let id = NodeId::new();
599        let graph_node = self.make_node(id.clone(), NodeKind::Macro, name, scope, node);
600        if let Some(cid) = container_id {
601            self.edges.push(Edge {
602                src: cid,
603                dst: id.clone(),
604                kind: EdgeKind::Contains,
605            });
606        }
607        self.nodes.push(graph_node);
608    }
609
610    /// Walk `use_declaration` nodes in the AST root and record deferred imports.
611    /// We emit one Imports edge per leaf identifier that is not a primitive.
612    fn collect_imports(&mut self, root: TsNode<'_>) {
613        let mut cursor = root.walk();
614        for child in root.named_children(&mut cursor) {
615            if child.kind() == "use_declaration" {
616                // Find the use_as_clause or use_list or scoped_identifier etc.
617                if let Some(arg) = child.child_by_field_name("argument") {
618                    self.collect_import_leaves(arg);
619                }
620            } else if child.kind() == "mod_item" {
621                if let Some(body) = child.child_by_field_name("body") {
622                    self.collect_imports(body);
623                }
624            }
625        }
626    }
627
628    fn collect_import_leaves(&mut self, node: TsNode<'_>) {
629        match node.kind() {
630            "identifier" | "type_identifier" => {
631                let name = self.text(node).to_owned();
632                if !name.is_empty()
633                    && !is_primitive(&name)
634                    && name != "self"
635                    && name != "super"
636                    && name != "crate"
637                {
638                    // Use file-level placeholder NodeId — resolved to real nodes in indexer.
639                    let placeholder = NodeId::new();
640                    self.deferred_imports.push((placeholder, name));
641                }
642            }
643            "use_list" => {
644                let mut cursor = node.walk();
645                for child in node.named_children(&mut cursor) {
646                    self.collect_import_leaves(child);
647                }
648            }
649            "scoped_identifier" | "scoped_use_list" => {
650                // Recurse to get the leaf name.
651                let mut cursor = node.walk();
652                for child in node.named_children(&mut cursor) {
653                    self.collect_import_leaves(child);
654                }
655            }
656            "use_as_clause" => {
657                // `use foo as bar` — record bar (the alias).
658                if let Some(alias) = node.child_by_field_name("alias") {
659                    self.collect_import_leaves(alias);
660                }
661            }
662            _ => {}
663        }
664    }
665}
666
667fn is_primitive(name: &str) -> bool {
668    matches!(
669        name,
670        "bool"
671            | "char"
672            | "str"
673            | "i8"
674            | "i16"
675            | "i32"
676            | "i64"
677            | "i128"
678            | "isize"
679            | "u8"
680            | "u16"
681            | "u32"
682            | "u64"
683            | "u128"
684            | "usize"
685            | "f32"
686            | "f64"
687            | "String"
688            | "Vec"
689            | "Option"
690            | "Result"
691            | "Box"
692            | "Rc"
693            | "Arc"
694            | "Cell"
695            | "RefCell"
696            | "Cow"
697            | "HashMap"
698            | "HashSet"
699            | "BTreeMap"
700            | "BTreeSet"
701            | "PathBuf"
702            | "Path"
703            | "OsString"
704            | "OsStr"
705            | "Send"
706            | "Sync"
707            | "Sized"
708            | "Clone"
709            | "Copy"
710            | "Debug"
711            | "Display"
712            | "Default"
713            | "PartialEq"
714            | "Eq"
715            | "PartialOrd"
716            | "Ord"
717            | "Hash"
718            | "Iterator"
719            | "Into"
720            | "From"
721            | "AsRef"
722            | "AsMut"
723            | "Deref"
724            | "DerefMut"
725            | "Error"
726            | "Write"
727            | "Read"
728            | "Seek"
729            | "Self"
730            | "()"
731            | "_"
732    )
733}
734
735// ── Tests ─────────────────────────────────────────────────────────────────────
736
737#[cfg(test)]
738mod tests {
739    use std::path::Path;
740
741    use gitcortex_core::{
742        graph::{Edge, Node},
743        schema::{EdgeKind, NodeKind},
744    };
745
746    use super::RustParser;
747    use crate::parser::LanguageParser;
748
749    fn parse(src: &str) -> (Vec<Node>, Vec<Edge>) {
750        let r = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
751        (r.nodes, r.edges)
752    }
753
754    #[test]
755    fn parses_free_function() {
756        let (nodes, _) = parse("pub fn greet(name: &str) -> String { name.into() }");
757        assert_eq!(nodes.len(), 1);
758        assert_eq!(nodes[0].kind, NodeKind::Function);
759        assert_eq!(nodes[0].name, "greet");
760    }
761
762    #[test]
763    fn parses_struct() {
764        let (nodes, _) = parse("pub struct Person { pub name: String }");
765        let structs: Vec<_> = nodes
766            .iter()
767            .filter(|n| n.kind == NodeKind::Struct)
768            .collect();
769        assert_eq!(structs.len(), 1);
770        assert_eq!(structs[0].name, "Person");
771    }
772
773    #[test]
774    fn parses_trait_impl_and_method() {
775        let src = r#"
776pub trait Greet { fn greet(&self) -> String; }
777pub struct Person { pub name: String }
778impl Greet for Person {
779    fn greet(&self) -> String { self.name.clone() }
780}
781"#;
782        let (nodes, edges) = parse(src);
783
784        let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
785        let structs: Vec<_> = nodes
786            .iter()
787            .filter(|n| n.kind == NodeKind::Struct)
788            .collect();
789        let methods: Vec<_> = nodes
790            .iter()
791            .filter(|n| n.kind == NodeKind::Method)
792            .collect();
793        let impl_edges: Vec<_> = edges
794            .iter()
795            .filter(|e| e.kind == EdgeKind::Implements)
796            .collect();
797
798        assert_eq!(traits.len(), 1, "expected Greet trait");
799        assert_eq!(structs.len(), 1, "expected Person struct");
800        assert_eq!(methods.len(), 1, "expected greet method");
801        assert_eq!(impl_edges.len(), 1, "expected Implements edge");
802    }
803
804    #[test]
805    fn parses_module_with_items() {
806        let src = r#"
807pub mod utils {
808    pub fn helper() {}
809    pub struct Config {}
810}
811"#;
812        let (nodes, edges) = parse(src);
813
814        let mods: Vec<_> = nodes
815            .iter()
816            .filter(|n| n.kind == NodeKind::Module)
817            .collect();
818        let fns: Vec<_> = nodes
819            .iter()
820            .filter(|n| n.kind == NodeKind::Function)
821            .collect();
822        let contains: Vec<_> = edges
823            .iter()
824            .filter(|e| e.kind == EdgeKind::Contains)
825            .collect();
826
827        assert_eq!(mods.len(), 1, "expected utils module");
828        assert_eq!(fns.len(), 1, "expected helper function");
829        assert!(!contains.is_empty(), "expected Contains edges");
830    }
831
832    #[test]
833    fn qualified_name_includes_module_path() {
834        let src = r#"
835pub mod inner {
836    pub fn foo() {}
837}
838"#;
839        let (nodes, _) = parse(src);
840        let foo = nodes.iter().find(|n| n.name == "foo").unwrap();
841        assert_eq!(foo.qualified_name, "crate::inner::foo");
842    }
843
844    #[test]
845    fn detects_intra_file_calls() {
846        let src = r#"
847pub fn caller() { callee(); }
848pub fn callee() {}
849"#;
850        let (_, edges) = parse(src);
851        let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
852        assert_eq!(calls.len(), 1, "expected one Calls edge");
853    }
854
855    #[test]
856    fn detects_uses_edges_for_param_types() {
857        let src = r#"
858pub struct Config {}
859pub fn run(cfg: Config) {}
860"#;
861        let (_, edges) = parse(src);
862        let uses: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Uses).collect();
863        assert_eq!(uses.len(), 1, "expected one Uses edge from run to Config");
864    }
865
866    #[test]
867    fn deferred_calls_capture_unknown_callees() {
868        let src = r#"
869pub fn caller() { external_fn(); }
870"#;
871        let result = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
872        assert_eq!(result.deferred_calls.len(), 1);
873        assert_eq!(result.deferred_calls[0].1, "external_fn");
874    }
875}