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