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 tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SymbolRelation {
25 pub from_id: String,
27 pub to_name: String,
29 pub kind: RelationType,
31 pub line: u32,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum RelationType {
38 Calls,
40 Imports,
42 Extends,
44 Implements,
46}
47
48#[derive(Debug)]
50pub struct ParseResult {
51 pub symbols: Vec<CodeNode>,
53 pub relations: Vec<SymbolRelation>,
55 pub file_path: String,
57}
58
59pub struct ArborParser {
68 parser: Parser,
70 queries: HashMap<String, CompiledQueries>,
72}
73
74struct CompiledQueries {
76 symbols: Query,
78 imports: Query,
80 calls: Query,
82 language: Language,
84}
85
86impl Default for ArborParser {
87 fn default() -> Self {
88 Self::new().expect("Failed to initialize ArborParser")
89 }
90}
91
92impl ArborParser {
93 pub fn new() -> Result<Self> {
97 let parser = Parser::new();
98 let mut queries = HashMap::new();
99
100 for ext in &["ts", "tsx", "js", "jsx"] {
102 let compiled = Self::compile_typescript_queries()?;
104 queries.insert(ext.to_string(), compiled);
105 }
106
107 let rs_queries = Self::compile_rust_queries()?;
109 queries.insert("rs".to_string(), rs_queries);
110
111 let py_queries = Self::compile_python_queries()?;
113 queries.insert("py".to_string(), py_queries);
114
115 let go_queries = Self::compile_go_queries()?;
118 queries.insert("go".to_string(), go_queries);
119
120 let java_queries = Self::compile_java_queries()?;
122 queries.insert("java".to_string(), java_queries);
123
124 for ext in &["c", "h"] {
126 queries.insert(ext.to_string(), Self::compile_c_queries()?);
127 }
128
129 for ext in &["cpp", "hpp", "cc", "hh", "cxx"] {
131 queries.insert(ext.to_string(), Self::compile_cpp_queries()?);
132 }
133
134 let csharp_queries = Self::compile_csharp_queries()?;
140 queries.insert("cs".to_string(), csharp_queries);
141
142 Ok(Self { parser, queries })
143 }
144
145 pub fn parse_file(&mut self, path: &Path) -> Result<ParseResult> {
157 let source = fs::read_to_string(path).map_err(|e| ParseError::io(path, e))?;
159
160 if source.is_empty() {
161 return Err(ParseError::EmptyFile(path.to_path_buf()));
162 }
163
164 let ext = path
166 .extension()
167 .and_then(|e| e.to_str())
168 .ok_or_else(|| ParseError::UnsupportedLanguage(path.to_path_buf()))?;
169 let ext = ext.to_ascii_lowercase();
170
171 let compiled = match self.queries.get(&ext) {
173 Some(compiled) => compiled,
174 None => {
175 if fallback_parser::is_fallback_supported_extension(&ext) {
176 return Ok(ParseResult {
177 symbols: fallback_parser::parse_fallback_source(
178 &source,
179 &path.to_string_lossy(),
180 &ext,
181 ),
182 relations: Vec::new(),
183 file_path: path.to_string_lossy().to_string(),
184 });
185 }
186 return Err(ParseError::UnsupportedLanguage(path.to_path_buf()));
187 }
188 };
189
190 self.parser
192 .set_language(&compiled.language)
193 .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
194
195 let tree = self
197 .parser
198 .parse(&source, None)
199 .ok_or_else(|| ParseError::ParserError("Tree-sitter returned no tree".into()))?;
200
201 let file_path = path.to_string_lossy().to_string();
202 let file_name = path
203 .file_name()
204 .and_then(|n| n.to_str())
205 .unwrap_or("unknown");
206
207 let symbols = self.extract_symbols(&tree, &source, &file_path, file_name, compiled);
209
210 let relations = self.extract_relations(&tree, &source, &file_path, &symbols, compiled);
212
213 Ok(ParseResult {
214 symbols,
215 relations,
216 file_path,
217 })
218 }
219
220 pub fn parse_source(
222 &mut self,
223 source: &str,
224 file_path: &str,
225 language: &str,
226 ) -> Result<ParseResult> {
227 if source.is_empty() {
228 return Err(ParseError::EmptyFile(file_path.into()));
229 }
230
231 let language = language.to_ascii_lowercase();
233 let compiled = self.queries.get(&language);
234
235 if compiled.is_none() {
236 if fallback_parser::is_fallback_supported_extension(&language) {
237 return Ok(ParseResult {
238 symbols: fallback_parser::parse_fallback_source(source, file_path, &language),
239 relations: Vec::new(),
240 file_path: file_path.to_string(),
241 });
242 }
243 return Err(ParseError::UnsupportedLanguage(file_path.into()));
244 }
245
246 let compiled = compiled.unwrap();
247
248 self.parser
249 .set_language(&compiled.language)
250 .map_err(|e| ParseError::ParserError(format!("Failed to set language: {}", e)))?;
251
252 let tree = self
253 .parser
254 .parse(source, None)
255 .ok_or_else(|| ParseError::ParserError("Tree-sitter returned no tree".into()))?;
256
257 let file_name = Path::new(file_path)
258 .file_name()
259 .and_then(|n| n.to_str())
260 .unwrap_or("unknown");
261
262 let symbols = self.extract_symbols(&tree, source, file_path, file_name, compiled);
263 let relations = self.extract_relations(&tree, source, file_path, &symbols, compiled);
264
265 Ok(ParseResult {
266 symbols,
267 relations,
268 file_path: file_path.to_string(),
269 })
270 }
271
272 fn extract_symbols(
277 &self,
278 tree: &Tree,
279 source: &str,
280 file_path: &str,
281 file_name: &str,
282 compiled: &CompiledQueries,
283 ) -> Vec<CodeNode> {
284 let mut symbols = Vec::new();
285 let mut cursor = QueryCursor::new();
286
287 let matches = cursor.matches(&compiled.symbols, tree.root_node(), source.as_bytes());
288
289 for match_ in matches {
290 let mut name: Option<&str> = None;
292 let mut kind: Option<NodeKind> = None;
293 let mut node = match_.captures.first().map(|c| c.node);
294
295 for capture in match_.captures {
296 let capture_name = compiled.symbols.capture_names()[capture.index as usize];
297 let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
298
299 match capture_name {
300 "name" | "function.name" | "class.name" | "interface.name" | "method.name" => {
301 name = Some(text);
302 }
303 "function" | "function_def" => {
304 kind = Some(NodeKind::Function);
305 node = Some(capture.node);
306 }
307 "class" | "class_def" => {
308 kind = Some(NodeKind::Class);
309 node = Some(capture.node);
310 }
311 "interface" | "interface_def" => {
312 kind = Some(NodeKind::Interface);
313 node = Some(capture.node);
314 }
315 "method" | "method_def" => {
316 kind = Some(NodeKind::Method);
317 node = Some(capture.node);
318 }
319 "struct" | "struct_def" => {
320 kind = Some(NodeKind::Struct);
321 node = Some(capture.node);
322 }
323 "enum" | "enum_def" => {
324 kind = Some(NodeKind::Enum);
325 node = Some(capture.node);
326 }
327 "trait" | "trait_def" => {
328 kind = Some(NodeKind::Interface);
329 node = Some(capture.node);
330 }
331 _ => {}
332 }
333 }
334
335 if let (Some(name), Some(kind), Some(node)) = (name, kind, node) {
336 let qualified_name = format!("{}:{}", file_name, name);
338
339 let signature = source
341 .lines()
342 .nth(node.start_position().row)
343 .map(|s| s.trim().to_string());
344
345 let mut symbol = CodeNode::new(name, &qualified_name, kind, file_path)
346 .with_lines(
347 node.start_position().row as u32 + 1,
348 node.end_position().row as u32 + 1,
349 )
350 .with_column(node.start_position().column as u32)
351 .with_bytes(node.start_byte() as u32, node.end_byte() as u32);
352
353 if let Some(sig) = signature {
354 symbol = symbol.with_signature(sig);
355 }
356
357 symbols.push(symbol);
358 }
359 }
360
361 symbols
362 }
363
364 fn extract_relations(
369 &self,
370 tree: &Tree,
371 source: &str,
372 file_path: &str,
373 symbols: &[CodeNode],
374 compiled: &CompiledQueries,
375 ) -> Vec<SymbolRelation> {
376 let mut relations = Vec::new();
377
378 self.extract_imports(tree, source, file_path, &mut relations, compiled);
380
381 self.extract_calls(tree, source, file_path, symbols, &mut relations, compiled);
383
384 relations
385 }
386
387 fn extract_imports(
388 &self,
389 tree: &Tree,
390 source: &str,
391 file_path: &str,
392 relations: &mut Vec<SymbolRelation>,
393 compiled: &CompiledQueries,
394 ) {
395 let mut cursor = QueryCursor::new();
396 let matches = cursor.matches(&compiled.imports, tree.root_node(), source.as_bytes());
397
398 for match_ in matches {
399 let mut module_name: Option<&str> = None;
400 let mut line: u32 = 0;
401
402 for capture in match_.captures {
403 let capture_name = compiled.imports.capture_names()[capture.index as usize];
404 let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
405
406 match capture_name {
407 "source" | "module" | "import.source" => {
408 module_name = Some(text.trim_matches(|c| c == '"' || c == '\''));
410 line = capture.node.start_position().row as u32 + 1;
411 }
412 _ => {}
413 }
414 }
415
416 if let Some(module) = module_name {
417 let file_id = format!("{}:__file__", file_path);
419 relations.push(SymbolRelation {
420 from_id: file_id,
421 to_name: module.to_string(),
422 kind: RelationType::Imports,
423 line,
424 });
425 }
426 }
427 }
428
429 fn extract_calls(
430 &self,
431 tree: &Tree,
432 source: &str,
433 file_path: &str,
434 symbols: &[CodeNode],
435 relations: &mut Vec<SymbolRelation>,
436 compiled: &CompiledQueries,
437 ) {
438 let mut cursor = QueryCursor::new();
439 let matches = cursor.matches(&compiled.calls, tree.root_node(), source.as_bytes());
440
441 for match_ in matches {
442 let mut callee_name: Option<&str> = None;
443 let mut call_line: u32 = 0;
444
445 for capture in match_.captures {
446 let capture_name = compiled.calls.capture_names()[capture.index as usize];
447 let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
448
449 match capture_name {
450 "callee" | "function" | "call.function" => {
451 if let Some(dot_pos) = text.rfind('.') {
453 callee_name = Some(&text[dot_pos + 1..]);
454 } else {
455 callee_name = Some(text);
456 }
457 call_line = capture.node.start_position().row as u32 + 1;
458 }
459 _ => {}
460 }
461 }
462
463 if let Some(callee) = callee_name {
464 let caller_id = self
466 .find_enclosing_symbol(call_line, symbols)
467 .map(|s| s.id.clone())
468 .unwrap_or_else(|| format!("{}:__file__", file_path));
469
470 relations.push(SymbolRelation {
471 from_id: caller_id,
472 to_name: callee.to_string(),
473 kind: RelationType::Calls,
474 line: call_line,
475 });
476 }
477 }
478 }
479
480 fn find_enclosing_symbol<'a>(
481 &self,
482 line: u32,
483 symbols: &'a [CodeNode],
484 ) -> Option<&'a CodeNode> {
485 symbols
486 .iter()
487 .filter(|s| s.line_start <= line && s.line_end >= line)
488 .min_by_key(|s| s.line_end - s.line_start) }
490
491 fn compile_typescript_queries() -> Result<CompiledQueries> {
496 let language = tree_sitter_typescript::language_typescript();
497
498 let symbols_query = r#"
500 (function_declaration name: (identifier) @name) @function_def
501 (class_declaration name: (type_identifier) @name) @class_def
502 (method_definition name: (property_identifier) @name) @method_def
503 (interface_declaration name: (type_identifier) @name) @interface_def
504 (type_alias_declaration name: (type_identifier) @name) @interface_def
505 "#;
506
507 let imports_query = r#"
509 (import_statement
510 source: (string) @source)
511 "#;
512
513 let calls_query = r#"
515 (call_expression
516 function: (identifier) @callee)
517
518 (call_expression
519 function: (member_expression
520 property: (property_identifier) @callee))
521 "#;
522
523 let symbols = Query::new(&language, symbols_query)
524 .map_err(|e| ParseError::QueryError(e.to_string()))?;
525 let imports = Query::new(&language, imports_query)
526 .map_err(|e| ParseError::QueryError(e.to_string()))?;
527 let calls = Query::new(&language, calls_query)
528 .map_err(|e| ParseError::QueryError(e.to_string()))?;
529
530 Ok(CompiledQueries {
531 symbols,
532 imports,
533 calls,
534 language,
535 })
536 }
537
538 fn compile_rust_queries() -> Result<CompiledQueries> {
539 let language = tree_sitter_rust::language();
540
541 let symbols_query = r#"
543 (function_item name: (identifier) @name) @function_def
544 (struct_item name: (type_identifier) @name) @struct_def
545 (enum_item name: (type_identifier) @name) @enum_def
546 (trait_item name: (type_identifier) @name) @trait_def
547 "#;
548
549 let imports_query = r#"
551 (use_declaration) @source
552 "#;
553
554 let calls_query = r#"
556 (call_expression function: (identifier) @callee)
557 (call_expression function: (field_expression field: (field_identifier) @callee))
558 "#;
559
560 let symbols = Query::new(&language, symbols_query)
561 .map_err(|e| ParseError::QueryError(e.to_string()))?;
562 let imports = Query::new(&language, imports_query)
563 .map_err(|e| ParseError::QueryError(e.to_string()))?;
564 let calls = Query::new(&language, calls_query)
565 .map_err(|e| ParseError::QueryError(e.to_string()))?;
566
567 Ok(CompiledQueries {
568 symbols,
569 imports,
570 calls,
571 language,
572 })
573 }
574
575 fn compile_python_queries() -> Result<CompiledQueries> {
576 let language = tree_sitter_python::language();
577
578 let symbols_query = r#"
580 (function_definition name: (identifier) @name) @function_def
581 (class_definition name: (identifier) @name) @class_def
582 "#;
583
584 let imports_query = r#"
586 (import_statement) @source
587 (import_from_statement) @source
588 "#;
589
590 let calls_query = r#"
592 (call function: (identifier) @callee)
593 (call function: (attribute attribute: (identifier) @callee))
594 "#;
595
596 let symbols = Query::new(&language, symbols_query)
597 .map_err(|e| ParseError::QueryError(e.to_string()))?;
598 let imports = Query::new(&language, imports_query)
599 .map_err(|e| ParseError::QueryError(e.to_string()))?;
600 let calls = Query::new(&language, calls_query)
601 .map_err(|e| ParseError::QueryError(e.to_string()))?;
602
603 Ok(CompiledQueries {
604 symbols,
605 imports,
606 calls,
607 language,
608 })
609 }
610
611 fn compile_go_queries() -> Result<CompiledQueries> {
612 let language = tree_sitter_go::language();
613
614 let symbols_query = r#"
615 (function_declaration name: (identifier) @name) @function_def
616 (method_declaration name: (field_identifier) @name) @method_def
617 (type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @struct_def
618 (type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @interface_def
619 "#;
620
621 let imports_query = r#"
622 (import_spec path: (interpreted_string_literal) @source)
623 "#;
624
625 let calls_query = r#"
626 (call_expression function: (identifier) @callee)
627 (call_expression function: (selector_expression field: (field_identifier) @callee))
628 "#;
629
630 let symbols = Query::new(&language, symbols_query)
631 .map_err(|e| ParseError::QueryError(e.to_string()))?;
632 let imports = Query::new(&language, imports_query)
633 .map_err(|e| ParseError::QueryError(e.to_string()))?;
634 let calls = Query::new(&language, calls_query)
635 .map_err(|e| ParseError::QueryError(e.to_string()))?;
636
637 Ok(CompiledQueries {
638 symbols,
639 imports,
640 calls,
641 language,
642 })
643 }
644
645 fn compile_java_queries() -> Result<CompiledQueries> {
646 let language = tree_sitter_java::language();
647
648 let symbols_query = r#"
649 (method_declaration name: (identifier) @name) @method_def
650 (class_declaration name: (identifier) @name) @class_def
651 (interface_declaration name: (identifier) @name) @interface_def
652 (constructor_declaration name: (identifier) @name) @function_def
653 "#;
654
655 let imports_query = r#"
656 (import_declaration) @source
657 "#;
658
659 let calls_query = r#"
660 (method_invocation name: (identifier) @callee)
661 "#;
662
663 let symbols = Query::new(&language, symbols_query)
664 .map_err(|e| ParseError::QueryError(e.to_string()))?;
665 let imports = Query::new(&language, imports_query)
666 .map_err(|e| ParseError::QueryError(e.to_string()))?;
667 let calls = Query::new(&language, calls_query)
668 .map_err(|e| ParseError::QueryError(e.to_string()))?;
669
670 Ok(CompiledQueries {
671 symbols,
672 imports,
673 calls,
674 language,
675 })
676 }
677
678 fn compile_c_queries() -> Result<CompiledQueries> {
679 let language = tree_sitter_c::language();
680
681 let symbols_query = r#"
682 (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
683 (struct_specifier name: (type_identifier) @name) @struct_def
684 (enum_specifier name: (type_identifier) @name) @enum_def
685 "#;
686
687 let imports_query = r#"
688 (preproc_include path: (string_literal) @source)
689 (preproc_include path: (system_lib_string) @source)
690 "#;
691
692 let calls_query = r#"
693 (call_expression function: (identifier) @callee)
694 "#;
695
696 let symbols = Query::new(&language, symbols_query)
697 .map_err(|e| ParseError::QueryError(e.to_string()))?;
698 let imports = Query::new(&language, imports_query)
699 .map_err(|e| ParseError::QueryError(e.to_string()))?;
700 let calls = Query::new(&language, calls_query)
701 .map_err(|e| ParseError::QueryError(e.to_string()))?;
702
703 Ok(CompiledQueries {
704 symbols,
705 imports,
706 calls,
707 language,
708 })
709 }
710
711 fn compile_cpp_queries() -> Result<CompiledQueries> {
712 let language = tree_sitter_cpp::language();
713
714 let symbols_query = r#"
715 (function_definition declarator: (function_declarator declarator: (identifier) @name)) @function_def
716 (function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @method_def
717 (class_specifier name: (type_identifier) @name) @class_def
718 (struct_specifier name: (type_identifier) @name) @struct_def
719 "#;
720
721 let imports_query = r#"
722 (preproc_include path: (string_literal) @source)
723 (preproc_include path: (system_lib_string) @source)
724 "#;
725
726 let calls_query = r#"
727 (call_expression function: (identifier) @callee)
728 (call_expression function: (field_expression field: (field_identifier) @callee))
729 "#;
730
731 let symbols = Query::new(&language, symbols_query)
732 .map_err(|e| ParseError::QueryError(e.to_string()))?;
733 let imports = Query::new(&language, imports_query)
734 .map_err(|e| ParseError::QueryError(e.to_string()))?;
735 let calls = Query::new(&language, calls_query)
736 .map_err(|e| ParseError::QueryError(e.to_string()))?;
737
738 Ok(CompiledQueries {
739 symbols,
740 imports,
741 calls,
742 language,
743 })
744 }
745
746 fn compile_csharp_queries() -> Result<CompiledQueries> {
747 let language = tree_sitter_c_sharp::language();
748
749 let symbols_query = r#"
750 (method_declaration name: (identifier) @name) @method_def
751 (class_declaration name: (identifier) @name) @class_def
752 (interface_declaration name: (identifier) @name) @interface_def
753 (struct_declaration name: (identifier) @name) @struct_def
754 (constructor_declaration name: (identifier) @name) @function_def
755 (property_declaration name: (identifier) @name) @method_def
756 "#;
757
758 let imports_query = r#"
759 (using_directive (identifier) @source)
760 (using_directive (qualified_name) @source)
761 "#;
762
763 let calls_query = r#"
764 (invocation_expression function: (identifier) @callee)
765 (invocation_expression function: (member_access_expression name: (identifier) @callee))
766 "#;
767
768 let symbols = Query::new(&language, symbols_query)
769 .map_err(|e| ParseError::QueryError(e.to_string()))?;
770 let imports = Query::new(&language, imports_query)
771 .map_err(|e| ParseError::QueryError(e.to_string()))?;
772 let calls = Query::new(&language, calls_query)
773 .map_err(|e| ParseError::QueryError(e.to_string()))?;
774
775 Ok(CompiledQueries {
776 symbols,
777 imports,
778 calls,
779 language,
780 })
781 }
782
783 }
787
788#[cfg(test)]
793mod tests {
794 use super::*;
795
796 #[test]
797 fn test_parser_initialization() {
798 match ArborParser::new() {
800 Ok(_) => println!("Parser initialized successfully!"),
801 Err(e) => panic!("Parser failed to initialize: {}", e),
802 }
803 }
804
805 #[test]
806 fn test_parse_typescript_symbols() {
807 let mut parser = ArborParser::new().unwrap();
808
809 let source = r#"
810 function greet(name: string): string {
811 return `Hello, ${name}!`;
812 }
813
814 export class UserService {
815 validate(user: User): boolean {
816 return true;
817 }
818 }
819
820 interface User {
821 name: string;
822 email: string;
823 }
824 "#;
825
826 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
827
828 assert!(result.symbols.iter().any(|s| s.name == "greet"));
829 assert!(result.symbols.iter().any(|s| s.name == "UserService"));
830 assert!(result.symbols.iter().any(|s| s.name == "validate"));
831 assert!(result.symbols.iter().any(|s| s.name == "User"));
832 }
833
834 #[test]
835 fn test_parse_typescript_imports() {
836 let mut parser = ArborParser::new().unwrap();
837
838 let source = r#"
839 import { useState } from 'react';
840 import lodash from 'lodash';
841
842 function Component() {
843 const [count, setCount] = useState(0);
844 }
845 "#;
846
847 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
848
849 let imports: Vec<_> = result
850 .relations
851 .iter()
852 .filter(|r| r.kind == RelationType::Imports)
853 .collect();
854
855 assert!(imports.iter().any(|i| i.to_name.contains("react")));
856 assert!(imports.iter().any(|i| i.to_name.contains("lodash")));
857 }
858
859 #[test]
860 fn test_parse_typescript_calls() {
861 let mut parser = ArborParser::new().unwrap();
862
863 let source = r#"
864 function outer() {
865 inner();
866 helper.process();
867 }
868
869 function inner() {
870 console.log("Hello");
871 }
872 "#;
873
874 let result = parser.parse_source(source, "test.ts", "ts").unwrap();
875
876 let calls: Vec<_> = result
877 .relations
878 .iter()
879 .filter(|r| r.kind == RelationType::Calls)
880 .collect();
881
882 assert!(calls.iter().any(|c| c.to_name == "inner"));
883 assert!(calls.iter().any(|c| c.to_name == "process"));
884 assert!(calls.iter().any(|c| c.to_name == "log"));
885 }
886
887 #[test]
888 fn test_parse_rust_symbols() {
889 let mut parser = ArborParser::new().unwrap();
890
891 let source = r#"
892 fn main() {
893 println!("Hello!");
894 }
895
896 pub struct User {
897 name: String,
898 }
899
900 impl User {
901 fn new(name: &str) -> Self {
902 Self { name: name.to_string() }
903 }
904 }
905
906 enum Status {
907 Active,
908 Inactive,
909 }
910 "#;
911
912 let result = parser.parse_source(source, "test.rs", "rs").unwrap();
913
914 assert!(result.symbols.iter().any(|s| s.name == "main"));
915 assert!(result.symbols.iter().any(|s| s.name == "User"));
916 assert!(result.symbols.iter().any(|s| s.name == "new"));
917 assert!(result.symbols.iter().any(|s| s.name == "Status"));
918 }
919
920 #[test]
921 fn test_parse_python_symbols() {
922 let mut parser = ArborParser::new().unwrap();
923
924 let source = r#"
925def greet(name):
926 return f"Hello, {name}!"
927
928class UserService:
929 def validate(self, user):
930 return True
931 "#;
932
933 let result = parser.parse_source(source, "test.py", "py").unwrap();
934
935 assert!(result.symbols.iter().any(|s| s.name == "greet"));
936 assert!(result.symbols.iter().any(|s| s.name == "UserService"));
937 assert!(result.symbols.iter().any(|s| s.name == "validate"));
938 }
939
940 #[test]
941 fn test_parse_fallback_kotlin_symbols() {
942 let mut parser = ArborParser::new().unwrap();
943
944 let source = r#"
945 class BillingService
946 fun computeInvoiceTotal(amount: Double): Double = amount
947 "#;
948
949 let result = parser.parse_source(source, "billing.kt", "kt").unwrap();
950
951 assert!(result.symbols.iter().any(|s| s.name == "BillingService"));
952 assert!(result
953 .symbols
954 .iter()
955 .any(|s| s.name == "computeInvoiceTotal"));
956 assert!(result.relations.is_empty());
957 }
958}