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            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                ..Default::default()
271            },
272        }
273    }
274
275    fn type_name(&self, node: TsNode<'_>) -> Option<String> {
276        match node.kind() {
277            "type_identifier" => Some(self.text(node).to_owned()),
278            "generic_type" => node
279                .child_by_field_name("type")
280                .map(|n| self.text(n).to_owned()),
281            "scoped_type_identifier" => node
282                .child_by_field_name("name")
283                .map(|n| self.text(n).to_owned()),
284            "reference_type" => node
285                .child_by_field_name("type")
286                .and_then(|n| self.type_name(n)),
287            "mutable_specifier" => None,
288            _ => Some(self.text(node).to_owned()),
289        }
290    }
291
292    // ── Pass 1: pre-allocate NodeIds for all named items ─────────────────────
293
294    fn collect_names(&mut self, node: TsNode<'_>) {
295        let mut cursor = node.walk();
296        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
297        for child in children {
298            match child.kind() {
299                "struct_item" | "enum_item" | "trait_item" => {
300                    if let Some(name) = self.field_text(child, "name") {
301                        self.type_index.entry(name).or_default();
302                    }
303                }
304                "function_item" => {
305                    if let Some(name) = self.field_text(child, "name") {
306                        self.fn_index.entry(name).or_default();
307                    }
308                }
309                "impl_item" => {
310                    // Methods are not pre-allocated — they can share names across
311                    // multiple impl blocks (e.g. fmt in Display and Debug).
312                    // Methods are never targets of bare call_expression resolution.
313                }
314                "mod_item" => {
315                    if let Some(body) = child.child_by_field_name("body") {
316                        self.collect_names(body);
317                    }
318                }
319                _ => {}
320            }
321        }
322    }
323
324    // ── Pass 2: full AST walk ─────────────────────────────────────────────────
325
326    fn visit_items(&mut self, parent: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
327        let mut cursor = parent.walk();
328        let children: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
329        for child in children {
330            self.visit_item(child, scope, container_id.clone());
331        }
332    }
333
334    fn visit_item(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
335        match node.kind() {
336            "function_item" => self.visit_function(node, scope, container_id, NodeKind::Function),
337            "struct_item" => self.visit_type_item(node, scope, container_id, NodeKind::Struct),
338            "enum_item" => self.visit_type_item(node, scope, container_id, NodeKind::Enum),
339            "trait_item" => self.visit_trait(node, scope, container_id),
340            "impl_item" => self.visit_impl(node, scope),
341            "mod_item" => self.visit_mod(node, scope, container_id),
342            "const_item" | "static_item" => self.visit_const(node, scope, container_id),
343            "type_item" => self.visit_type_alias(node, scope, container_id),
344            "macro_definition" => self.visit_macro_def(node, scope, container_id),
345            _ => {}
346        }
347    }
348
349    fn visit_function(
350        &mut self,
351        node: TsNode<'_>,
352        scope: &[String],
353        container_id: Option<NodeId>,
354        kind: NodeKind,
355    ) {
356        let Some(name) = self.field_text(node, "name") else {
357            return;
358        };
359        // Methods always get a fresh ID — same method name can appear in multiple
360        // impl blocks (e.g. `fmt` in Display and Debug) and bare-name call resolution
361        // doesn't apply to methods. Free functions use the fn_index for deferred calls.
362        let id = if kind == NodeKind::Method {
363            NodeId::new()
364        } else {
365            self.fn_index
366                .get(&name)
367                .cloned()
368                .unwrap_or_else(NodeId::new)
369        };
370        let graph_node = self.make_node(id.clone(), kind, name, scope, node);
371
372        if let Some(cid) = container_id {
373            self.edges.push(Edge {
374                src: cid,
375                dst: id.clone(),
376                kind: EdgeKind::Contains,
377            });
378        }
379
380        // Uses edges: parameter types and return type referencing same-file types.
381        self.collect_uses_edges(node, &id);
382
383        // Attribute annotations → deferred_annotated.
384        for attr_name in self.collect_attributes(node) {
385            self.deferred_annotated.push((id.clone(), attr_name));
386        }
387
388        self.nodes.push(graph_node);
389
390        // Walk the function body for call sites.
391        if let Some(body) = node.child_by_field_name("body") {
392            self.collect_calls(body, &id);
393        }
394    }
395
396    /// Create `Uses` edges for each parameter/return type. Intra-file types
397    /// resolve immediately; cross-file types go into `deferred_uses`.
398    fn collect_uses_edges(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
399        let mut type_names: Vec<String> = Vec::new();
400
401        if let Some(params) = fn_node.child_by_field_name("parameters") {
402            let mut cursor = params.walk();
403            for param in params.named_children(&mut cursor) {
404                if param.kind() == "parameter" {
405                    if let Some(type_node) = param.child_by_field_name("type") {
406                        if let Some(tname) = self.type_name(type_node) {
407                            type_names.push(tname);
408                        }
409                    }
410                }
411            }
412        }
413
414        if let Some(ret_type) = fn_node.child_by_field_name("return_type") {
415            if let Some(tname) = self.type_name(ret_type) {
416                type_names.push(tname);
417            }
418        }
419
420        for tname in type_names {
421            if let Some(tid) = self.type_index.get(&tname).cloned() {
422                self.edges.push(Edge {
423                    src: fn_id.clone(),
424                    dst: tid,
425                    kind: EdgeKind::Uses,
426                });
427            } else if !tname.is_empty()
428                && !is_primitive(&tname)
429                && !self
430                    .deferred_uses
431                    .iter()
432                    .any(|(id, n)| id == fn_id && n == &tname)
433            {
434                self.deferred_uses.push((fn_id.clone(), tname));
435            }
436        }
437    }
438
439    /// Recursively walk an expression node collecting call sites.
440    ///
441    /// Called both on container nodes (blocks, statements) and directly on
442    /// call/method-call nodes when recursing into chained receivers. We match
443    /// on `node.kind()` first so the node itself is never silently skipped.
444    fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
445        match node.kind() {
446            "call_expression" => {
447                if let Some(callee) = self.callee_name(node) {
448                    self.record_call(caller_id.clone(), callee);
449                }
450                if let Some(args) = node.child_by_field_name("arguments") {
451                    self.collect_calls(args, caller_id);
452                }
453                // For chained calls like `store.apply_diff(...).context("x")`,
454                // tree-sitter represents .context as a call_expression whose
455                // `function` is a field_expression whose `value` is the inner
456                // call_expression for .apply_diff. Recurse into that value.
457                if let Some(func) = node.child_by_field_name("function") {
458                    if let Some(value) = func.child_by_field_name("value") {
459                        self.collect_calls(value, caller_id);
460                    }
461                }
462            }
463            "method_call_expression" => {
464                // tree-sitter-rust uses call_expression for dot-call syntax,
465                // but method_call_expression may appear for other constructs.
466                if let Some(name_node) = node.child_by_field_name("name") {
467                    let method = self.text(name_node).to_owned();
468                    self.record_call(caller_id.clone(), method);
469                }
470                if let Some(args) = node.child_by_field_name("arguments") {
471                    self.collect_calls(args, caller_id);
472                }
473                if let Some(recv) = node.child_by_field_name("receiver") {
474                    self.collect_calls(recv, caller_id);
475                }
476            }
477            _ => {
478                let mut cursor = node.walk();
479                let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
480                for child in children {
481                    self.collect_calls(child, caller_id);
482                }
483            }
484        }
485    }
486
487    /// Extract the simple callee name from a `call_expression` function field.
488    fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
489        let func = call_expr.child_by_field_name("function")?;
490        match func.kind() {
491            "identifier" => Some(self.text(func).to_owned()),
492            "scoped_identifier" => func
493                .child_by_field_name("name")
494                .and_then(|n| n.utf8_text(self.source).ok())
495                .map(str::to_owned),
496            "field_expression" => func
497                .child_by_field_name("field")
498                .and_then(|n| n.utf8_text(self.source).ok())
499                .map(str::to_owned),
500            _ => None,
501        }
502    }
503
504    /// Resolve a call: create an intra-file edge or push to deferred list.
505    fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
506        if callee_name.is_empty() {
507            return;
508        }
509        if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
510            let edge = Edge {
511                src: caller_id,
512                dst: callee_id,
513                kind: EdgeKind::Calls,
514            };
515            if !self.edges.contains(&edge) {
516                self.edges.push(edge);
517            }
518        } else if !self
519            .deferred_calls
520            .iter()
521            .any(|(c, n)| c == &caller_id && n == &callee_name)
522        {
523            self.deferred_calls.push((caller_id, callee_name));
524        }
525    }
526
527    fn visit_type_item(
528        &mut self,
529        node: TsNode<'_>,
530        scope: &[String],
531        container_id: Option<NodeId>,
532        kind: NodeKind,
533    ) {
534        let Some(name) = self.field_text(node, "name") else {
535            return;
536        };
537        let id = self
538            .type_index
539            .get(&name)
540            .cloned()
541            .unwrap_or_else(NodeId::new);
542        let graph_node = self.make_node(id.clone(), kind, name, scope, node);
543        if let Some(cid) = container_id {
544            self.edges.push(Edge {
545                src: cid,
546                dst: id.clone(),
547                kind: EdgeKind::Contains,
548            });
549        }
550        for attr_name in self.collect_attributes(node) {
551            self.deferred_annotated.push((id.clone(), attr_name));
552        }
553        self.nodes.push(graph_node);
554    }
555
556    fn visit_trait(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
557        let Some(name) = self.field_text(node, "name") else {
558            return;
559        };
560        let id = self
561            .type_index
562            .get(&name)
563            .cloned()
564            .unwrap_or_else(NodeId::new);
565        let graph_node = self.make_node(id.clone(), NodeKind::Trait, name.clone(), scope, node);
566        if let Some(cid) = container_id {
567            self.edges.push(Edge {
568                src: cid,
569                dst: id.clone(),
570                kind: EdgeKind::Contains,
571            });
572        }
573        for attr_name in self.collect_attributes(node) {
574            self.deferred_annotated.push((id.clone(), attr_name));
575        }
576        self.nodes.push(graph_node);
577
578        if let Some(body) = node.child_by_field_name("body") {
579            let mut new_scope = scope.to_vec();
580            new_scope.push(name);
581            self.visit_items(body, &new_scope, Some(id));
582        }
583    }
584
585    fn visit_impl(&mut self, node: TsNode<'_>, scope: &[String]) {
586        let type_node = node.child_by_field_name("type");
587        let type_name = type_node.and_then(|n| self.type_name(n));
588        let Some(type_name) = type_name else { return };
589        let type_id = self.type_index.get(&type_name).cloned();
590
591        if let Some(trait_node) = node.child_by_field_name("trait") {
592            if let Some(trait_name) = self.type_name(trait_node) {
593                let trait_id = self.type_index.get(&trait_name).cloned();
594                match (type_id.clone(), trait_id) {
595                    (Some(tid), Some(trid)) => {
596                        self.edges.push(Edge {
597                            src: tid,
598                            dst: trid,
599                            kind: EdgeKind::Implements,
600                        });
601                    }
602                    (Some(tid), None)
603                        if !is_primitive(&trait_name)
604                            && !self
605                                .deferred_implements
606                                .iter()
607                                .any(|(id, n)| id == &tid && n == &trait_name) =>
608                    {
609                        self.deferred_implements.push((tid, trait_name));
610                    }
611                    _ => {}
612                }
613            }
614        }
615
616        if let Some(body) = node.child_by_field_name("body") {
617            let mut cursor = body.walk();
618            let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
619            let mut impl_scope = scope.to_vec();
620            impl_scope.push(type_name);
621
622            for child in children {
623                if child.kind() == "function_item" {
624                    self.visit_function(child, &impl_scope, type_id.clone(), NodeKind::Method);
625                }
626            }
627        }
628    }
629
630    fn visit_mod(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
631        let Some(name) = self.field_text(node, "name") else {
632            return;
633        };
634        let id = NodeId::new();
635        let graph_node = self.make_node(id.clone(), NodeKind::Module, name.clone(), scope, node);
636        if let Some(cid) = container_id {
637            self.edges.push(Edge {
638                src: cid,
639                dst: id.clone(),
640                kind: EdgeKind::Contains,
641            });
642        }
643        self.nodes.push(graph_node);
644
645        if let Some(body) = node.child_by_field_name("body") {
646            let mut new_scope = scope.to_vec();
647            new_scope.push(name);
648            self.visit_items(body, &new_scope, Some(id));
649        }
650    }
651
652    fn visit_const(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
653        let Some(name) = self.field_text(node, "name") else {
654            return;
655        };
656        let id = NodeId::new();
657        let graph_node = self.make_node(id.clone(), NodeKind::Constant, name, scope, node);
658        if let Some(cid) = container_id {
659            self.edges.push(Edge {
660                src: cid,
661                dst: id.clone(),
662                kind: EdgeKind::Contains,
663            });
664        }
665        self.nodes.push(graph_node);
666    }
667
668    fn visit_type_alias(
669        &mut self,
670        node: TsNode<'_>,
671        scope: &[String],
672        container_id: Option<NodeId>,
673    ) {
674        let Some(name) = self.field_text(node, "name") else {
675            return;
676        };
677        let id = NodeId::new();
678        let graph_node = self.make_node(id.clone(), NodeKind::TypeAlias, name, scope, node);
679        if let Some(cid) = container_id {
680            self.edges.push(Edge {
681                src: cid,
682                dst: id.clone(),
683                kind: EdgeKind::Contains,
684            });
685        }
686        self.nodes.push(graph_node);
687    }
688
689    fn visit_macro_def(
690        &mut self,
691        node: TsNode<'_>,
692        scope: &[String],
693        container_id: Option<NodeId>,
694    ) {
695        let Some(name) = self.field_text(node, "name") else {
696            return;
697        };
698        let id = NodeId::new();
699        let graph_node = self.make_node(id.clone(), NodeKind::Macro, name, scope, node);
700        if let Some(cid) = container_id {
701            self.edges.push(Edge {
702                src: cid,
703                dst: id.clone(),
704                kind: EdgeKind::Contains,
705            });
706        }
707        self.nodes.push(graph_node);
708    }
709
710    /// Walk `use_declaration` nodes in the AST root and record deferred imports.
711    /// We emit one Imports edge per leaf identifier that is not a primitive.
712    fn collect_imports(&mut self, root: TsNode<'_>) {
713        let mut cursor = root.walk();
714        for child in root.named_children(&mut cursor) {
715            if child.kind() == "use_declaration" {
716                // Find the use_as_clause or use_list or scoped_identifier etc.
717                if let Some(arg) = child.child_by_field_name("argument") {
718                    self.collect_import_leaves(arg);
719                }
720            } else if child.kind() == "mod_item" {
721                if let Some(body) = child.child_by_field_name("body") {
722                    self.collect_imports(body);
723                }
724            }
725        }
726    }
727
728    fn collect_import_leaves(&mut self, node: TsNode<'_>) {
729        match node.kind() {
730            "identifier" | "type_identifier" => {
731                let name = self.text(node).to_owned();
732                if !name.is_empty()
733                    && !is_primitive(&name)
734                    && name != "self"
735                    && name != "super"
736                    && name != "crate"
737                {
738                    // Use file-level placeholder NodeId — resolved to real nodes in indexer.
739                    let placeholder = NodeId::new();
740                    self.deferred_imports.push((placeholder, name));
741                }
742            }
743            "use_list" => {
744                let mut cursor = node.walk();
745                for child in node.named_children(&mut cursor) {
746                    self.collect_import_leaves(child);
747                }
748            }
749            "scoped_identifier" | "scoped_use_list" => {
750                // Recurse to get the leaf name.
751                let mut cursor = node.walk();
752                for child in node.named_children(&mut cursor) {
753                    self.collect_import_leaves(child);
754                }
755            }
756            "use_as_clause" => {
757                // `use foo as bar` — record bar (the alias).
758                if let Some(alias) = node.child_by_field_name("alias") {
759                    self.collect_import_leaves(alias);
760                }
761            }
762            _ => {}
763        }
764    }
765}
766
767fn is_primitive(name: &str) -> bool {
768    matches!(
769        name,
770        "bool"
771            | "char"
772            | "str"
773            | "i8"
774            | "i16"
775            | "i32"
776            | "i64"
777            | "i128"
778            | "isize"
779            | "u8"
780            | "u16"
781            | "u32"
782            | "u64"
783            | "u128"
784            | "usize"
785            | "f32"
786            | "f64"
787            | "String"
788            | "Vec"
789            | "Option"
790            | "Result"
791            | "Box"
792            | "Rc"
793            | "Arc"
794            | "Cell"
795            | "RefCell"
796            | "Cow"
797            | "HashMap"
798            | "HashSet"
799            | "BTreeMap"
800            | "BTreeSet"
801            | "PathBuf"
802            | "Path"
803            | "OsString"
804            | "OsStr"
805            | "Send"
806            | "Sync"
807            | "Sized"
808            | "Clone"
809            | "Copy"
810            | "Debug"
811            | "Display"
812            | "Default"
813            | "PartialEq"
814            | "Eq"
815            | "PartialOrd"
816            | "Ord"
817            | "Hash"
818            | "Iterator"
819            | "Into"
820            | "From"
821            | "AsRef"
822            | "AsMut"
823            | "Deref"
824            | "DerefMut"
825            | "Error"
826            | "Write"
827            | "Read"
828            | "Seek"
829            | "Self"
830            | "()"
831            | "_"
832    )
833}
834
835// ── Tests ─────────────────────────────────────────────────────────────────────
836
837#[cfg(test)]
838mod tests {
839    use std::path::Path;
840
841    use gitcortex_core::{
842        graph::{Edge, Node},
843        schema::{EdgeKind, NodeKind},
844    };
845
846    use super::RustParser;
847    use crate::parser::LanguageParser;
848
849    fn parse(src: &str) -> (Vec<Node>, Vec<Edge>) {
850        let r = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
851        (r.nodes, r.edges)
852    }
853
854    #[test]
855    fn parses_free_function() {
856        let (nodes, _) = parse("pub fn greet(name: &str) -> String { name.into() }");
857        assert_eq!(nodes.len(), 1);
858        assert_eq!(nodes[0].kind, NodeKind::Function);
859        assert_eq!(nodes[0].name, "greet");
860    }
861
862    #[test]
863    fn parses_struct() {
864        let (nodes, _) = parse("pub struct Person { pub name: String }");
865        let structs: Vec<_> = nodes
866            .iter()
867            .filter(|n| n.kind == NodeKind::Struct)
868            .collect();
869        assert_eq!(structs.len(), 1);
870        assert_eq!(structs[0].name, "Person");
871    }
872
873    #[test]
874    fn parses_trait_impl_and_method() {
875        let src = r#"
876pub trait Greet { fn greet(&self) -> String; }
877pub struct Person { pub name: String }
878impl Greet for Person {
879    fn greet(&self) -> String { self.name.clone() }
880}
881"#;
882        let (nodes, edges) = parse(src);
883
884        let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
885        let structs: Vec<_> = nodes
886            .iter()
887            .filter(|n| n.kind == NodeKind::Struct)
888            .collect();
889        let methods: Vec<_> = nodes
890            .iter()
891            .filter(|n| n.kind == NodeKind::Method)
892            .collect();
893        let impl_edges: Vec<_> = edges
894            .iter()
895            .filter(|e| e.kind == EdgeKind::Implements)
896            .collect();
897
898        assert_eq!(traits.len(), 1, "expected Greet trait");
899        assert_eq!(structs.len(), 1, "expected Person struct");
900        assert_eq!(methods.len(), 1, "expected greet method");
901        assert_eq!(impl_edges.len(), 1, "expected Implements edge");
902    }
903
904    #[test]
905    fn parses_module_with_items() {
906        let src = r#"
907pub mod utils {
908    pub fn helper() {}
909    pub struct Config {}
910}
911"#;
912        let (nodes, edges) = parse(src);
913
914        let mods: Vec<_> = nodes
915            .iter()
916            .filter(|n| n.kind == NodeKind::Module)
917            .collect();
918        let fns: Vec<_> = nodes
919            .iter()
920            .filter(|n| n.kind == NodeKind::Function)
921            .collect();
922        let contains: Vec<_> = edges
923            .iter()
924            .filter(|e| e.kind == EdgeKind::Contains)
925            .collect();
926
927        assert_eq!(mods.len(), 1, "expected utils module");
928        assert_eq!(fns.len(), 1, "expected helper function");
929        assert!(!contains.is_empty(), "expected Contains edges");
930    }
931
932    #[test]
933    fn qualified_name_includes_module_path() {
934        let src = r#"
935pub mod inner {
936    pub fn foo() {}
937}
938"#;
939        let (nodes, _) = parse(src);
940        let foo = nodes.iter().find(|n| n.name == "foo").unwrap();
941        assert_eq!(foo.qualified_name, "crate::inner::foo");
942    }
943
944    #[test]
945    fn detects_intra_file_calls() {
946        let src = r#"
947pub fn caller() { callee(); }
948pub fn callee() {}
949"#;
950        let (_, edges) = parse(src);
951        let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
952        assert_eq!(calls.len(), 1, "expected one Calls edge");
953    }
954
955    #[test]
956    fn detects_uses_edges_for_param_types() {
957        let src = r#"
958pub struct Config {}
959pub fn run(cfg: Config) {}
960"#;
961        let (_, edges) = parse(src);
962        let uses: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Uses).collect();
963        assert_eq!(uses.len(), 1, "expected one Uses edge from run to Config");
964    }
965
966    #[test]
967    fn deferred_calls_capture_unknown_callees() {
968        let src = r#"
969pub fn caller() { external_fn(); }
970"#;
971        let result = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
972        assert_eq!(result.deferred_calls.len(), 1);
973        assert_eq!(result.deferred_calls[0].1, "external_fn");
974    }
975}