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