1use 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#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct SymbolRelation {
26 pub from_id: String,
28 pub to_name: String,
30 pub kind: RelationType,
32 pub line: u32,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum RelationType {
39 Calls,
41 Imports,
43 Extends,
45 Implements,
47}
48
49#[derive(Debug)]
51pub struct ParseResult {
52 pub symbols: Vec<CodeNode>,
54 pub relations: Vec<SymbolRelation>,
56 pub file_path: String,
58}
59
60pub struct ArborParser {
69 parser: Parser,
71 queries: HashMap<String, CompiledQueries>,
73}
74
75struct CompiledQueries {
77 symbols: Query,
79 imports: Query,
81 calls: Query,
83 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 pub fn new() -> Result<Self> {
110 let parser = Parser::new();
111 let mut queries = HashMap::new();
112
113 for ext in &["ts", "tsx", "js", "jsx"] {
115 let compiled = Self::compile_typescript_queries()?;
116 queries.insert(ext.to_string(), compiled);
117 }
118
119 let rs_queries = Self::compile_rust_queries()?;
121 queries.insert("rs".to_string(), rs_queries);
122
123 let py_queries = Self::compile_python_queries()?;
125 queries.insert("py".to_string(), py_queries);
126
127 let go_queries = Self::compile_go_queries()?;
129 queries.insert("go".to_string(), go_queries);
130
131 let java_queries = Self::compile_java_queries()?;
133 queries.insert("java".to_string(), java_queries);
134
135 for ext in &["c", "h"] {
137 queries.insert(ext.to_string(), Self::compile_c_queries()?);
138 }
139
140 for ext in &["cpp", "hpp", "cc", "hh", "cxx", "hxx"] {
142 queries.insert(ext.to_string(), Self::compile_cpp_queries()?);
143 }
144
145 let csharp_queries = Self::compile_csharp_queries()?;
148 queries.insert("cs".to_string(), csharp_queries);
149
150 Ok(Self { parser, queries })
151 }
152
153 pub fn parse_file(&mut self, path: &Path) -> Result<ParseResult> {
165 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 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 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 self.parser
200 .set_language(&compiled.language)
201 .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
202
203 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 let symbols = self.extract_symbols(&tree, &source, &file_path, file_name, compiled);
224
225 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 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 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(
254 source, file_path, &language,
255 ),
256 relations: Vec::new(),
257 file_path: file_path.to_string(),
258 });
259 }
260 return Err(ParseError::UnsupportedLanguage(file_path.into()));
261 }
262 };
263
264 self.parser
265 .set_language(&compiled.language)
266 .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
267
268 let Some(tree) = self.parser.parse(source, None) else {
269 warn!(
270 "Tree-sitter returned no tree for in-memory source '{}'; returning partial empty parse result",
271 file_path
272 );
273 return Ok(ParseResult {
274 symbols: Vec::new(),
275 relations: Vec::new(),
276 file_path: file_path.to_string(),
277 });
278 };
279
280 let file_name = Path::new(file_path)
281 .file_name()
282 .and_then(|n| n.to_str())
283 .unwrap_or("unknown");
284
285 let symbols = self.extract_symbols(&tree, source, file_path, file_name, compiled);
286 let relations = self.extract_relations(&tree, source, file_path, &symbols, compiled);
287
288 Ok(ParseResult {
289 symbols,
290 relations,
291 file_path: file_path.to_string(),
292 })
293 }
294
295 fn extract_symbols(
300 &self,
301 tree: &Tree,
302 source: &str,
303 file_path: &str,
304 file_name: &str,
305 compiled: &CompiledQueries,
306 ) -> Vec<CodeNode> {
307 let mut symbols = Vec::new();
308 let mut cursor = QueryCursor::new();
309 let symbol_capture_names = compiled.symbols.capture_names();
310 let source_bytes = source.as_bytes();
311
312 let matches = cursor.matches(&compiled.symbols, tree.root_node(), source_bytes);
313
314 for match_ in matches {
315 let mut name: Option<&str> = None;
317 let mut kind: Option<NodeKind> = None;
318 let mut node = match_.captures.first().map(|c| c.node);
319
320 for capture in match_.captures {
321 let Some(capture_name) = symbol_capture_names.get(capture.index as usize) else {
322 warn!(
323 "Symbol capture index out of bounds (index={} file='{}')",
324 capture.index, file_path
325 );
326 continue;
327 };
328
329 let Some(text) = Self::node_text(capture.node, source_bytes, file_path) else {
330 continue;
331 };
332
333 let capture_name = *capture_name;
334 match capture_name {
335 "name" | "function.name" | "class.name" | "interface.name" | "method.name" => {
336 name = Some(text);
337 }
338 "function" | "function_def" => {
339 kind = Some(NodeKind::Function);
340 node = Some(capture.node);
341 }
342 "class" | "class_def" => {
343 kind = Some(NodeKind::Class);
344 node = Some(capture.node);
345 }
346 "interface" | "interface_def" => {
347 kind = Some(NodeKind::Interface);
348 node = Some(capture.node);
349 }
350 "method" | "method_def" => {
351 kind = Some(NodeKind::Method);
352 node = Some(capture.node);
353 }
354 "struct" | "struct_def" => {
355 kind = Some(NodeKind::Struct);
356 node = Some(capture.node);
357 }
358 "enum" | "enum_def" => {
359 kind = Some(NodeKind::Enum);
360 node = Some(capture.node);
361 }
362 "trait" | "trait_def" => {
363 kind = Some(NodeKind::Interface);
364 node = Some(capture.node);
365 }
366 _ => {}
367 }
368 }
369
370 if let (Some(name), Some(kind), Some(node)) = (name, kind, node) {
371 let qualified_name = format!("{}:{}", file_name, name);
373
374 let signature = Self::first_line_signature(source, node);
376
377 let mut symbol = CodeNode::new(name, &qualified_name, kind, file_path)
378 .with_lines(
379 node.start_position().row as u32 + 1,
380 node.end_position().row as u32 + 1,
381 )
382 .with_column(node.start_position().column as u32)
383 .with_bytes(node.start_byte() as u32, node.end_byte() as u32);
384
385 if let Some(sig) = signature {
386 symbol = symbol.with_signature(sig.to_owned());
387 }
388
389 symbols.push(symbol);
390 }
391 }
392
393 symbols
394 }
395
396 fn extract_relations(
401 &self,
402 tree: &Tree,
403 source: &str,
404 file_path: &str,
405 symbols: &[CodeNode],
406 compiled: &CompiledQueries,
407 ) -> Vec<SymbolRelation> {
408 let mut relations = Vec::new();
409 let mut cursor = QueryCursor::new();
410
411 self.extract_imports(
413 tree,
414 source,
415 file_path,
416 &mut cursor,
417 &mut relations,
418 compiled,
419 );
420
421 self.extract_calls(
423 tree,
424 source,
425 file_path,
426 symbols,
427 &mut cursor,
428 &mut relations,
429 compiled,
430 );
431
432 relations
433 }
434
435 fn extract_imports(
436 &self,
437 tree: &Tree,
438 source: &str,
439 file_path: &str,
440 cursor: &mut QueryCursor,
441 relations: &mut Vec<SymbolRelation>,
442 compiled: &CompiledQueries,
443 ) {
444 let import_capture_names = compiled.imports.capture_names();
445 let source_bytes = source.as_bytes();
446 let file_id = format!("{}:__file__", file_path);
447 let matches = cursor.matches(&compiled.imports, tree.root_node(), source_bytes);
448
449 for match_ in matches {
450 let mut module_name: Option<&str> = None;
451 let mut line: u32 = 0;
452
453 for capture in match_.captures {
454 let Some(capture_name) = import_capture_names.get(capture.index as usize) else {
455 warn!(
456 "Import capture index out of bounds (index={} file='{}')",
457 capture.index, file_path
458 );
459 continue;
460 };
461
462 let Some(text) = Self::node_text(capture.node, source_bytes, file_path) else {
463 continue;
464 };
465
466 let capture_name = *capture_name;
467 match capture_name {
468 "source" | "module" | "import.source" => {
469 module_name = Some(text.trim_matches(|c| c == '"' || c == '\''));
471 line = capture.node.start_position().row as u32 + 1;
472 }
473 _ => {}
474 }
475 }
476
477 if let Some(module) = module_name {
478 relations.push(SymbolRelation {
480 from_id: file_id.clone(),
481 to_name: module.to_string(),
482 kind: RelationType::Imports,
483 line,
484 });
485 }
486 }
487 }
488
489 fn extract_calls(
490 &self,
491 tree: &Tree,
492 source: &str,
493 file_path: &str,
494 symbols: &[CodeNode],
495 cursor: &mut QueryCursor,
496 relations: &mut Vec<SymbolRelation>,
497 compiled: &CompiledQueries,
498 ) {
499 let call_capture_names = compiled.calls.capture_names();
500 let source_bytes = source.as_bytes();
501 let file_id = format!("{}:__file__", file_path);
502 let matches = cursor.matches(&compiled.calls, tree.root_node(), source_bytes);
503
504 for match_ in matches {
505 let mut callee_name: Option<&str> = None;
506 let mut call_line: u32 = 0;
507
508 for capture in match_.captures {
509 let Some(capture_name) = call_capture_names.get(capture.index as usize) else {
510 warn!(
511 "Call capture index out of bounds (index={} file='{}')",
512 capture.index, file_path
513 );
514 continue;
515 };
516
517 let Some(text) = Self::node_text(capture.node, source_bytes, file_path) else {
518 continue;
519 };
520
521 let capture_name = *capture_name;
522 match capture_name {
523 "callee" | "function" | "call.function" => {
524 callee_name = Some(text.rsplit('.').next().unwrap_or(text));
526 call_line = capture.node.start_position().row as u32 + 1;
527 }
528 _ => {}
529 }
530 }
531
532 if let Some(callee) = callee_name {
533 let caller_id = self
535 .find_enclosing_symbol(call_line, symbols)
536 .map(|s| s.id.clone())
537 .unwrap_or_else(|| file_id.clone());
538
539 relations.push(SymbolRelation {
540 from_id: caller_id,
541 to_name: callee.to_string(),
542 kind: RelationType::Calls,
543 line: call_line,
544 });
545 }
546 }
547 }
548
549 fn find_enclosing_symbol<'a>(
550 &self,
551 line: u32,
552 symbols: &'a [CodeNode],
553 ) -> Option<&'a CodeNode> {
554 symbols
555 .iter()
556 .filter(|s| s.line_start <= line && s.line_end >= line)
557 .min_by_key(|s| s.line_end - s.line_start) }
559
560 #[inline]
561 fn node_text<'a>(node: Node<'a>, source_bytes: &'a [u8], file_path: &str) -> Option<&'a str> {
562 match node.utf8_text(source_bytes) {
563 Ok(text) => Some(text),
564 Err(error) => {
565 warn!(
566 "Skipping invalid UTF-8 capture in file '{}' at row {}: {}",
567 file_path,
568 node.start_position().row,
569 error
570 );
571 None
572 }
573 }
574 }
575
576 #[inline]
577 fn first_line_signature<'a>(source: &'a str, node: Node<'_>) -> Option<&'a str> {
578 let tail = source.get(node.start_byte()..)?;
579 let signature = tail.lines().next()?.trim();
580 if signature.is_empty() {
581 None
582 } else {
583 Some(signature)
584 }
585 }
586
587 fn compile_queries(
595 language: Language,
596 symbols_query: &str,
597 imports_query: &str,
598 calls_query: &str,
599 ) -> Result<CompiledQueries> {
600 let symbols = Query::new(&language, symbols_query)
601 .map_err(|e| ParseError::QueryError(e.to_string()))?;
602 let imports = Query::new(&language, imports_query)
603 .map_err(|e| ParseError::QueryError(e.to_string()))?;
604 let calls = Query::new(&language, calls_query)
605 .map_err(|e| ParseError::QueryError(e.to_string()))?;
606
607 Ok(CompiledQueries {
608 symbols,
609 imports,
610 calls,
611 language,
612 })
613 }
614
615 fn compile_typescript_queries() -> Result<CompiledQueries> {
616 let language = tree_sitter_typescript::language_typescript();
617
618 let symbols_query = r#"
619 (function_declaration name: (identifier) @name) @function_def
620 (class_declaration name: (type_identifier) @name) @class_def
621 (method_definition name: (property_identifier) @name) @method_def
622 (interface_declaration name: (type_identifier) @name) @interface_def
623 (type_alias_declaration name: (type_identifier) @name) @interface_def
624 "#;
625
626 let imports_query = r#"
627 (import_statement
628 source: (string) @source)
629 "#;
630
631 let calls_query = r#"
632 (call_expression
633 function: (identifier) @callee)
634
635 (call_expression
636 function: (member_expression
637 property: (property_identifier) @callee))
638 "#;
639
640 Self::compile_queries(language, symbols_query, imports_query, calls_query)
641 }
642
643 fn compile_rust_queries() -> Result<CompiledQueries> {
644 let language = tree_sitter_rust::language();
645
646 let symbols_query = r#"
647 (function_item name: (identifier) @name) @function_def
648 (struct_item name: (type_identifier) @name) @struct_def
649 (enum_item name: (type_identifier) @name) @enum_def
650 (trait_item name: (type_identifier) @name) @trait_def
651 "#;
652
653 let imports_query = r#"
654 (use_declaration) @source
655 "#;
656
657 let calls_query = r#"
658 (call_expression function: (identifier) @callee)
659 (call_expression function: (field_expression field: (field_identifier) @callee))
660 "#;
661
662 Self::compile_queries(language, symbols_query, imports_query, calls_query)
663 }
664
665 fn compile_python_queries() -> Result<CompiledQueries> {
666 let language = tree_sitter_python::language();
667
668 let symbols_query = r#"
669 (function_definition name: (identifier) @name) @function_def
670 (class_definition name: (identifier) @name) @class_def
671 "#;
672
673 let imports_query = r#"
674 (import_statement) @source
675 (import_from_statement) @source
676 "#;
677
678 let calls_query = r#"
679 (call function: (identifier) @callee)
680 (call function: (attribute attribute: (identifier) @callee))
681 "#;
682
683 Self::compile_queries(language, symbols_query, imports_query, calls_query)
684 }
685
686 fn compile_go_queries() -> Result<CompiledQueries> {
687 let language = tree_sitter_go::language();
688
689 let symbols_query = r#"
690 (function_declaration name: (identifier) @name) @function_def
691 (method_declaration name: (field_identifier) @name) @method_def
692 (type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @struct_def
693 (type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @interface_def
694 "#;
695
696 let imports_query = r#"
697 (import_spec path: (interpreted_string_literal) @source)
698 "#;
699
700 let calls_query = r#"
701 (call_expression function: (identifier) @callee)
702 (call_expression function: (selector_expression field: (field_identifier) @callee))
703 "#;
704
705 Self::compile_queries(language, symbols_query, imports_query, calls_query)
706 }
707
708 fn compile_java_queries() -> Result<CompiledQueries> {
709 let language = tree_sitter_java::language();
710
711 let symbols_query = r#"
712 (method_declaration name: (identifier) @name) @method_def
713 (class_declaration name: (identifier) @name) @class_def
714 (interface_declaration name: (identifier) @name) @interface_def
715 (constructor_declaration name: (identifier) @name) @function_def
716 "#;
717
718 let imports_query = r#"
719 (import_declaration) @source
720 "#;
721
722 let calls_query = r#"
723 (method_invocation name: (identifier) @callee)
724 "#;
725
726 Self::compile_queries(language, symbols_query, imports_query, calls_query)
727 }
728
729 fn compile_c_queries() -> Result<CompiledQueries> {
730 let language = tree_sitter_c::language();
731
732 let symbols_query = r#"
733 (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
734 (struct_specifier name: (type_identifier) @name) @struct_def
735 (enum_specifier name: (type_identifier) @name) @enum_def
736 "#;
737
738 let imports_query = r#"
739 (preproc_include path: (string_literal) @source)
740 (preproc_include path: (system_lib_string) @source)
741 "#;
742
743 let calls_query = r#"
744 (call_expression function: (identifier) @callee)
745 "#;
746
747 Self::compile_queries(language, symbols_query, imports_query, calls_query)
748 }
749
750 fn compile_cpp_queries() -> Result<CompiledQueries> {
751 let language = tree_sitter_cpp::language();
752
753 let symbols_query = r#"
754 (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
755 (function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @method_def
756 (class_specifier name: (type_identifier) @name) @class_def
757 (struct_specifier name: (type_identifier) @name) @struct_def
758 "#;
759
760 let imports_query = r#"
761 (preproc_include path: (string_literal) @source)
762 (preproc_include path: (system_lib_string) @source)
763 "#;
764
765 let calls_query = r#"
766 (call_expression function: (identifier) @callee)
767 (call_expression function: (field_expression field: (field_identifier) @callee))
768 "#;
769
770 Self::compile_queries(language, symbols_query, imports_query, calls_query)
771 }
772
773 fn compile_csharp_queries() -> Result<CompiledQueries> {
774 let language = tree_sitter_c_sharp::language();
775
776 let symbols_query = r#"
777 (method_declaration name: (identifier) @name) @method_def
778 (class_declaration name: (identifier) @name) @class_def
779 (interface_declaration name: (identifier) @name) @interface_def
780 (struct_declaration name: (identifier) @name) @struct_def
781 (constructor_declaration name: (identifier) @name) @function_def
782 (property_declaration name: (identifier) @name) @method_def
783 "#;
784
785 let imports_query = r#"
786 (using_directive (identifier) @source)
787 (using_directive (qualified_name) @source)
788 "#;
789
790 let calls_query = r#"
791 (invocation_expression function: (identifier) @callee)
792 (invocation_expression function: (member_access_expression name: (identifier) @callee))
793 "#;
794
795 Self::compile_queries(language, symbols_query, imports_query, calls_query)
796 }
797}
798
799#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
808 fn test_parser_initialization() {
809 match ArborParser::new() {
811 Ok(_) => println!("Parser initialized successfully!"),
812 Err(e) => panic!("Parser failed to initialize: {}", e),
813 }
814 }
815
816 #[test]
817 fn test_parse_typescript_symbols() {
818 let mut parser = ArborParser::new().unwrap();
819
820 let source = r#"
821 function greet(name: string): string {
822 return `Hello, ${name}!`;
823 }
824
825 export class UserService {
826 validate(user: User): boolean {
827 return true;
828 }
829 }
830
831 interface User {
832 name: string;
833 email: string;
834 }
835 "#;
836
837 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
838
839 assert!(result.symbols.iter().any(|s| s.name == "greet"));
840 assert!(result.symbols.iter().any(|s| s.name == "UserService"));
841 assert!(result.symbols.iter().any(|s| s.name == "validate"));
842 assert!(result.symbols.iter().any(|s| s.name == "User"));
843 }
844
845 #[test]
846 fn test_parse_typescript_imports() {
847 let mut parser = ArborParser::new().unwrap();
848
849 let source = r#"
850 import { useState } from 'react';
851 import lodash from 'lodash';
852
853 function Component() {
854 const [count, setCount] = useState(0);
855 }
856 "#;
857
858 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
859
860 let imports: Vec<_> = result
861 .relations
862 .iter()
863 .filter(|r| r.kind == RelationType::Imports)
864 .collect();
865
866 assert!(imports.iter().any(|i| i.to_name.contains("react")));
867 assert!(imports.iter().any(|i| i.to_name.contains("lodash")));
868 }
869
870 #[test]
871 fn test_parse_typescript_calls() {
872 let mut parser = ArborParser::new().unwrap();
873
874 let source = r#"
875 function outer() {
876 inner();
877 helper.process();
878 }
879
880 function inner() {
881 console.log("Hello");
882 }
883 "#;
884
885 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
886
887 let calls: Vec<_> = result
888 .relations
889 .iter()
890 .filter(|r| r.kind == RelationType::Calls)
891 .collect();
892
893 assert!(calls.iter().any(|c| c.to_name == "inner"));
894 assert!(calls.iter().any(|c| c.to_name == "process"));
895 assert!(calls.iter().any(|c| c.to_name == "log"));
896 }
897
898 #[test]
899 fn test_parse_rust_symbols() {
900 let mut parser = ArborParser::new().unwrap();
901
902 let source = r#"
903 fn main() {
904 println!("Hello!");
905 }
906
907 pub struct User {
908 name: String,
909 }
910
911 impl User {
912 fn new(name: &str) -> Self {
913 Self { name: name.to_string() }
914 }
915 }
916
917 enum Status {
918 Active,
919 Inactive,
920 }
921 "#;
922
923 let result = parser.parse_source(source, "test.rs", "rs").unwrap();
924
925 assert!(result.symbols.iter().any(|s| s.name == "main"));
926 assert!(result.symbols.iter().any(|s| s.name == "User"));
927 assert!(result.symbols.iter().any(|s| s.name == "new"));
928 assert!(result.symbols.iter().any(|s| s.name == "Status"));
929 }
930
931 #[test]
932 fn test_parse_python_symbols() {
933 let mut parser = ArborParser::new().unwrap();
934
935 let source = r#"
936def greet(name):
937 return f"Hello, {name}!"
938
939class UserService:
940 def validate(self, user):
941 return True
942 "#;
943
944 let result = parser.parse_source(source, "test.py", "py").unwrap();
945
946 assert!(result.symbols.iter().any(|s| s.name == "greet"));
947 assert!(result.symbols.iter().any(|s| s.name == "UserService"));
948 assert!(result.symbols.iter().any(|s| s.name == "validate"));
949 }
950
951 #[test]
952 fn test_parse_fallback_kotlin_symbols() {
953 let mut parser = ArborParser::new().unwrap();
954
955 let source = r#"
956 class BillingService
957 fun computeInvoiceTotal(amount: Double): Double = amount
958 "#;
959
960 let result = parser.parse_source(source, "billing.kt", "kt").unwrap();
961
962 assert!(result.symbols.iter().any(|s| s.name == "BillingService"));
963 assert!(result
964 .symbols
965 .iter()
966 .any(|s| s.name == "computeInvoiceTotal"));
967 assert!(result.relations.is_empty());
968 }
969
970 #[test]
971 fn test_parse_go_symbols() {
972 let mut parser = ArborParser::new().unwrap();
973
974 let source = r#"
975package main
976
977import "fmt"
978
979func greet(name string) string {
980 return fmt.Sprintf("Hello, %s!", name)
981}
982
983type User struct {
984 Name string
985 Age int
986}
987
988type Service interface {
989 Process(data []byte) error
990}
991"#;
992
993 let result = parser.parse_source(source, "main.go", "go").unwrap();
994
995 assert!(result.symbols.iter().any(|s| s.name == "greet"));
996 assert!(result.symbols.iter().any(|s| s.name == "User"));
997 assert!(result.symbols.iter().any(|s| s.name == "Service"));
998 }
999
1000 #[test]
1001 fn test_parse_java_symbols() {
1002 let mut parser = ArborParser::new().unwrap();
1003
1004 let source = r#"
1005package com.example;
1006
1007import java.util.List;
1008
1009public class OrderService {
1010 public void processOrder(String orderId) {
1011 validate(orderId);
1012 }
1013
1014 private void validate(String id) {
1015 }
1016}
1017"#;
1018
1019 let result = parser
1020 .parse_source(source, "OrderService.java", "java")
1021 .unwrap();
1022
1023 assert!(result.symbols.iter().any(|s| s.name == "OrderService"));
1024 assert!(result.symbols.iter().any(|s| s.name == "processOrder"));
1025 assert!(result.symbols.iter().any(|s| s.name == "validate"));
1026 }
1027
1028 #[test]
1029 fn test_parse_c_symbols() {
1030 let mut parser = ArborParser::new().unwrap();
1031
1032 let source = r#"
1033#include <stdio.h>
1034
1035struct Point {
1036 int x;
1037 int y;
1038};
1039
1040void print_point(struct Point p) {
1041 printf("(%d, %d)\n", p.x, p.y);
1042}
1043
1044int add(int a, int b) {
1045 return a + b;
1046}
1047"#;
1048
1049 let result = parser.parse_source(source, "math.c", "c").unwrap();
1050
1051 assert!(result.symbols.iter().any(|s| s.name == "Point"));
1052 assert!(result.symbols.iter().any(|s| s.name == "print_point"));
1053 assert!(result.symbols.iter().any(|s| s.name == "add"));
1054 }
1055
1056 #[test]
1057 fn test_parse_cpp_symbols() {
1058 let mut parser = ArborParser::new().unwrap();
1059
1060 let source = r#"
1061#include <iostream>
1062
1063class Calculator {
1064public:
1065 int add(int a, int b) {
1066 return a + b;
1067 }
1068};
1069
1070struct Config {
1071 int timeout;
1072};
1073
1074void helpers() {
1075 std::cout << "ok" << std::endl;
1076}
1077"#;
1078
1079 let result = parser.parse_source(source, "calc.cpp", "cpp").unwrap();
1080
1081 assert!(result.symbols.iter().any(|s| s.name == "Calculator"));
1082 assert!(result.symbols.iter().any(|s| s.name == "Config"));
1083 assert!(result.symbols.iter().any(|s| s.name == "helpers"));
1084 }
1085
1086 #[test]
1087 fn test_parse_csharp_symbols() {
1088 let mut parser = ArborParser::new().unwrap();
1089
1090 let source = r#"
1091using System;
1092
1093namespace MyApp
1094{
1095 public class UserController
1096 {
1097 public string GetUser(int id)
1098 {
1099 return "user";
1100 }
1101 }
1102
1103 public interface IRepository
1104 {
1105 void Save(string data);
1106 }
1107}
1108"#;
1109
1110 let result = parser
1111 .parse_source(source, "UserController.cs", "cs")
1112 .unwrap();
1113
1114 assert!(result.symbols.iter().any(|s| s.name == "UserController"));
1115 assert!(result.symbols.iter().any(|s| s.name == "GetUser"));
1116 assert!(result.symbols.iter().any(|s| s.name == "IRepository"));
1117 assert!(result.symbols.iter().any(|s| s.name == "Save"));
1118 }
1119
1120 #[test]
1121 fn test_parse_unsupported_extension_errors() {
1122 let mut parser = ArborParser::new().unwrap();
1123 let result = parser.parse_source("anything", "test.xyz", "xyz");
1124 assert!(result.is_err());
1125 }
1126
1127 #[test]
1128 fn test_parse_result_file_path() {
1129 let mut parser = ArborParser::new().unwrap();
1130 let result = parser
1131 .parse_source("fn main() {}", "test.rs", "rs")
1132 .unwrap();
1133 assert_eq!(result.file_path, "test.rs");
1134 }
1135
1136 #[test]
1137 fn test_parse_python_imports_detected() {
1138 let mut parser = ArborParser::new().unwrap();
1139
1140 let source = r#"
1141import os
1142from pathlib import Path
1143
1144def read_file(path):
1145 with open(path) as f:
1146 return f.read()
1147"#;
1148
1149 let result = parser.parse_source(source, "utils.py", "py").unwrap();
1150 assert!(result.symbols.iter().any(|s| s.name == "read_file"));
1151 assert!(result
1152 .relations
1153 .iter()
1154 .any(|r| r.kind == RelationType::Imports));
1155 }
1156}