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(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 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 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 let qualified_name = format!("{}:{}", file_name, name);
372
373 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 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 self.extract_imports(tree, source, file_path, &mut cursor, &mut relations, compiled);
412
413 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 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 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 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 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) }
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 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#[cfg(test)]
798mod tests {
799 use super::*;
800
801 #[test]
802 fn test_parser_initialization() {
803 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}