Skip to main content

the_code_graph_parser/
rust_lang.rs

1use std::cell::RefCell;
2use std::path::Path;
3
4use tree_sitter::{Node, Parser};
5use tree_sitter_language::LanguageFn;
6
7use domain::error::CodeGraphError;
8use domain::model::{Edge, EdgeKind, Language, Location, SymbolKind, SymbolNode, Visibility};
9
10use crate::{Export, ImportName, LanguageParser, ParseResult, RawImport};
11
12thread_local! {
13    static RUST_PARSER: RefCell<Parser> = RefCell::new(Parser::new());
14}
15
16/// Parser for Rust (.rs) files.
17pub struct RustParser {
18    lang: LanguageFn,
19}
20
21impl RustParser {
22    pub fn new() -> Self {
23        Self {
24            lang: tree_sitter_rust::LANGUAGE,
25        }
26    }
27}
28
29impl Default for RustParser {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl LanguageParser for RustParser {
36    fn language(&self) -> Language {
37        Language::Rust
38    }
39
40    fn file_extensions(&self) -> &[&str] {
41        &["rs"]
42    }
43
44    fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
45        let lang: tree_sitter::Language = self.lang.into();
46
47        RUST_PARSER.with(|parser_cell| {
48            let mut parser = parser_cell.borrow_mut();
49            parser
50                .set_language(&lang)
51                .map_err(|e| CodeGraphError::Parse {
52                    file: path.to_path_buf(),
53                    message: format!("failed to set language: {e}"),
54                })?;
55
56            let tree = parser
57                .parse(source, None)
58                .ok_or_else(|| CodeGraphError::Parse {
59                    file: path.to_path_buf(),
60                    message: "tree-sitter parse returned None".into(),
61                })?;
62
63            extract_all(source, path, &tree)
64        })
65    }
66}
67
68fn extract_all(
69    source: &[u8],
70    path: &Path,
71    tree: &tree_sitter::Tree,
72) -> domain::error::Result<ParseResult> {
73    let mut symbols = Vec::new();
74    let mut edges = Vec::new();
75    let mut imports = Vec::new();
76    let mut exports = Vec::new();
77    let file_path = path.to_string_lossy().to_string();
78    let root = tree.root_node();
79    let mut cursor = root.walk();
80
81    // Collect preceding attribute_item texts to attach to the next item.
82    // In tree-sitter-rust, `#[test]` etc. are sibling nodes before the fn.
83    let mut pending_attrs: Vec<String> = Vec::new();
84
85    for child in root.children(&mut cursor) {
86        if !child.is_named() {
87            continue;
88        }
89        match child.kind() {
90            "attribute_item" => {
91                // Collect attribute text — attached to the next named item
92                if let Ok(text) = child.utf8_text(source) {
93                    pending_attrs.push(text.to_string());
94                }
95                continue; // do not reset pending_attrs below
96            }
97            "function_item" => {
98                if let Some(sym) = extract_function(source, &file_path, child, None, &pending_attrs)
99                {
100                    edges.push(contains_edge(&file_path, &sym.qualified_name));
101                    symbols.push(sym);
102                }
103            }
104            "struct_item" => {
105                if let Some(sym) = extract_struct(source, &file_path, child) {
106                    edges.push(contains_edge(&file_path, &sym.qualified_name));
107                    symbols.push(sym);
108                }
109            }
110            "enum_item" => {
111                if let Some(sym) = extract_enum(source, &file_path, child) {
112                    edges.push(contains_edge(&file_path, &sym.qualified_name));
113                    symbols.push(sym);
114                }
115            }
116            "trait_item" => {
117                if let Some(sym) = extract_trait(source, &file_path, child) {
118                    edges.push(contains_edge(&file_path, &sym.qualified_name));
119                    symbols.push(sym);
120                }
121            }
122            "type_item" => {
123                if let Some(sym) = extract_type_alias(source, &file_path, child) {
124                    edges.push(contains_edge(&file_path, &sym.qualified_name));
125                    symbols.push(sym);
126                }
127            }
128            "const_item" => {
129                if let Some(sym) = extract_const(source, &file_path, child) {
130                    edges.push(contains_edge(&file_path, &sym.qualified_name));
131                    symbols.push(sym);
132                }
133            }
134            "static_item" => {
135                if let Some(sym) = extract_static(source, &file_path, child) {
136                    edges.push(contains_edge(&file_path, &sym.qualified_name));
137                    symbols.push(sym);
138                }
139            }
140            "macro_definition" => {
141                if let Some(sym) = extract_macro(source, &file_path, child) {
142                    edges.push(contains_edge(&file_path, &sym.qualified_name));
143                    symbols.push(sym);
144                }
145            }
146            "impl_item" => {
147                extract_impl(source, &file_path, child, &mut symbols, &mut edges);
148            }
149            "use_declaration" => {
150                extract_use_declaration(source, child, &mut imports, &mut exports);
151            }
152            "mod_item" => {
153                // Only capture file module declarations (no body); inline modules are skipped.
154                if child.child_by_field_name("body").is_none() {
155                    if let Some(name) = node_name(source, child) {
156                        let line = child.start_position().row + 1;
157                        imports.push(RawImport {
158                            specifier: format!("mod::{name}"),
159                            names: Vec::new(),
160                            is_type_only: false,
161                            is_side_effect: false,
162                            is_namespace: false,
163                            line,
164                        });
165                    }
166                }
167            }
168            _ => {}
169        }
170        // Consumed by the item above — reset for next
171        pending_attrs.clear();
172    }
173
174    Ok(ParseResult {
175        symbols,
176        edges,
177        imports,
178        exports,
179    })
180}
181
182// ---------------------------------------------------------------------------
183// Symbol extraction helpers
184// ---------------------------------------------------------------------------
185
186/// Extract a top-level or method function_item node.
187/// When `owner_name` is Some, this is a method inside an impl block.
188/// `preceding_attrs` are sibling `attribute_item` texts collected before this node.
189fn extract_function(
190    source: &[u8],
191    file_path: &str,
192    node: Node,
193    owner_name: Option<&str>,
194    preceding_attrs: &[String],
195) -> Option<SymbolNode> {
196    let name = node_name(source, node)?;
197    let qualified_name = match owner_name {
198        Some(owner) => format!("{file_path}::{owner}.{name}"),
199        None => format!("{file_path}::{name}"),
200    };
201    let visibility = extract_visibility(source, node);
202    let is_exported = visibility == Visibility::Public;
203    let is_async = is_async_fn(source, node);
204    let is_test = attrs_contain_test(preceding_attrs);
205    let signature = build_rust_signature(source, node);
206    let kind = if owner_name.is_some() {
207        SymbolKind::Method
208    } else {
209        SymbolKind::Function
210    };
211
212    Some(SymbolNode {
213        name,
214        qualified_name,
215        kind,
216        location: node_location(file_path, node),
217        visibility,
218        is_exported,
219        is_async,
220        is_test,
221        decorators: Vec::new(),
222        signature,
223    })
224}
225
226fn extract_struct(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
227    let name = node_name(source, node)?;
228    let qualified_name = format!("{file_path}::{name}");
229    let visibility = extract_visibility(source, node);
230    let is_exported = visibility == Visibility::Public;
231
232    Some(SymbolNode {
233        name,
234        qualified_name,
235        kind: SymbolKind::Struct,
236        location: node_location(file_path, node),
237        visibility,
238        is_exported,
239        is_async: false,
240        is_test: false,
241        decorators: Vec::new(),
242        signature: None,
243    })
244}
245
246fn extract_enum(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
247    let name = node_name(source, node)?;
248    let qualified_name = format!("{file_path}::{name}");
249    let visibility = extract_visibility(source, node);
250    let is_exported = visibility == Visibility::Public;
251
252    Some(SymbolNode {
253        name,
254        qualified_name,
255        kind: SymbolKind::Enum,
256        location: node_location(file_path, node),
257        visibility,
258        is_exported,
259        is_async: false,
260        is_test: false,
261        decorators: Vec::new(),
262        signature: None,
263    })
264}
265
266fn extract_trait(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
267    let name = node_name(source, node)?;
268    let qualified_name = format!("{file_path}::{name}");
269    let visibility = extract_visibility(source, node);
270    let is_exported = visibility == Visibility::Public;
271
272    Some(SymbolNode {
273        name,
274        qualified_name,
275        kind: SymbolKind::Trait,
276        location: node_location(file_path, node),
277        visibility,
278        is_exported,
279        is_async: false,
280        is_test: false,
281        decorators: Vec::new(),
282        signature: None,
283    })
284}
285
286fn extract_type_alias(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
287    let name = node_name(source, node)?;
288    let qualified_name = format!("{file_path}::{name}");
289    let visibility = extract_visibility(source, node);
290    let is_exported = visibility == Visibility::Public;
291
292    Some(SymbolNode {
293        name,
294        qualified_name,
295        kind: SymbolKind::TypeAlias,
296        location: node_location(file_path, node),
297        visibility,
298        is_exported,
299        is_async: false,
300        is_test: false,
301        decorators: Vec::new(),
302        signature: None,
303    })
304}
305
306fn extract_const(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
307    let name = node_name(source, node)?;
308    let qualified_name = format!("{file_path}::{name}");
309    let visibility = extract_visibility(source, node);
310    let is_exported = visibility == Visibility::Public;
311
312    Some(SymbolNode {
313        name,
314        qualified_name,
315        kind: SymbolKind::Const,
316        location: node_location(file_path, node),
317        visibility,
318        is_exported,
319        is_async: false,
320        is_test: false,
321        decorators: Vec::new(),
322        signature: None,
323    })
324}
325
326fn extract_static(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
327    let name = node_name(source, node)?;
328    let qualified_name = format!("{file_path}::{name}");
329    let visibility = extract_visibility(source, node);
330    let is_exported = visibility == Visibility::Public;
331
332    Some(SymbolNode {
333        name,
334        qualified_name,
335        kind: SymbolKind::Variable,
336        location: node_location(file_path, node),
337        visibility,
338        is_exported,
339        is_async: false,
340        is_test: false,
341        decorators: Vec::new(),
342        signature: None,
343    })
344}
345
346fn extract_macro(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
347    let name = node_name(source, node)?;
348    let qualified_name = format!("{file_path}::{name}");
349
350    Some(SymbolNode {
351        name,
352        qualified_name,
353        kind: SymbolKind::Macro,
354        location: node_location(file_path, node),
355        visibility: Visibility::Public, // macro_rules! is always public-ish
356        is_exported: false,
357        is_async: false,
358        is_test: false,
359        decorators: Vec::new(),
360        signature: None,
361    })
362}
363
364/// Extract methods from an `impl_item` and generate edges.
365fn extract_impl(
366    source: &[u8],
367    file_path: &str,
368    node: Node,
369    symbols: &mut Vec<SymbolNode>,
370    edges: &mut Vec<Edge>,
371) {
372    // The implementing type is in the "type" field
373    let type_name = match node.child_by_field_name("type") {
374        Some(t) => match t.utf8_text(source) {
375            Ok(s) => s.to_string(),
376            Err(_) => return,
377        },
378        None => return,
379    };
380
381    // Check if this is `impl Trait for Type`
382    let trait_name = node
383        .child_by_field_name("trait")
384        .and_then(|t| t.utf8_text(source).ok())
385        .map(|s| s.to_string());
386
387    // If it's a trait impl, emit an Implements edge
388    if let Some(ref tname) = trait_name {
389        let trait_qn = format!("{file_path}::{tname}");
390        let type_qn = format!("{file_path}::{type_name}");
391        edges.push(Edge {
392            kind: EdgeKind::Implements,
393            source: type_qn,
394            target: trait_qn,
395            metadata: None,
396        });
397    }
398
399    // Walk the body (declaration_list) for methods
400    let body = match node.child_by_field_name("body") {
401        Some(b) => b,
402        None => return,
403    };
404
405    let mut body_cursor = body.walk();
406    let mut pending_attrs: Vec<String> = Vec::new();
407    for member in body.children(&mut body_cursor) {
408        if !member.is_named() {
409            continue;
410        }
411        if member.kind() == "attribute_item" {
412            if let Ok(text) = member.utf8_text(source) {
413                pending_attrs.push(text.to_string());
414            }
415            continue;
416        }
417        if member.kind() == "function_item" {
418            if let Some(sym) =
419                extract_function(source, file_path, member, Some(&type_name), &pending_attrs)
420            {
421                edges.push(contains_edge(file_path, &sym.qualified_name));
422                // ChildOf edge: method → impl type
423                let type_qn = format!("{file_path}::{type_name}");
424                edges.push(Edge {
425                    kind: EdgeKind::ChildOf,
426                    source: sym.qualified_name.clone(),
427                    target: type_qn,
428                    metadata: None,
429                });
430                symbols.push(sym);
431            }
432        }
433        pending_attrs.clear();
434    }
435}
436
437// ---------------------------------------------------------------------------
438// use_declaration helpers
439// ---------------------------------------------------------------------------
440
441/// Extract RawImport (and optionally an Export for `pub use`) from a single
442/// `use_declaration` node.
443fn extract_use_declaration(
444    source: &[u8],
445    node: Node,
446    imports: &mut Vec<RawImport>,
447    exports: &mut Vec<Export>,
448) {
449    let line = node.start_position().row + 1;
450
451    // Detect `pub use` — presence of visibility_modifier child
452    let is_pub_use = {
453        let mut cur = node.walk();
454        let result = node
455            .children(&mut cur)
456            .any(|c| c.kind() == "visibility_modifier");
457        result
458    };
459
460    // The `argument` field contains the actual use path
461    let argument = match node.child_by_field_name("argument") {
462        Some(a) => a,
463        None => return,
464    };
465
466    process_use_argument(source, argument, &[], line, is_pub_use, imports, exports);
467}
468
469/// Recursively process a use argument node, accumulating path prefix segments.
470/// `prefix_parts` are the path components gathered so far from ancestor scoped
471/// identifiers / scoped_use_lists.
472fn process_use_argument(
473    source: &[u8],
474    node: Node,
475    prefix_parts: &[String],
476    line: usize,
477    is_pub_use: bool,
478    imports: &mut Vec<RawImport>,
479    exports: &mut Vec<Export>,
480) {
481    match node.kind() {
482        // `use foo::bar::baz` — scoped_identifier with optional nested scoped_identifier
483        "scoped_identifier" => {
484            let parts = flatten_scoped_identifier(source, node);
485            let specifier = parts.join("::");
486            let name = parts.last().cloned().unwrap_or_default();
487            emit_import_and_maybe_export(
488                specifier,
489                vec![ImportName {
490                    name,
491                    alias: None,
492                    is_type: false,
493                }],
494                false,
495                line,
496                is_pub_use,
497                imports,
498                exports,
499            );
500        }
501
502        // `use foo::*` or `use foo::bar::*`
503        // use_wildcard embeds an optional path as its first named child (no field name).
504        "use_wildcard" => {
505            // Find the path embedded in the wildcard node: any named child that is
506            // a path-like node (scoped_identifier, identifier, crate, self, super).
507            let embedded_parts: Vec<String> = {
508                let mut cur = node.walk();
509                node.children(&mut cur)
510                    .filter(|c| {
511                        matches!(
512                            c.kind(),
513                            "scoped_identifier" | "identifier" | "crate" | "self" | "super"
514                        )
515                    })
516                    .flat_map(|c| flatten_path_node(source, c))
517                    .collect()
518            };
519
520            let specifier = if !embedded_parts.is_empty() {
521                embedded_parts.join("::")
522            } else {
523                // fallback: prefix from outer scoped_use_list context
524                prefix_parts.join("::")
525            };
526
527            emit_import_and_maybe_export(
528                specifier,
529                Vec::new(),
530                true, // is_namespace
531                line,
532                is_pub_use,
533                imports,
534                exports,
535            );
536        }
537
538        // `use foo::{A, B}` — scoped_use_list
539        "scoped_use_list" => {
540            // path field gives the prefix, list field contains the use_list
541            let path_parts: Vec<String> = match node.child_by_field_name("path") {
542                Some(p) => flatten_path_node(source, p),
543                None => prefix_parts.to_vec(),
544            };
545
546            let list = match node.child_by_field_name("list") {
547                Some(l) => l,
548                None => return,
549            };
550
551            let mut list_cursor = list.walk();
552            for item in list.children(&mut list_cursor) {
553                if !item.is_named() {
554                    continue;
555                }
556                process_use_argument(
557                    source,
558                    item,
559                    &path_parts,
560                    line,
561                    is_pub_use,
562                    imports,
563                    exports,
564                );
565            }
566        }
567
568        // `use foo as bar`
569        "use_as_clause" => {
570            let path_node = node.child_by_field_name("path");
571            let alias_node = node.child_by_field_name("alias");
572
573            let (specifier, name) = match path_node {
574                Some(p) => {
575                    let parts = flatten_path_node(source, p);
576                    let name = parts.last().cloned().unwrap_or_default();
577                    (parts.join("::"), name)
578                }
579                None => (prefix_parts.join("::"), String::new()),
580            };
581
582            let alias = alias_node
583                .and_then(|a| a.utf8_text(source).ok())
584                .map(|s| s.to_string());
585
586            emit_import_and_maybe_export(
587                specifier,
588                vec![ImportName {
589                    name,
590                    alias,
591                    is_type: false,
592                }],
593                false,
594                line,
595                is_pub_use,
596                imports,
597                exports,
598            );
599        }
600
601        // Simple identifier: `use foo` (bare, inside a use_list)
602        "identifier" | "crate" | "self" | "super" => {
603            if let Ok(text) = node.utf8_text(source) {
604                let mut parts = prefix_parts.to_vec();
605                let name = text.to_string();
606                parts.push(name.clone());
607                let specifier = parts.join("::");
608                emit_import_and_maybe_export(
609                    specifier,
610                    vec![ImportName {
611                        name,
612                        alias: None,
613                        is_type: false,
614                    }],
615                    false,
616                    line,
617                    is_pub_use,
618                    imports,
619                    exports,
620                );
621            }
622        }
623
624        // use_list: `{ A, B, C }` — iterate children
625        "use_list" => {
626            let mut list_cursor = node.walk();
627            for item in node.children(&mut list_cursor) {
628                if !item.is_named() {
629                    continue;
630                }
631                process_use_argument(
632                    source,
633                    item,
634                    prefix_parts,
635                    line,
636                    is_pub_use,
637                    imports,
638                    exports,
639                );
640            }
641        }
642
643        _ => {}
644    }
645}
646
647/// Build a RawImport and optionally an Export (for `pub use`).
648fn emit_import_and_maybe_export(
649    specifier: String,
650    names: Vec<ImportName>,
651    is_namespace: bool,
652    line: usize,
653    is_pub_use: bool,
654    imports: &mut Vec<RawImport>,
655    exports: &mut Vec<Export>,
656) {
657    if is_pub_use {
658        // For each re-exported name, emit an Export
659        for n in &names {
660            exports.push(Export {
661                name: n.alias.clone().unwrap_or_else(|| n.name.clone()),
662                local_name: Some(n.name.clone()),
663                is_default: false,
664                is_type_only: false,
665                is_reexport: true,
666                source_specifier: Some(specifier.clone()),
667            });
668        }
669        if is_namespace {
670            exports.push(Export {
671                name: String::new(),
672                local_name: None,
673                is_default: false,
674                is_type_only: false,
675                is_reexport: true,
676                source_specifier: Some(specifier.clone()),
677            });
678        }
679    }
680
681    imports.push(RawImport {
682        specifier,
683        names,
684        is_type_only: false,
685        is_side_effect: false,
686        is_namespace,
687        line,
688    });
689}
690
691/// Flatten a `scoped_identifier` node into its constituent path segments.
692/// e.g. `crate::auth::validate` → ["crate", "auth", "validate"]
693fn flatten_scoped_identifier(source: &[u8], node: Node) -> Vec<String> {
694    let mut parts = Vec::new();
695    flatten_scoped_identifier_into(source, node, &mut parts);
696    parts
697}
698
699fn flatten_scoped_identifier_into(source: &[u8], node: Node, parts: &mut Vec<String>) {
700    // path field can be another scoped_identifier, crate, self, super, identifier
701    if let Some(path) = node.child_by_field_name("path") {
702        match path.kind() {
703            "scoped_identifier" => flatten_scoped_identifier_into(source, path, parts),
704            _ => {
705                if let Ok(text) = path.utf8_text(source) {
706                    parts.push(text.to_string());
707                }
708            }
709        }
710    }
711    if let Some(name) = node.child_by_field_name("name") {
712        if let Ok(text) = name.utf8_text(source) {
713            parts.push(text.to_string());
714        }
715    }
716}
717
718/// Flatten any path-like node (identifier, scoped_identifier, crate, self, super)
719/// into path segments.
720fn flatten_path_node(source: &[u8], node: Node) -> Vec<String> {
721    match node.kind() {
722        "scoped_identifier" => flatten_scoped_identifier(source, node),
723        "identifier" | "crate" | "self" | "super" => node
724            .utf8_text(source)
725            .ok()
726            .map(|s| vec![s.to_string()])
727            .unwrap_or_default(),
728        _ => Vec::new(),
729    }
730}
731
732// ---------------------------------------------------------------------------
733// Low-level helpers
734// ---------------------------------------------------------------------------
735
736/// Get the text of the "name" field child.
737fn node_name(source: &[u8], node: Node) -> Option<String> {
738    node.child_by_field_name("name")
739        .and_then(|n| n.utf8_text(source).ok())
740        .map(|s| s.to_string())
741}
742
743/// Build a Location from a tree-sitter node (1-based lines).
744fn node_location(file_path: &str, node: Node) -> Location {
745    let start = node.start_position();
746    let end = node.end_position();
747    Location {
748        file: file_path.into(),
749        line_start: start.row + 1,
750        line_end: end.row + 1,
751        col_start: start.column,
752        col_end: end.column,
753    }
754}
755
756/// Create a Contains edge from file path to a qualified name.
757fn contains_edge(file_path: &str, qualified_name: &str) -> Edge {
758    Edge {
759        kind: EdgeKind::Contains,
760        source: file_path.to_string(),
761        target: qualified_name.to_string(),
762        metadata: None,
763    }
764}
765
766/// Extract Visibility from the first `visibility_modifier` child, if present.
767fn extract_visibility(source: &[u8], node: Node) -> Visibility {
768    let mut cursor = node.walk();
769    for child in node.children(&mut cursor) {
770        if child.kind() == "visibility_modifier" {
771            let text = child.utf8_text(source).unwrap_or("");
772            return if text.contains("crate") {
773                Visibility::Crate
774            } else {
775                // "pub" (possibly with path like "pub(super)") → Public
776                Visibility::Public
777            };
778        }
779    }
780    Visibility::Private
781}
782
783/// Check if a `function_item` node has the `async` modifier.
784///
785/// In tree-sitter-rust the `async` keyword lives inside a named `function_modifiers`
786/// child (e.g. `async unsafe extern`), not as a bare unnamed token.
787fn is_async_fn(source: &[u8], node: Node) -> bool {
788    let mut cursor = node.walk();
789    for child in node.children(&mut cursor) {
790        if child.kind() == "function_modifiers" {
791            let text = child.utf8_text(source).unwrap_or("");
792            return text.split_whitespace().any(|w| w == "async");
793        }
794    }
795    false
796}
797
798/// Check if any of the provided attribute texts contains "test".
799///
800/// Attributes in tree-sitter-rust are sibling `attribute_item` nodes that precede
801/// the function in the parent's children list. Callers collect them and pass the
802/// texts here.
803fn attrs_contain_test(attrs: &[String]) -> bool {
804    attrs.iter().any(|a| a.contains("test"))
805}
806
807/// Build a simplified function signature from `parameters` and optional `return_type`.
808fn build_rust_signature(source: &[u8], node: Node) -> Option<String> {
809    let params = node
810        .child_by_field_name("parameters")
811        .and_then(|n| n.utf8_text(source).ok())?;
812
813    let return_type = node
814        .child_by_field_name("return_type")
815        .and_then(|n| n.utf8_text(source).ok());
816
817    Some(match return_type {
818        Some(ret) => format!("{params} {ret}"),
819        None => params.to_string(),
820    })
821}
822
823// ---------------------------------------------------------------------------
824// Tests
825// ---------------------------------------------------------------------------
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830
831    fn parse_rust(source: &str) -> ParseResult {
832        let parser = RustParser::new();
833        parser
834            .parse(source.as_bytes(), Path::new("test.rs"))
835            .expect("parse failed")
836    }
837
838    // -----------------------------------------------------------------------
839    // AC6: fn foo() {} → Function symbol, name="foo"
840    // -----------------------------------------------------------------------
841
842    #[test]
843    fn ac6_function_item_extracts_function_symbol() {
844        let result = parse_rust("fn foo() {}");
845        assert_eq!(result.symbols.len(), 1);
846        let sym = &result.symbols[0];
847        assert_eq!(sym.name, "foo");
848        assert_eq!(sym.kind, SymbolKind::Function);
849    }
850
851    #[test]
852    fn function_qualified_name_uses_file_path() {
853        let result = parse_rust("fn foo() {}");
854        assert_eq!(result.symbols[0].qualified_name, "test.rs::foo");
855    }
856
857    #[test]
858    fn function_contains_edge_emitted() {
859        let result = parse_rust("fn foo() {}");
860        let contains: Vec<_> = result
861            .edges
862            .iter()
863            .filter(|e| e.kind == EdgeKind::Contains)
864            .collect();
865        assert_eq!(contains.len(), 1);
866        assert_eq!(contains[0].source, "test.rs");
867        assert_eq!(contains[0].target, "test.rs::foo");
868    }
869
870    // -----------------------------------------------------------------------
871    // AC7: struct Bar {} → Struct symbol
872    // -----------------------------------------------------------------------
873
874    #[test]
875    fn ac7_struct_item_extracts_struct_symbol() {
876        let result = parse_rust("struct Bar {}");
877        assert_eq!(result.symbols.len(), 1);
878        let sym = &result.symbols[0];
879        assert_eq!(sym.name, "Bar");
880        assert_eq!(sym.kind, SymbolKind::Struct);
881    }
882
883    // -----------------------------------------------------------------------
884    // AC8: impl Foo { fn bar(&self) {} } → Method + ChildOf edge
885    // -----------------------------------------------------------------------
886
887    #[test]
888    fn ac8_impl_item_extracts_method_symbol() {
889        let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
890        let method = result
891            .symbols
892            .iter()
893            .find(|s| s.name == "bar")
894            .expect("method 'bar' not found");
895        assert_eq!(method.kind, SymbolKind::Method);
896        assert_eq!(method.qualified_name, "test.rs::Foo.bar");
897    }
898
899    #[test]
900    fn ac8_impl_method_has_child_of_edge() {
901        let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
902        let child_of = result
903            .edges
904            .iter()
905            .find(|e| e.kind == EdgeKind::ChildOf)
906            .expect("ChildOf edge not found");
907        assert_eq!(child_of.source, "test.rs::Foo.bar");
908        assert_eq!(child_of.target, "test.rs::Foo");
909    }
910
911    #[test]
912    fn impl_method_also_has_contains_edge() {
913        let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
914        let contains: Vec<_> = result
915            .edges
916            .iter()
917            .filter(|e| e.kind == EdgeKind::Contains && e.target == "test.rs::Foo.bar")
918            .collect();
919        assert_eq!(contains.len(), 1);
920    }
921
922    // -----------------------------------------------------------------------
923    // AC9: trait Baz {} → Trait symbol
924    // -----------------------------------------------------------------------
925
926    #[test]
927    fn ac9_trait_item_extracts_trait_symbol() {
928        let result = parse_rust("trait Baz {}");
929        assert_eq!(result.symbols.len(), 1);
930        let sym = &result.symbols[0];
931        assert_eq!(sym.name, "Baz");
932        assert_eq!(sym.kind, SymbolKind::Trait);
933    }
934
935    // -----------------------------------------------------------------------
936    // AC10: enum Color { Red, Green } → Enum symbol
937    // -----------------------------------------------------------------------
938
939    #[test]
940    fn ac10_enum_item_extracts_enum_symbol() {
941        let result = parse_rust("enum Color { Red, Green }");
942        assert_eq!(result.symbols.len(), 1);
943        let sym = &result.symbols[0];
944        assert_eq!(sym.name, "Color");
945        assert_eq!(sym.kind, SymbolKind::Enum);
946    }
947
948    // -----------------------------------------------------------------------
949    // AC14: Visibility extraction
950    // -----------------------------------------------------------------------
951
952    #[test]
953    fn ac14_pub_fn_is_public() {
954        let result = parse_rust("pub fn visible() {}");
955        let sym = &result.symbols[0];
956        assert_eq!(sym.visibility, Visibility::Public);
957        assert!(sym.is_exported);
958    }
959
960    #[test]
961    fn ac14_pub_crate_fn_is_crate() {
962        let result = parse_rust("pub(crate) fn crate_fn() {}");
963        let sym = &result.symbols[0];
964        assert_eq!(sym.visibility, Visibility::Crate);
965        assert!(!sym.is_exported);
966    }
967
968    #[test]
969    fn ac14_private_fn_is_private() {
970        let result = parse_rust("fn private_fn() {}");
971        let sym = &result.symbols[0];
972        assert_eq!(sym.visibility, Visibility::Private);
973        assert!(!sym.is_exported);
974    }
975
976    #[test]
977    fn pub_struct_is_exported() {
978        let result = parse_rust("pub struct MyStruct {}");
979        let sym = &result.symbols[0];
980        assert_eq!(sym.visibility, Visibility::Public);
981        assert!(sym.is_exported);
982    }
983
984    // -----------------------------------------------------------------------
985    // AC49: Invalid/empty source → no panic
986    // -----------------------------------------------------------------------
987
988    #[test]
989    fn ac49_empty_source_does_not_panic() {
990        let parser = RustParser::new();
991        let result = parser.parse(b"", Path::new("empty.rs"));
992        assert!(result.is_ok());
993        let r = result.unwrap();
994        assert!(r.symbols.is_empty());
995        assert!(r.edges.is_empty());
996    }
997
998    // -----------------------------------------------------------------------
999    // AC50: Source with errors → partial extraction
1000    // -----------------------------------------------------------------------
1001
1002    #[test]
1003    fn ac50_partial_extraction_from_broken_source() {
1004        let source = r#"
1005fn valid() {}
1006fn broken( {{{
1007fn also_valid() {}
1008"#;
1009        let parser = RustParser::new();
1010        let result = parser
1011            .parse(source.as_bytes(), Path::new("broken.rs"))
1012            .expect("should not error on syntax errors");
1013        // tree-sitter does best-effort — at least valid() should be found
1014        assert!(
1015            result.symbols.iter().any(|s| s.name == "valid"),
1016            "should find 'valid' function in broken source"
1017        );
1018    }
1019
1020    // -----------------------------------------------------------------------
1021    // Trait impl → Implements edge
1022    // -----------------------------------------------------------------------
1023
1024    #[test]
1025    fn trait_impl_emits_implements_edge() {
1026        let source = "trait Display {} struct Foo; impl Display for Foo {}";
1027        let result = parse_rust(source);
1028        let implements = result
1029            .edges
1030            .iter()
1031            .find(|e| e.kind == EdgeKind::Implements)
1032            .expect("Implements edge not found");
1033        assert_eq!(implements.source, "test.rs::Foo");
1034        assert_eq!(implements.target, "test.rs::Display");
1035    }
1036
1037    // -----------------------------------------------------------------------
1038    // Additional symbol kinds
1039    // -----------------------------------------------------------------------
1040
1041    #[test]
1042    fn type_alias_is_extracted() {
1043        let result = parse_rust("type MyAlias = u32;");
1044        assert_eq!(result.symbols.len(), 1);
1045        assert_eq!(result.symbols[0].name, "MyAlias");
1046        assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
1047    }
1048
1049    #[test]
1050    fn const_item_is_extracted() {
1051        let result = parse_rust("const MAX: u32 = 100;");
1052        assert_eq!(result.symbols.len(), 1);
1053        assert_eq!(result.symbols[0].name, "MAX");
1054        assert_eq!(result.symbols[0].kind, SymbolKind::Const);
1055    }
1056
1057    #[test]
1058    fn static_item_is_extracted_as_variable() {
1059        let result = parse_rust(r#"static GREETING: &str = "hello";"#);
1060        assert_eq!(result.symbols.len(), 1);
1061        assert_eq!(result.symbols[0].name, "GREETING");
1062        assert_eq!(result.symbols[0].kind, SymbolKind::Variable);
1063    }
1064
1065    #[test]
1066    fn macro_definition_is_extracted() {
1067        let result = parse_rust("macro_rules! my_macro { () => {} }");
1068        assert_eq!(result.symbols.len(), 1);
1069        assert_eq!(result.symbols[0].name, "my_macro");
1070        assert_eq!(result.symbols[0].kind, SymbolKind::Macro);
1071    }
1072
1073    // -----------------------------------------------------------------------
1074    // is_async
1075    // -----------------------------------------------------------------------
1076
1077    #[test]
1078    fn async_fn_is_flagged() {
1079        let result = parse_rust("async fn fetch() {}");
1080        assert!(result.symbols[0].is_async);
1081    }
1082
1083    #[test]
1084    fn sync_fn_is_not_async() {
1085        let result = parse_rust("fn sync_fn() {}");
1086        assert!(!result.symbols[0].is_async);
1087    }
1088
1089    // -----------------------------------------------------------------------
1090    // is_test via #[test] attribute
1091    // -----------------------------------------------------------------------
1092
1093    #[test]
1094    fn test_attribute_sets_is_test() {
1095        let result = parse_rust("#[test]\nfn my_test() {}");
1096        assert!(result.symbols[0].is_test);
1097    }
1098
1099    #[test]
1100    fn no_test_attribute_is_not_test() {
1101        let result = parse_rust("fn regular_fn() {}");
1102        assert!(!result.symbols[0].is_test);
1103    }
1104
1105    // -----------------------------------------------------------------------
1106    // Signature
1107    // -----------------------------------------------------------------------
1108
1109    #[test]
1110    fn function_signature_includes_params_and_return_type() {
1111        let result = parse_rust("fn add(a: i32, b: i32) -> i32 { a + b }");
1112        let sig = result.symbols[0].signature.as_ref().expect("no signature");
1113        assert!(sig.contains("a: i32"));
1114        assert!(sig.contains("b: i32"));
1115        assert!(sig.contains("i32")); // return type
1116    }
1117
1118    #[test]
1119    fn function_signature_without_return_type() {
1120        let result = parse_rust("fn greet(name: &str) {}");
1121        let sig = result.symbols[0].signature.as_ref().expect("no signature");
1122        assert!(sig.contains("name: &str"));
1123    }
1124
1125    // -----------------------------------------------------------------------
1126    // Location
1127    // -----------------------------------------------------------------------
1128
1129    #[test]
1130    fn location_is_one_based_line_numbers() {
1131        let result = parse_rust("fn foo() {}");
1132        let loc = &result.symbols[0].location;
1133        assert_eq!(loc.file.to_string_lossy(), "test.rs");
1134        assert_eq!(loc.line_start, 1);
1135        assert!(loc.line_end >= 1);
1136    }
1137
1138    // -----------------------------------------------------------------------
1139    // Multiple top-level items
1140    // -----------------------------------------------------------------------
1141
1142    #[test]
1143    fn multiple_top_level_items_all_extracted() {
1144        let source = r#"
1145fn foo() {}
1146struct Bar {}
1147enum Baz { A }
1148trait Qux {}
1149"#;
1150        let result = parse_rust(source);
1151        assert_eq!(result.symbols.len(), 4);
1152        assert!(result
1153            .symbols
1154            .iter()
1155            .any(|s| s.name == "foo" && s.kind == SymbolKind::Function));
1156        assert!(result
1157            .symbols
1158            .iter()
1159            .any(|s| s.name == "Bar" && s.kind == SymbolKind::Struct));
1160        assert!(result
1161            .symbols
1162            .iter()
1163            .any(|s| s.name == "Baz" && s.kind == SymbolKind::Enum));
1164        assert!(result
1165            .symbols
1166            .iter()
1167            .any(|s| s.name == "Qux" && s.kind == SymbolKind::Trait));
1168        let contains_count = result
1169            .edges
1170            .iter()
1171            .filter(|e| e.kind == EdgeKind::Contains)
1172            .count();
1173        assert_eq!(contains_count, 4);
1174    }
1175
1176    // -----------------------------------------------------------------------
1177    // impl with multiple methods
1178    // -----------------------------------------------------------------------
1179
1180    #[test]
1181    fn impl_with_multiple_methods() {
1182        let source = r#"
1183struct Counter;
1184impl Counter {
1185    fn new() -> Self { Counter }
1186    fn increment(&mut self) {}
1187    fn value(&self) -> u32 { 0 }
1188}
1189"#;
1190        let result = parse_rust(source);
1191        let methods: Vec<_> = result
1192            .symbols
1193            .iter()
1194            .filter(|s| s.kind == SymbolKind::Method)
1195            .collect();
1196        assert_eq!(methods.len(), 3);
1197        let child_of_count = result
1198            .edges
1199            .iter()
1200            .filter(|e| e.kind == EdgeKind::ChildOf)
1201            .count();
1202        assert_eq!(child_of_count, 3);
1203    }
1204
1205    // -----------------------------------------------------------------------
1206    // AC11: use crate::auth::validate → RawImport
1207    // -----------------------------------------------------------------------
1208
1209    #[test]
1210    fn ac11_use_scoped_identifier_extracts_raw_import() {
1211        let result = parse_rust("use crate::auth::validate;");
1212        assert_eq!(result.imports.len(), 1);
1213        let imp = &result.imports[0];
1214        assert_eq!(imp.specifier, "crate::auth::validate");
1215        assert_eq!(imp.names.len(), 1);
1216        assert_eq!(imp.names[0].name, "validate");
1217        assert!(!imp.is_namespace);
1218    }
1219
1220    #[test]
1221    fn use_simple_identifier_extracts_raw_import() {
1222        let result = parse_rust("use std::fmt;");
1223        assert_eq!(result.imports.len(), 1);
1224        let imp = &result.imports[0];
1225        assert_eq!(imp.specifier, "std::fmt");
1226        assert_eq!(imp.names[0].name, "fmt");
1227    }
1228
1229    // -----------------------------------------------------------------------
1230    // AC12: pub use self::greetings::hello → reexport
1231    // -----------------------------------------------------------------------
1232
1233    #[test]
1234    fn ac12_pub_use_creates_reexport_export_entry() {
1235        let result = parse_rust("pub use self::greetings::hello;");
1236        assert_eq!(result.imports.len(), 1);
1237        let imp = &result.imports[0];
1238        assert_eq!(imp.specifier, "self::greetings::hello");
1239
1240        // Should also create an Export entry with is_reexport = true
1241        assert_eq!(result.exports.len(), 1);
1242        let exp = &result.exports[0];
1243        assert!(exp.is_reexport);
1244        assert_eq!(
1245            exp.source_specifier.as_deref(),
1246            Some("self::greetings::hello")
1247        );
1248        assert_eq!(exp.name, "hello");
1249    }
1250
1251    // -----------------------------------------------------------------------
1252    // use_wildcard → is_namespace = true
1253    // -----------------------------------------------------------------------
1254
1255    #[test]
1256    fn use_wildcard_sets_is_namespace() {
1257        let result = parse_rust("use foo::bar::*;");
1258        assert_eq!(result.imports.len(), 1);
1259        let imp = &result.imports[0];
1260        assert!(imp.is_namespace);
1261        assert_eq!(imp.specifier, "foo::bar");
1262    }
1263
1264    // -----------------------------------------------------------------------
1265    // use_list / scoped_use_list → multiple names
1266    // -----------------------------------------------------------------------
1267
1268    #[test]
1269    fn use_scoped_list_extracts_multiple_names() {
1270        let result = parse_rust("use foo::{A, B};");
1271        // Each item in the list becomes its own RawImport
1272        assert_eq!(result.imports.len(), 2);
1273        let specifiers: Vec<_> = result
1274            .imports
1275            .iter()
1276            .map(|i| i.specifier.as_str())
1277            .collect();
1278        assert!(
1279            specifiers.contains(&"foo::A"),
1280            "expected foo::A, got {specifiers:?}"
1281        );
1282        assert!(
1283            specifiers.contains(&"foo::B"),
1284            "expected foo::B, got {specifiers:?}"
1285        );
1286    }
1287
1288    // -----------------------------------------------------------------------
1289    // use_as_clause → alias
1290    // -----------------------------------------------------------------------
1291
1292    #[test]
1293    fn use_as_clause_extracts_alias() {
1294        let result = parse_rust("use foo as bar;");
1295        assert_eq!(result.imports.len(), 1);
1296        let imp = &result.imports[0];
1297        assert_eq!(imp.specifier, "foo");
1298        assert_eq!(imp.names.len(), 1);
1299        assert_eq!(imp.names[0].name, "foo");
1300        assert_eq!(imp.names[0].alias.as_deref(), Some("bar"));
1301    }
1302
1303    // -----------------------------------------------------------------------
1304    // AC13: mod submodule; → captured with "mod::" prefix
1305    // -----------------------------------------------------------------------
1306
1307    #[test]
1308    fn ac13_mod_declaration_captured_with_mod_prefix() {
1309        let result = parse_rust("mod submodule;");
1310        assert_eq!(result.imports.len(), 1);
1311        let imp = &result.imports[0];
1312        assert_eq!(imp.specifier, "mod::submodule");
1313        assert!(imp.names.is_empty());
1314    }
1315
1316    #[test]
1317    fn inline_mod_not_captured_as_import() {
1318        let result = parse_rust("mod inline { fn inner() {} }");
1319        // The inline mod has a body, so it must not generate an import entry
1320        assert!(
1321            result.imports.is_empty(),
1322            "inline mod should not produce an import, got: {:?}",
1323            result.imports
1324        );
1325    }
1326
1327    // -----------------------------------------------------------------------
1328    // Integration: full real-world snippet
1329    // -----------------------------------------------------------------------
1330
1331    #[test]
1332    fn integration_realistic_module() {
1333        let source = r#"
1334use std::fmt;
1335
1336pub struct Point {
1337    pub x: f64,
1338    pub y: f64,
1339}
1340
1341impl Point {
1342    pub fn new(x: f64, y: f64) -> Self {
1343        Point { x, y }
1344    }
1345
1346    pub fn distance(&self, other: &Point) -> f64 {
1347        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
1348    }
1349}
1350
1351impl fmt::Display for Point {
1352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1353        write!(f, "({}, {})", self.x, self.y)
1354    }
1355}
1356
1357pub trait Shape {
1358    fn area(&self) -> f64;
1359}
1360
1361pub enum Color {
1362    Red,
1363    Green,
1364    Blue,
1365}
1366
1367pub const MAX_POINTS: usize = 1000;
1368
1369pub async fn fetch_data() -> Vec<Point> {
1370    vec![]
1371}
1372
1373#[test]
1374fn test_distance() {
1375    let a = Point { x: 0.0, y: 0.0 };
1376    let b = Point { x: 3.0, y: 4.0 };
1377    assert_eq!(a.distance(&b), 5.0);
1378}
1379"#;
1380        let result = parse_rust(source);
1381
1382        // Symbols
1383        assert!(result
1384            .symbols
1385            .iter()
1386            .any(|s| s.name == "Point" && s.kind == SymbolKind::Struct));
1387        assert!(result
1388            .symbols
1389            .iter()
1390            .any(|s| s.name == "Shape" && s.kind == SymbolKind::Trait));
1391        assert!(result
1392            .symbols
1393            .iter()
1394            .any(|s| s.name == "Color" && s.kind == SymbolKind::Enum));
1395        assert!(result
1396            .symbols
1397            .iter()
1398            .any(|s| s.name == "MAX_POINTS" && s.kind == SymbolKind::Const));
1399        assert!(result
1400            .symbols
1401            .iter()
1402            .any(|s| s.name == "fetch_data" && s.is_async));
1403        assert!(result
1404            .symbols
1405            .iter()
1406            .any(|s| s.name == "test_distance" && s.is_test));
1407
1408        // Methods from impl Point
1409        assert!(result.symbols.iter().any(|s| s.name == "new"
1410            && s.kind == SymbolKind::Method
1411            && s.qualified_name == "test.rs::Point.new"));
1412        assert!(result
1413            .symbols
1414            .iter()
1415            .any(|s| s.name == "distance" && s.kind == SymbolKind::Method));
1416
1417        // Method from impl Display for Point
1418        assert!(result
1419            .symbols
1420            .iter()
1421            .any(|s| s.name == "fmt" && s.kind == SymbolKind::Method));
1422
1423        // Visibility
1424        let point_sym = result.symbols.iter().find(|s| s.name == "Point").unwrap();
1425        assert_eq!(point_sym.visibility, Visibility::Public);
1426        assert!(point_sym.is_exported);
1427
1428        // Implements edge for Display
1429        let implements = result
1430            .edges
1431            .iter()
1432            .filter(|e| e.kind == EdgeKind::Implements)
1433            .collect::<Vec<_>>();
1434        assert!(
1435            !implements.is_empty(),
1436            "should have at least one Implements edge"
1437        );
1438
1439        // ChildOf edges
1440        let child_of_count = result
1441            .edges
1442            .iter()
1443            .filter(|e| e.kind == EdgeKind::ChildOf)
1444            .count();
1445        assert!(
1446            child_of_count >= 3,
1447            "expected at least 3 ChildOf edges, got {child_of_count}"
1448        );
1449    }
1450}