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