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