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