Skip to main content

arbor_core/
parser_v2.rs

1//! ArborParser - The Eyes of Arbor
2//!
3//! This module implements high-performance code parsing using Tree-sitter queries.
4//! It extracts symbols (functions, classes, interfaces) and their relationships
5//! (imports, calls) to build a comprehensive code graph.
6//!
7//! The parser is designed for incremental updates - calling it on the same file
8//! will update existing nodes rather than creating duplicates.
9
10use crate::error::{ParseError, Result};
11use crate::fallback_parser;
12use crate::node::{CodeNode, NodeKind};
13use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
17
18// ─────────────────────────────────────────────────────────────────────────────
19// Types
20// ─────────────────────────────────────────────────────────────────────────────
21
22/// A relationship between two symbols in the code.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SymbolRelation {
25    /// The source symbol (caller/importer).
26    pub from_id: String,
27    /// The target symbol name (what is being called/imported).
28    pub to_name: String,
29    /// The type of relationship.
30    pub kind: RelationType,
31    /// Line number where the relationship occurs.
32    pub line: u32,
33}
34
35/// Types of relationships between code symbols.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum RelationType {
38    /// Function/method calls another function.
39    Calls,
40    /// Module imports another module or symbol.
41    Imports,
42    /// Class extends another class.
43    Extends,
44    /// Class/type implements an interface.
45    Implements,
46}
47
48/// Result of parsing a single file.
49#[derive(Debug)]
50pub struct ParseResult {
51    /// Extracted code symbols.
52    pub symbols: Vec<CodeNode>,
53    /// Relationships between symbols.
54    pub relations: Vec<SymbolRelation>,
55    /// File path that was parsed.
56    pub file_path: String,
57}
58
59// ─────────────────────────────────────────────────────────────────────────────
60// ArborParser
61// ─────────────────────────────────────────────────────────────────────────────
62
63/// High-performance code parser using Tree-sitter queries.
64///
65/// The parser caches compiled queries for reuse across multiple files,
66/// making it efficient for large codebase indexing.
67pub struct ArborParser {
68    /// Tree-sitter parser instance.
69    parser: Parser,
70    /// Compiled queries by language.
71    queries: HashMap<String, CompiledQueries>,
72}
73
74/// Pre-compiled queries for a specific language.
75struct CompiledQueries {
76    /// Query for extracting symbols (functions, classes, etc.).
77    symbols: Query,
78    /// Query for extracting imports.
79    imports: Query,
80    /// Query for extracting function calls.
81    calls: Query,
82    /// The language for this query set.
83    language: Language,
84}
85
86impl Default for ArborParser {
87    fn default() -> Self {
88        Self::new().expect("Failed to initialize ArborParser")
89    }
90}
91
92impl ArborParser {
93    /// Creates a new ArborParser with pre-compiled queries.
94    ///
95    /// Returns an error if any language queries fail to compile.
96    pub fn new() -> Result<Self> {
97        let parser = Parser::new();
98        let mut queries = HashMap::new();
99
100        // Compile TypeScript/JavaScript queries
101        for ext in &["ts", "tsx", "js", "jsx"] {
102            let compiled = Self::compile_typescript_queries()?;
103            queries.insert(ext.to_string(), compiled);
104        }
105
106        // Compile Rust queries
107        let rs_queries = Self::compile_rust_queries()?;
108        queries.insert("rs".to_string(), rs_queries);
109
110        // Compile Python queries
111        let py_queries = Self::compile_python_queries()?;
112        queries.insert("py".to_string(), py_queries);
113
114        // Compile Go queries
115        let go_queries = Self::compile_go_queries()?;
116        queries.insert("go".to_string(), go_queries);
117
118        // Compile Java queries
119        let java_queries = Self::compile_java_queries()?;
120        queries.insert("java".to_string(), java_queries);
121
122        // Compile C queries
123        for ext in &["c", "h"] {
124            queries.insert(ext.to_string(), Self::compile_c_queries()?);
125        }
126
127        // Compile C++ queries
128        for ext in &["cpp", "hpp", "cc", "hh", "cxx", "hxx"] {
129            queries.insert(ext.to_string(), Self::compile_cpp_queries()?);
130        }
131
132        // Dart remains on legacy path (queries incompatible with tree-sitter-dart v0.0.4). Registry helper enables trivial future addition.
133        // Compile C# queries
134        let csharp_queries = Self::compile_csharp_queries()?;
135        queries.insert("cs".to_string(), csharp_queries);
136
137        Ok(Self { parser, queries })
138    }
139
140    /// Parses a file and extracts symbols and relationships.
141    ///
142    /// This is the main entry point for parsing. It returns a ParseResult
143    /// containing all symbols and their relationships, ready to be inserted
144    /// into an ArborGraph.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the file cannot be read, the language is unsupported,
149    /// or parsing fails. Syntax errors in the source code are handled gracefully -
150    /// the parser will still extract what it can.
151    pub fn parse_file(&mut self, path: &Path) -> Result<ParseResult> {
152        // Read the file
153        let source = fs::read_to_string(path).map_err(|e| ParseError::io(path, e))?;
154
155        if source.is_empty() {
156            return Err(ParseError::EmptyFile(path.to_path_buf()));
157        }
158
159        // Get the extension (normalize to lowercase)
160        let ext = path
161            .extension()
162            .and_then(|e| e.to_str())
163            .ok_or_else(|| ParseError::UnsupportedLanguage(path.to_path_buf()))?;
164        let ext = ext.to_ascii_lowercase();
165
166        // Get compiled queries
167        let compiled = match self.queries.get(&ext) {
168            Some(compiled) => compiled,
169            None => {
170                if fallback_parser::is_fallback_supported_extension(&ext) {
171                    return Ok(ParseResult {
172                        symbols: fallback_parser::parse_fallback_source(
173                            &source,
174                            &path.to_string_lossy(),
175                            &ext,
176                        ),
177                        relations: Vec::new(),
178                        file_path: path.to_string_lossy().to_string(),
179                    });
180                }
181                return Err(ParseError::UnsupportedLanguage(path.to_path_buf()));
182            }
183        };
184
185        // Configure parser for this language
186        self.parser
187            .set_language(&compiled.language)
188            .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
189
190        // Parse the source
191        let tree = self
192            .parser
193            .parse(&source, None)
194            .ok_or_else(|| ParseError::ParserError("Tree-sitter returned no tree".into()))?;
195
196        let file_path = path.to_string_lossy().to_string();
197        let file_name = path
198            .file_name()
199            .and_then(|n| n.to_str())
200            .unwrap_or("unknown");
201
202        // Extract symbols
203        let symbols = self.extract_symbols(&tree, &source, &file_path, file_name, compiled);
204
205        // Extract relationships
206        let relations = self.extract_relations(&tree, &source, &file_path, &symbols, compiled);
207
208        Ok(ParseResult {
209            symbols,
210            relations,
211            file_path,
212        })
213    }
214
215    /// Parses source code directly (for testing or in-memory content).
216    pub fn parse_source(
217        &mut self,
218        source: &str,
219        file_path: &str,
220        language: &str,
221    ) -> Result<ParseResult> {
222        if source.is_empty() {
223            return Err(ParseError::EmptyFile(file_path.into()));
224        }
225
226        // Normalize language/extension to lowercase
227        let language = language.to_ascii_lowercase();
228        let compiled = self.queries.get(&language);
229
230        if compiled.is_none() {
231            if fallback_parser::is_fallback_supported_extension(&language) {
232                return Ok(ParseResult {
233                    symbols: fallback_parser::parse_fallback_source(source, file_path, &language),
234                    relations: Vec::new(),
235                    file_path: file_path.to_string(),
236                });
237            }
238            return Err(ParseError::UnsupportedLanguage(file_path.into()));
239        }
240
241        let compiled = compiled.unwrap();
242
243        self.parser
244            .set_language(&compiled.language)
245            .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
246
247        let tree = self
248            .parser
249            .parse(source, None)
250            .ok_or_else(|| ParseError::ParserError("Tree-sitter returned no tree".into()))?;
251
252        let file_name = Path::new(file_path)
253            .file_name()
254            .and_then(|n| n.to_str())
255            .unwrap_or("unknown");
256
257        let symbols = self.extract_symbols(&tree, source, file_path, file_name, compiled);
258        let relations = self.extract_relations(&tree, source, file_path, &symbols, compiled);
259
260        Ok(ParseResult {
261            symbols,
262            relations,
263            file_path: file_path.to_string(),
264        })
265    }
266
267    // ─────────────────────────────────────────────────────────────────────────
268    // Symbol Extraction
269    // ─────────────────────────────────────────────────────────────────────────
270
271    fn extract_symbols(
272        &self,
273        tree: &Tree,
274        source: &str,
275        file_path: &str,
276        file_name: &str,
277        compiled: &CompiledQueries,
278    ) -> Vec<CodeNode> {
279        let mut symbols = Vec::new();
280        let mut cursor = QueryCursor::new();
281
282        let matches = cursor.matches(&compiled.symbols, tree.root_node(), source.as_bytes());
283
284        for match_ in matches {
285            // Extract name and type from captures
286            let mut name: Option<&str> = None;
287            let mut kind: Option<NodeKind> = None;
288            let mut node = match_.captures.first().map(|c| c.node);
289
290            for capture in match_.captures {
291                let capture_name = compiled.symbols.capture_names()[capture.index as usize];
292                let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
293
294                match capture_name {
295                    "name" | "function.name" | "class.name" | "interface.name" | "method.name" => {
296                        name = Some(text);
297                    }
298                    "function" | "function_def" => {
299                        kind = Some(NodeKind::Function);
300                        node = Some(capture.node);
301                    }
302                    "class" | "class_def" => {
303                        kind = Some(NodeKind::Class);
304                        node = Some(capture.node);
305                    }
306                    "interface" | "interface_def" => {
307                        kind = Some(NodeKind::Interface);
308                        node = Some(capture.node);
309                    }
310                    "method" | "method_def" => {
311                        kind = Some(NodeKind::Method);
312                        node = Some(capture.node);
313                    }
314                    "struct" | "struct_def" => {
315                        kind = Some(NodeKind::Struct);
316                        node = Some(capture.node);
317                    }
318                    "enum" | "enum_def" => {
319                        kind = Some(NodeKind::Enum);
320                        node = Some(capture.node);
321                    }
322                    "trait" | "trait_def" => {
323                        kind = Some(NodeKind::Interface);
324                        node = Some(capture.node);
325                    }
326                    _ => {}
327                }
328            }
329
330            if let (Some(name), Some(kind), Some(node)) = (name, kind, node) {
331                // Build fully qualified name: filename:symbol_name
332                let qualified_name = format!("{}:{}", file_name, name);
333
334                // Extract signature (first line of the node)
335                let signature = source
336                    .lines()
337                    .nth(node.start_position().row)
338                    .map(|s| s.trim().to_string());
339
340                let mut symbol = CodeNode::new(name, &qualified_name, kind, file_path)
341                    .with_lines(
342                        node.start_position().row as u32 + 1,
343                        node.end_position().row as u32 + 1,
344                    )
345                    .with_column(node.start_position().column as u32)
346                    .with_bytes(node.start_byte() as u32, node.end_byte() as u32);
347
348                if let Some(sig) = signature {
349                    symbol = symbol.with_signature(sig);
350                }
351
352                symbols.push(symbol);
353            }
354        }
355
356        symbols
357    }
358
359    // ─────────────────────────────────────────────────────────────────────────
360    // Relationship Extraction
361    // ─────────────────────────────────────────────────────────────────────────
362
363    fn extract_relations(
364        &self,
365        tree: &Tree,
366        source: &str,
367        file_path: &str,
368        symbols: &[CodeNode],
369        compiled: &CompiledQueries,
370    ) -> Vec<SymbolRelation> {
371        let mut relations = Vec::new();
372
373        // Extract imports
374        self.extract_imports(tree, source, file_path, &mut relations, compiled);
375
376        // Extract calls
377        self.extract_calls(tree, source, file_path, symbols, &mut relations, compiled);
378
379        relations
380    }
381
382    fn extract_imports(
383        &self,
384        tree: &Tree,
385        source: &str,
386        file_path: &str,
387        relations: &mut Vec<SymbolRelation>,
388        compiled: &CompiledQueries,
389    ) {
390        let mut cursor = QueryCursor::new();
391        let matches = cursor.matches(&compiled.imports, tree.root_node(), source.as_bytes());
392
393        for match_ in matches {
394            let mut module_name: Option<&str> = None;
395            let mut line: u32 = 0;
396
397            for capture in match_.captures {
398                let capture_name = compiled.imports.capture_names()[capture.index as usize];
399                let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
400
401                match capture_name {
402                    "source" | "module" | "import.source" => {
403                        // Remove quotes from module name
404                        module_name = Some(text.trim_matches(|c| c == '"' || c == '\''));
405                        line = capture.node.start_position().row as u32 + 1;
406                    }
407                    _ => {}
408                }
409            }
410
411            if let Some(module) = module_name {
412                // Create a file-level import relation
413                let file_id = format!("{}:__file__", file_path);
414                relations.push(SymbolRelation {
415                    from_id: file_id,
416                    to_name: module.to_string(),
417                    kind: RelationType::Imports,
418                    line,
419                });
420            }
421        }
422    }
423
424    fn extract_calls(
425        &self,
426        tree: &Tree,
427        source: &str,
428        file_path: &str,
429        symbols: &[CodeNode],
430        relations: &mut Vec<SymbolRelation>,
431        compiled: &CompiledQueries,
432    ) {
433        let mut cursor = QueryCursor::new();
434        let matches = cursor.matches(&compiled.calls, tree.root_node(), source.as_bytes());
435
436        for match_ in matches {
437            let mut callee_name: Option<&str> = None;
438            let mut call_line: u32 = 0;
439
440            for capture in match_.captures {
441                let capture_name = compiled.calls.capture_names()[capture.index as usize];
442                let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
443
444                match capture_name {
445                    "callee" | "function" | "call.function" => {
446                        // Handle method calls like obj.method()
447                        if let Some(dot_pos) = text.rfind('.') {
448                            callee_name = Some(&text[dot_pos + 1..]);
449                        } else {
450                            callee_name = Some(text);
451                        }
452                        call_line = capture.node.start_position().row as u32 + 1;
453                    }
454                    _ => {}
455                }
456            }
457
458            if let Some(callee) = callee_name {
459                // Find the enclosing function/method
460                let caller_id = self
461                    .find_enclosing_symbol(call_line, symbols)
462                    .map(|s| s.id.clone())
463                    .unwrap_or_else(|| format!("{}:__file__", file_path));
464
465                relations.push(SymbolRelation {
466                    from_id: caller_id,
467                    to_name: callee.to_string(),
468                    kind: RelationType::Calls,
469                    line: call_line,
470                });
471            }
472        }
473    }
474
475    fn find_enclosing_symbol<'a>(
476        &self,
477        line: u32,
478        symbols: &'a [CodeNode],
479    ) -> Option<&'a CodeNode> {
480        symbols
481            .iter()
482            .filter(|s| s.line_start <= line && s.line_end >= line)
483            .min_by_key(|s| s.line_end - s.line_start) // Smallest enclosing
484    }
485
486    // ─────────────────────────────────────────────────────────────────────────
487    // Query Registry Helper
488    // Extracts S-expressions into reusable strings. Eliminates duplication across
489    // compile functions. Makes adding new languages trivial (just add compile_xxx).
490    // Aligns with Accessibility (easy extension) and Affordability (less code).
491    // ─────────────────────────────────────────────────────────────────────────
492
493    fn compile_queries(
494        language: Language,
495        symbols_query: &str,
496        imports_query: &str,
497        calls_query: &str,
498    ) -> Result<CompiledQueries> {
499        let symbols = Query::new(&language, symbols_query)
500            .map_err(|e| ParseError::QueryError(e.to_string()))?;
501        let imports = Query::new(&language, imports_query)
502            .map_err(|e| ParseError::QueryError(e.to_string()))?;
503        let calls = Query::new(&language, calls_query)
504            .map_err(|e| ParseError::QueryError(e.to_string()))?;
505
506        Ok(CompiledQueries {
507            symbols,
508            imports,
509            calls,
510            language,
511        })
512    }
513
514    fn compile_typescript_queries() -> Result<CompiledQueries> {
515        let language = tree_sitter_typescript::language_typescript();
516
517        let symbols_query = r#"
518            (function_declaration name: (identifier) @name) @function_def
519            (class_declaration name: (type_identifier) @name) @class_def
520            (method_definition name: (property_identifier) @name) @method_def
521            (interface_declaration name: (type_identifier) @name) @interface_def
522            (type_alias_declaration name: (type_identifier) @name) @interface_def
523        "#;
524
525        let imports_query = r#"
526            (import_statement
527                source: (string) @source)
528        "#;
529
530        let calls_query = r#"
531            (call_expression
532                function: (identifier) @callee)
533
534            (call_expression
535                function: (member_expression
536                    property: (property_identifier) @callee))
537        "#;
538
539        Self::compile_queries(language, symbols_query, imports_query, calls_query)
540    }
541
542    fn compile_rust_queries() -> Result<CompiledQueries> {
543        let language = tree_sitter_rust::language();
544
545        let symbols_query = r#"
546            (function_item name: (identifier) @name) @function_def
547            (struct_item name: (type_identifier) @name) @struct_def
548            (enum_item name: (type_identifier) @name) @enum_def
549            (trait_item name: (type_identifier) @name) @trait_def
550        "#;
551
552        let imports_query = r#"
553            (use_declaration) @source
554        "#;
555
556        let calls_query = r#"
557            (call_expression function: (identifier) @callee)
558            (call_expression function: (field_expression field: (field_identifier) @callee))
559        "#;
560
561        Self::compile_queries(language, symbols_query, imports_query, calls_query)
562    }
563
564    fn compile_python_queries() -> Result<CompiledQueries> {
565        let language = tree_sitter_python::language();
566
567        let symbols_query = r#"
568            (function_definition name: (identifier) @name) @function_def
569            (class_definition name: (identifier) @name) @class_def
570        "#;
571
572        let imports_query = r#"
573            (import_statement) @source
574            (import_from_statement) @source
575        "#;
576
577        let calls_query = r#"
578            (call function: (identifier) @callee)
579            (call function: (attribute attribute: (identifier) @callee))
580        "#;
581
582        Self::compile_queries(language, symbols_query, imports_query, calls_query)
583    }
584
585    fn compile_go_queries() -> Result<CompiledQueries> {
586        let language = tree_sitter_go::language();
587
588        let symbols_query = r#"
589            (function_declaration name: (identifier) @name) @function_def
590            (method_declaration name: (field_identifier) @name) @method_def
591            (type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @struct_def
592            (type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @interface_def
593        "#;
594
595        let imports_query = r#"
596            (import_spec path: (interpreted_string_literal) @source)
597        "#;
598
599        let calls_query = r#"
600            (call_expression function: (identifier) @callee)
601            (call_expression function: (selector_expression field: (field_identifier) @callee))
602        "#;
603
604        Self::compile_queries(language, symbols_query, imports_query, calls_query)
605    }
606
607    fn compile_java_queries() -> Result<CompiledQueries> {
608        let language = tree_sitter_java::language();
609
610        let symbols_query = r#"
611            (method_declaration name: (identifier) @name) @method_def
612            (class_declaration name: (identifier) @name) @class_def
613            (interface_declaration name: (identifier) @name) @interface_def
614            (constructor_declaration name: (identifier) @name) @function_def
615        "#;
616
617        let imports_query = r#"
618            (import_declaration) @source
619        "#;
620
621        let calls_query = r#"
622            (method_invocation name: (identifier) @callee)
623        "#;
624
625        Self::compile_queries(language, symbols_query, imports_query, calls_query)
626    }
627
628    fn compile_c_queries() -> Result<CompiledQueries> {
629        let language = tree_sitter_c::language();
630
631        let symbols_query = r#"
632            (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
633            (struct_specifier name: (type_identifier) @name) @struct_def
634            (enum_specifier name: (type_identifier) @name) @enum_def
635        "#;
636
637        let imports_query = r#"
638            (preproc_include path: (string_literal) @source)
639            (preproc_include path: (system_lib_string) @source)
640        "#;
641
642        let calls_query = r#"
643            (call_expression function: (identifier) @callee)
644        "#;
645
646        Self::compile_queries(language, symbols_query, imports_query, calls_query)
647    }
648
649    fn compile_cpp_queries() -> Result<CompiledQueries> {
650        let language = tree_sitter_cpp::language();
651
652        let symbols_query = r#"
653            (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
654            (function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @method_def
655            (class_specifier name: (type_identifier) @name) @class_def
656            (struct_specifier name: (type_identifier) @name) @struct_def
657        "#;
658
659        let imports_query = r#"
660            (preproc_include path: (string_literal) @source)
661            (preproc_include path: (system_lib_string) @source)
662        "#;
663
664        let calls_query = r#"
665            (call_expression function: (identifier) @callee)
666            (call_expression function: (field_expression field: (field_identifier) @callee))
667        "#;
668
669        Self::compile_queries(language, symbols_query, imports_query, calls_query)
670    }
671
672    fn compile_csharp_queries() -> Result<CompiledQueries> {
673        let language = tree_sitter_c_sharp::language();
674
675        let symbols_query = r#"
676            (method_declaration name: (identifier) @name) @method_def
677            (class_declaration name: (identifier) @name) @class_def
678            (interface_declaration name: (identifier) @name) @interface_def
679            (struct_declaration name: (identifier) @name) @struct_def
680            (constructor_declaration name: (identifier) @name) @function_def
681            (property_declaration name: (identifier) @name) @method_def
682        "#;
683
684        let imports_query = r#"
685            (using_directive (identifier) @source)
686            (using_directive (qualified_name) @source)
687        "#;
688
689        let calls_query = r#"
690            (invocation_expression function: (identifier) @callee)
691            (invocation_expression function: (member_access_expression name: (identifier) @callee))
692        "#;
693
694        Self::compile_queries(language, symbols_query, imports_query, calls_query)
695    }
696}
697
698// ─────────────────────────────────────────────────────────────────────────────
699// Tests
700// ─────────────────────────────────────────────────────────────────────────────
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_parser_initialization() {
708        // This test will show us the actual error if query compilation fails
709        match ArborParser::new() {
710            Ok(_) => println!("Parser initialized successfully!"),
711            Err(e) => panic!("Parser failed to initialize: {}", e),
712        }
713    }
714
715    #[test]
716    fn test_parse_typescript_symbols() {
717        let mut parser = ArborParser::new().unwrap();
718
719        let source = r#"
720            function greet(name: string): string {
721                return `Hello, ${name}!`;
722            }
723
724            export class UserService {
725                validate(user: User): boolean {
726                    return true;
727                }
728            }
729
730            interface User {
731                name: string;
732                email: string;
733            }
734        "#;
735
736        let result = parser.parse_source(source, "test.ts", "ts").unwrap();
737
738        assert!(result.symbols.iter().any(|s| s.name == "greet"));
739        assert!(result.symbols.iter().any(|s| s.name == "UserService"));
740        assert!(result.symbols.iter().any(|s| s.name == "validate"));
741        assert!(result.symbols.iter().any(|s| s.name == "User"));
742    }
743
744    #[test]
745    fn test_parse_typescript_imports() {
746        let mut parser = ArborParser::new().unwrap();
747
748        let source = r#"
749            import { useState } from 'react';
750            import lodash from 'lodash';
751
752            function Component() {
753                const [count, setCount] = useState(0);
754            }
755        "#;
756
757        let result = parser.parse_source(source, "test.ts", "ts").unwrap();
758
759        let imports: Vec<_> = result
760            .relations
761            .iter()
762            .filter(|r| r.kind == RelationType::Imports)
763            .collect();
764
765        assert!(imports.iter().any(|i| i.to_name.contains("react")));
766        assert!(imports.iter().any(|i| i.to_name.contains("lodash")));
767    }
768
769    #[test]
770    fn test_parse_typescript_calls() {
771        let mut parser = ArborParser::new().unwrap();
772
773        let source = r#"
774            function outer() {
775                inner();
776                helper.process();
777            }
778
779            function inner() {
780                console.log("Hello");
781            }
782        "#;
783
784        let result = parser.parse_source(source, "test.ts", "ts").unwrap();
785
786        let calls: Vec<_> = result
787            .relations
788            .iter()
789            .filter(|r| r.kind == RelationType::Calls)
790            .collect();
791
792        assert!(calls.iter().any(|c| c.to_name == "inner"));
793        assert!(calls.iter().any(|c| c.to_name == "process"));
794        assert!(calls.iter().any(|c| c.to_name == "log"));
795    }
796
797    #[test]
798    fn test_parse_rust_symbols() {
799        let mut parser = ArborParser::new().unwrap();
800
801        let source = r#"
802            fn main() {
803                println!("Hello!");
804            }
805
806            pub struct User {
807                name: String,
808            }
809
810            impl User {
811                fn new(name: &str) -> Self {
812                    Self { name: name.to_string() }
813                }
814            }
815
816            enum Status {
817                Active,
818                Inactive,
819            }
820        "#;
821
822        let result = parser.parse_source(source, "test.rs", "rs").unwrap();
823
824        assert!(result.symbols.iter().any(|s| s.name == "main"));
825        assert!(result.symbols.iter().any(|s| s.name == "User"));
826        assert!(result.symbols.iter().any(|s| s.name == "new"));
827        assert!(result.symbols.iter().any(|s| s.name == "Status"));
828    }
829
830    #[test]
831    fn test_parse_python_symbols() {
832        let mut parser = ArborParser::new().unwrap();
833
834        let source = r#"
835def greet(name):
836    return f"Hello, {name}!"
837
838class UserService:
839    def validate(self, user):
840        return True
841        "#;
842
843        let result = parser.parse_source(source, "test.py", "py").unwrap();
844
845        assert!(result.symbols.iter().any(|s| s.name == "greet"));
846        assert!(result.symbols.iter().any(|s| s.name == "UserService"));
847        assert!(result.symbols.iter().any(|s| s.name == "validate"));
848    }
849
850    #[test]
851    fn test_parse_fallback_kotlin_symbols() {
852        let mut parser = ArborParser::new().unwrap();
853
854        let source = r#"
855            class BillingService
856            fun computeInvoiceTotal(amount: Double): Double = amount
857        "#;
858
859        let result = parser.parse_source(source, "billing.kt", "kt").unwrap();
860
861        assert!(result.symbols.iter().any(|s| s.name == "BillingService"));
862        assert!(result
863            .symbols
864            .iter()
865            .any(|s| s.name == "computeInvoiceTotal"));
866        assert!(result.relations.is_empty());
867    }
868
869    #[test]
870    fn test_parse_go_symbols() {
871        let mut parser = ArborParser::new().unwrap();
872
873        let source = r#"
874package main
875
876import "fmt"
877
878func greet(name string) string {
879    return fmt.Sprintf("Hello, %s!", name)
880}
881
882type User struct {
883    Name string
884    Age  int
885}
886
887type Service interface {
888    Process(data []byte) error
889}
890"#;
891
892        let result = parser.parse_source(source, "main.go", "go").unwrap();
893
894        assert!(result.symbols.iter().any(|s| s.name == "greet"));
895        assert!(result.symbols.iter().any(|s| s.name == "User"));
896        assert!(result.symbols.iter().any(|s| s.name == "Service"));
897    }
898
899    #[test]
900    fn test_parse_java_symbols() {
901        let mut parser = ArborParser::new().unwrap();
902
903        let source = r#"
904package com.example;
905
906import java.util.List;
907
908public class OrderService {
909    public void processOrder(String orderId) {
910        validate(orderId);
911    }
912
913    private void validate(String id) {
914    }
915}
916"#;
917
918        let result = parser
919            .parse_source(source, "OrderService.java", "java")
920            .unwrap();
921
922        assert!(result.symbols.iter().any(|s| s.name == "OrderService"));
923        assert!(result.symbols.iter().any(|s| s.name == "processOrder"));
924        assert!(result.symbols.iter().any(|s| s.name == "validate"));
925    }
926
927    #[test]
928    fn test_parse_c_symbols() {
929        let mut parser = ArborParser::new().unwrap();
930
931        let source = r#"
932#include <stdio.h>
933
934struct Point {
935    int x;
936    int y;
937};
938
939void print_point(struct Point p) {
940    printf("(%d, %d)\n", p.x, p.y);
941}
942
943int add(int a, int b) {
944    return a + b;
945}
946"#;
947
948        let result = parser.parse_source(source, "math.c", "c").unwrap();
949
950        assert!(result.symbols.iter().any(|s| s.name == "Point"));
951        assert!(result.symbols.iter().any(|s| s.name == "print_point"));
952        assert!(result.symbols.iter().any(|s| s.name == "add"));
953    }
954
955    #[test]
956    fn test_parse_cpp_symbols() {
957        let mut parser = ArborParser::new().unwrap();
958
959        let source = r#"
960#include <iostream>
961
962class Calculator {
963public:
964    int add(int a, int b) {
965        return a + b;
966    }
967};
968
969struct Config {
970    int timeout;
971};
972
973void helpers() {
974    std::cout << "ok" << std::endl;
975}
976"#;
977
978        let result = parser.parse_source(source, "calc.cpp", "cpp").unwrap();
979
980        assert!(result.symbols.iter().any(|s| s.name == "Calculator"));
981        assert!(result.symbols.iter().any(|s| s.name == "Config"));
982        assert!(result.symbols.iter().any(|s| s.name == "helpers"));
983    }
984
985    #[test]
986    fn test_parse_csharp_symbols() {
987        let mut parser = ArborParser::new().unwrap();
988
989        let source = r#"
990using System;
991
992namespace MyApp
993{
994    public class UserController
995    {
996        public string GetUser(int id)
997        {
998            return "user";
999        }
1000    }
1001
1002    public interface IRepository
1003    {
1004        void Save(string data);
1005    }
1006}
1007"#;
1008
1009        let result = parser
1010            .parse_source(source, "UserController.cs", "cs")
1011            .unwrap();
1012
1013        assert!(result.symbols.iter().any(|s| s.name == "UserController"));
1014        assert!(result.symbols.iter().any(|s| s.name == "GetUser"));
1015        assert!(result.symbols.iter().any(|s| s.name == "IRepository"));
1016        assert!(result.symbols.iter().any(|s| s.name == "Save"));
1017    }
1018
1019    #[test]
1020    fn test_parse_unsupported_extension_errors() {
1021        let mut parser = ArborParser::new().unwrap();
1022        let result = parser.parse_source("anything", "test.xyz", "xyz");
1023        assert!(result.is_err());
1024    }
1025
1026    #[test]
1027    fn test_parse_result_file_path() {
1028        let mut parser = ArborParser::new().unwrap();
1029        let result = parser
1030            .parse_source("fn main() {}", "test.rs", "rs")
1031            .unwrap();
1032        assert_eq!(result.file_path, "test.rs");
1033    }
1034
1035    #[test]
1036    fn test_parse_python_imports_detected() {
1037        let mut parser = ArborParser::new().unwrap();
1038
1039        let source = r#"
1040import os
1041from pathlib import Path
1042
1043def read_file(path):
1044    with open(path) as f:
1045        return f.read()
1046"#;
1047
1048        let result = parser.parse_source(source, "utils.py", "py").unwrap();
1049        assert!(result.symbols.iter().any(|s| s.name == "read_file"));
1050        assert!(result
1051            .relations
1052            .iter()
1053            .any(|r| r.kind == RelationType::Imports));
1054    }
1055}