1use crate::languages::get_language_info;
13use crate::types::{ImplTraitInfo, SemanticAnalysis};
14use std::cell::RefCell;
15use std::collections::HashMap;
16use std::path::Path;
17use std::sync::LazyLock;
18use thiserror::Error;
19use tracing::instrument;
20use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator};
21
22use crate::parser_elements::{
24 extract_calls, extract_def_use, extract_elements, extract_impl_methods,
25 extract_impl_traits_from_tree, extract_imports, extract_references,
26};
27
28#[derive(Debug, Error)]
29#[non_exhaustive]
30pub enum ParserError {
31 #[error("Unsupported language: {0}")]
32 UnsupportedLanguage(String),
33 #[error("Failed to parse file: {0}")]
34 ParseError(String),
35 #[error("Invalid UTF-8 in file")]
36 InvalidUtf8,
37 #[error("Query error: {0}")]
38 QueryError(String),
39 #[error("Parse timeout exceeded: {0} microseconds")]
40 Timeout(u64),
41}
42
43#[derive(Clone, Copy)]
46pub(crate) struct TimeoutConfig {
47 pub deadline: Option<std::time::Instant>,
49 pub micros: u64,
51}
52
53impl TimeoutConfig {
54 fn new(timeout_micros: Option<u64>) -> Self {
55 let deadline = timeout_micros
56 .map(|us| std::time::Instant::now() + std::time::Duration::from_micros(us));
57 Self {
58 deadline,
59 micros: timeout_micros.unwrap_or(0),
60 }
61 }
62
63 pub(crate) fn is_exceeded(self) -> bool {
65 self.deadline
66 .is_some_and(|d| std::time::Instant::now() >= d)
67 }
68}
69
70pub(crate) struct CompiledQueries {
73 pub element: Query,
74 pub call: Query,
75 pub import: Option<Query>,
76 pub impl_block: Option<Query>,
77 pub reference: Option<Query>,
78 pub impl_trait: Option<Query>,
79 pub defuse: Option<Query>,
80}
81
82fn build_compiled_queries(
88 lang_info: &crate::languages::LanguageInfo,
89) -> Result<CompiledQueries, ParserError> {
90 let element = Query::new(&lang_info.language, lang_info.element_query).map_err(|e| {
91 ParserError::QueryError(format!(
92 "Failed to compile element query for {}: {}",
93 lang_info.name, e
94 ))
95 })?;
96
97 let call = Query::new(&lang_info.language, lang_info.call_query).map_err(|e| {
98 ParserError::QueryError(format!(
99 "Failed to compile call query for {}: {}",
100 lang_info.name, e
101 ))
102 })?;
103
104 let import = if let Some(import_query_str) = lang_info.import_query {
105 Some(
106 Query::new(&lang_info.language, import_query_str).map_err(|e| {
107 ParserError::QueryError(format!(
108 "Failed to compile import query for {}: {}",
109 lang_info.name, e
110 ))
111 })?,
112 )
113 } else {
114 None
115 };
116
117 let impl_block = if let Some(impl_query_str) = lang_info.impl_query {
118 Some(
119 Query::new(&lang_info.language, impl_query_str).map_err(|e| {
120 ParserError::QueryError(format!(
121 "Failed to compile impl query for {}: {}",
122 lang_info.name, e
123 ))
124 })?,
125 )
126 } else {
127 None
128 };
129
130 let reference = if let Some(reference_query_str) = lang_info.reference_query {
131 Some(
132 Query::new(&lang_info.language, reference_query_str).map_err(|e| {
133 ParserError::QueryError(format!(
134 "Failed to compile reference query for {}: {}",
135 lang_info.name, e
136 ))
137 })?,
138 )
139 } else {
140 None
141 };
142
143 let impl_trait = if let Some(impl_trait_query_str) = lang_info.impl_trait_query {
144 Some(
145 Query::new(&lang_info.language, impl_trait_query_str).map_err(|e| {
146 ParserError::QueryError(format!(
147 "Failed to compile impl_trait query for {}: {}",
148 lang_info.name, e
149 ))
150 })?,
151 )
152 } else {
153 None
154 };
155
156 let defuse = if let Some(defuse_query_str) = lang_info.defuse_query {
157 Some(
158 Query::new(&lang_info.language, defuse_query_str).map_err(|e| {
159 ParserError::QueryError(format!(
160 "Failed to compile defuse query for {}: {}",
161 lang_info.name, e
162 ))
163 })?,
164 )
165 } else {
166 None
167 };
168
169 Ok(CompiledQueries {
170 element,
171 call,
172 import,
173 impl_block,
174 reference,
175 impl_trait,
176 defuse,
177 })
178}
179
180#[cfg_attr(coverage_nightly, coverage(off))]
185fn init_query_cache() -> HashMap<&'static str, CompiledQueries> {
186 let mut cache = HashMap::new();
187
188 for lang_name in crate::lang::supported_languages() {
189 if let Some(lang_info) = get_language_info(lang_name) {
190 match build_compiled_queries(&lang_info) {
191 Ok(compiled) => {
192 cache.insert(*lang_name, compiled);
193 }
194 Err(e) => {
195 tracing::error!(
196 "Failed to compile queries for language {}: {}",
197 lang_name,
198 e
199 );
200 }
201 }
202 }
203 }
204
205 cache
206}
207
208static QUERY_CACHE: LazyLock<HashMap<&'static str, CompiledQueries>> =
210 LazyLock::new(init_query_cache);
211
212fn get_compiled_queries(language: &str) -> Result<&'static CompiledQueries, ParserError> {
214 QUERY_CACHE
215 .get(language)
216 .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))
217}
218
219thread_local! {
220 pub(crate) static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
221 pub(crate) static QUERY_CURSOR: RefCell<QueryCursor> = RefCell::new(QueryCursor::new());
222}
223
224pub struct ElementExtractor;
226
227impl ElementExtractor {
228 #[instrument(skip_all, fields(language))]
236 pub fn extract_with_depth(source: &str, language: &str) -> Result<(usize, usize), ParserError> {
237 let lang_info = get_language_info(language)
238 .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
239
240 let tree = PARSER.with(|p| {
241 let mut parser = p.borrow_mut();
242 parser
243 .set_language(&lang_info.language)
244 .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
245 parser
246 .parse(source, None)
247 .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
248 })?;
249
250 let compiled = get_compiled_queries(language)?;
251
252 let (function_count, class_count) = QUERY_CURSOR.with(|c| {
253 let mut cursor = c.borrow_mut();
254 cursor.set_max_start_depth(None);
255 let mut function_count = 0;
256 let mut class_count = 0;
257
258 let mut matches =
259 cursor.matches(&compiled.element, tree.root_node(), source.as_bytes());
260 while let Some(mat) = matches.next() {
261 for capture in mat.captures {
262 let capture_name = compiled.element.capture_names()[capture.index as usize];
263 match capture_name {
264 "function" => function_count += 1,
265 "class" => class_count += 1,
266 _ => {}
267 }
268 }
269 }
270 (function_count, class_count)
271 });
272
273 tracing::debug!(language = %language, functions = function_count, classes = class_count, "parse complete");
274
275 Ok((function_count, class_count))
276 }
277}
278
279pub struct SemanticExtractor;
281
282impl SemanticExtractor {
283 #[instrument(skip_all, fields(language))]
310 pub fn extract(
311 source: &str,
312 language: &str,
313 ast_recursion_limit: Option<usize>,
314 timeout_micros: Option<u64>,
315 ) -> Result<SemanticAnalysis, ParserError> {
316 let tc = TimeoutConfig::new(timeout_micros);
317
318 if tc.is_exceeded() {
320 return Err(ParserError::Timeout(tc.micros));
321 }
322
323 let lang_info = get_language_info(language)
324 .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
325
326 let tree = PARSER.with(|p| {
327 let mut parser = p.borrow_mut();
328 parser
329 .set_language(&lang_info.language)
330 .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
331 parser
332 .parse(source, None)
333 .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
334 })?;
335
336 if tc.is_exceeded() {
338 return Err(ParserError::Timeout(tc.micros));
339 }
340
341 let compiled = get_compiled_queries(language)?;
342 let root = tree.root_node();
343
344 let max_depth: Option<u32> = ast_recursion_limit
346 .filter(|&limit| limit > 0)
347 .and_then(|limit| u32::try_from(limit).ok());
348
349 let mut functions = Vec::new();
350 let mut classes = Vec::new();
351 let mut imports = Vec::new();
352 let mut references = Vec::new();
353 let mut calls = Vec::new();
354 let mut call_frequency = HashMap::new();
355
356 extract_elements(
358 source,
359 compiled,
360 root,
361 max_depth,
362 &mut functions,
363 &mut classes,
364 tc,
365 &lang_info,
366 )?;
367
368 if tc.is_exceeded() {
370 return Err(ParserError::Timeout(tc.micros));
371 }
372
373 extract_calls(
374 source,
375 compiled,
376 root,
377 max_depth,
378 &mut calls,
379 &mut call_frequency,
380 tc,
381 )?;
382 extract_imports(source, compiled, root, max_depth, &mut imports, tc)?;
383 extract_impl_methods(source, compiled, root, max_depth, &mut classes, tc)?;
384 extract_references(source, compiled, root, max_depth, &mut references, tc)?;
385
386 let impl_traits = if language == "rust" {
388 extract_impl_traits_from_tree(source, compiled, root, tc)?
389 } else {
390 vec![]
391 };
392
393 tracing::debug!(language = %language, functions = functions.len(), classes = classes.len(), imports = imports.len(), references = references.len(), calls = calls.len(), impl_traits = impl_traits.len(), "extraction complete");
394
395 Ok(SemanticAnalysis {
396 functions,
397 classes,
398 imports,
399 references,
400 call_frequency,
401 calls,
402 impl_traits,
403 def_use_sites: Vec::new(),
404 })
405 }
406
407 #[instrument(skip_all, fields(language))]
430 pub fn extract_module_info(
431 source: &str,
432 language: &str,
433 timeout_micros: Option<u64>,
434 ) -> Result<crate::types::ModuleInfo, ParserError> {
435 let tc = TimeoutConfig::new(timeout_micros);
436
437 if tc.is_exceeded() {
439 return Err(ParserError::Timeout(tc.micros));
440 }
441
442 let lang_info = get_language_info(language)
443 .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
444
445 let tree = PARSER.with(|p| {
446 let mut parser = p.borrow_mut();
447 parser
448 .set_language(&lang_info.language)
449 .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
450 parser
451 .parse(source, None)
452 .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
453 })?;
454
455 if tc.is_exceeded() {
457 return Err(ParserError::Timeout(tc.micros));
458 }
459
460 let compiled = get_compiled_queries(language)?;
461 let root = tree.root_node();
462
463 let mut functions = Vec::new();
464 let mut classes = Vec::new();
465 let mut imports = Vec::new();
466
467 extract_elements(
469 source,
470 compiled,
471 root,
472 None,
473 &mut functions,
474 &mut classes,
475 tc,
476 &lang_info,
477 )?;
478
479 if tc.is_exceeded() {
481 return Err(ParserError::Timeout(tc.micros));
482 }
483
484 extract_imports(source, compiled, root, None, &mut imports, tc)?;
486
487 if tc.is_exceeded() {
489 return Err(ParserError::Timeout(tc.micros));
490 }
491
492 let module_functions = functions
494 .into_iter()
495 .map(|f| crate::types::ModuleFunctionInfo {
496 name: f.name,
497 line: f.line,
498 })
499 .collect();
500
501 let module_imports = imports
502 .into_iter()
503 .map(|i| crate::types::ModuleImportInfo {
504 module: i.module,
505 items: i.items,
506 })
507 .collect();
508
509 let line_count = source.lines().count();
510
511 Ok(crate::types::ModuleInfo::new(
512 String::new(), line_count,
514 language.to_string(),
515 module_functions,
516 module_imports,
517 ))
518 }
519
520 pub(crate) fn extract_def_use_for_file(
523 source: &str,
524 language: &str,
525 symbol: &str,
526 file_path: &str,
527 ast_recursion_limit: Option<usize>,
528 ) -> Vec<crate::types::DefUseSite> {
529 let Some(lang_info) = get_language_info(language) else {
530 return vec![];
531 };
532 let Ok(compiled) = get_compiled_queries(language) else {
533 return vec![];
534 };
535 if compiled.defuse.is_none() {
536 return vec![];
537 }
538
539 let tree = match PARSER.with(|p| {
540 let mut parser = p.borrow_mut();
541 if parser.set_language(&lang_info.language).is_err() {
542 return None;
543 }
544 parser.parse(source, None)
545 }) {
546 Some(t) => t,
547 None => return vec![],
548 };
549
550 let root = tree.root_node();
551
552 let max_depth: Option<u32> = ast_recursion_limit
555 .filter(|&limit| limit > 0)
556 .and_then(|limit| u32::try_from(limit).ok());
557
558 extract_def_use(source, compiled, root, symbol, file_path, max_depth)
559 }
560}
561
562#[must_use]
567pub fn extract_impl_traits(source: &str, path: &Path) -> Vec<ImplTraitInfo> {
568 let Some(lang_info) = get_language_info("rust") else {
569 return vec![];
570 };
571
572 let Ok(compiled) = get_compiled_queries("rust") else {
573 return vec![];
574 };
575
576 let Some(query) = &compiled.impl_trait else {
577 return vec![];
578 };
579
580 let Some(tree) = PARSER.with(|p| {
581 let mut parser = p.borrow_mut();
582 let _ = parser.set_language(&lang_info.language);
583 parser.parse(source, None)
584 }) else {
585 return vec![];
586 };
587
588 let root = tree.root_node();
589 let mut results = Vec::new();
590
591 QUERY_CURSOR.with(|c| {
592 let mut cursor = c.borrow_mut();
593 cursor.set_max_start_depth(None);
594 let mut matches = cursor.matches(query, root, source.as_bytes());
595
596 while let Some(mat) = matches.next() {
597 let mut trait_name = String::new();
598 let mut impl_type = String::new();
599 let mut line = 0usize;
600
601 for capture in mat.captures {
602 let capture_name = query.capture_names()[capture.index as usize];
603 let node = capture.node;
604 let text = source[node.start_byte()..node.end_byte()].to_string();
605 match capture_name {
606 "trait_name" => {
607 trait_name = text;
608 line = node.start_position().row + 1;
609 }
610 "impl_type" => {
611 impl_type = text;
612 }
613 _ => {}
614 }
615 }
616
617 if !trait_name.is_empty() && !impl_type.is_empty() {
618 results.push(ImplTraitInfo {
619 trait_name,
620 impl_type,
621 path: path.to_path_buf(),
622 line,
623 });
624 }
625 }
626 });
627
628 results
629}
630
631pub(crate) fn execute_query_impl(
635 language: &str,
636 source: &str,
637 query_str: &str,
638) -> Result<Vec<crate::QueryCapture>, ParserError> {
639 let ts_language = crate::languages::get_ts_language(language)
641 .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
642
643 let mut parser = Parser::new();
644 parser
645 .set_language(&ts_language)
646 .map_err(|e| ParserError::QueryError(e.to_string()))?;
647
648 let tree = parser
649 .parse(source.as_bytes(), None)
650 .ok_or_else(|| ParserError::QueryError("failed to parse source".to_string()))?;
651
652 let query =
653 Query::new(&ts_language, query_str).map_err(|e| ParserError::QueryError(e.to_string()))?;
654
655 let source_bytes = source.as_bytes();
656
657 let mut captures = Vec::new();
658 QUERY_CURSOR.with(|c| {
659 let mut cursor = c.borrow_mut();
660 cursor.set_max_start_depth(None);
661 let mut matches = cursor.matches(&query, tree.root_node(), source_bytes);
662 while let Some(m) = matches.next() {
663 for cap in m.captures {
664 let node = cap.node;
665 let capture_name = query.capture_names()[cap.index as usize].to_string();
666 let text = node.utf8_text(source_bytes).unwrap_or("").to_string();
667 captures.push(crate::QueryCapture {
668 capture_name,
669 text,
670 start_line: node.start_position().row,
671 end_line: node.end_position().row,
672 start_byte: node.start_byte(),
673 end_byte: node.end_byte(),
674 });
675 }
676 }
677 });
678 Ok(captures)
679}
680
681#[cfg(test)]
682mod tests_rust {
684 use super::*;
685 use crate::types::CallInfo;
686
687 #[test]
688 fn test_ast_recursion_limit_zero_is_unlimited() {
689 let source = r#"fn hello() -> u32 { 42 }"#;
691 let result = SemanticExtractor::extract(source, "rust", Some(0), None);
693 assert!(result.is_ok(), "extract with limit=0 should succeed");
695 let analysis = result.unwrap();
696 assert_eq!(
697 analysis.functions.len(),
698 1,
699 "should find exactly one function"
700 );
701 }
702
703 #[test]
704 fn test_rust_use_as_imports() {
705 let source = "use std::io as stdio;\n";
707 let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
709 let stdio_import = result
711 .imports
712 .iter()
713 .find(|imp| imp.items.iter().any(|i| i == "stdio"));
714 assert!(
715 stdio_import.is_some(),
716 "expected import with alias 'stdio' in {:?}",
717 result.imports
718 );
719 }
720
721 #[test]
722 fn test_rust_use_as_clause_plain_identifier() {
723 let source = "use io as stdio;\n";
725 let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
727 let alias_import = result
729 .imports
730 .iter()
731 .find(|imp| imp.items.iter().any(|i| i == "stdio"));
732 assert!(
733 alias_import.is_some(),
734 "expected import with alias 'stdio' in {:?}",
735 result.imports
736 );
737 }
738
739 #[test]
740 fn test_rust_scoped_use_with_prefix() {
741 let source = "use std::{io, fs};\n";
743 let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
745 let has_io = result
747 .imports
748 .iter()
749 .any(|imp| imp.items.iter().any(|i| i == "io"));
750 let has_fs = result
751 .imports
752 .iter()
753 .any(|imp| imp.items.iter().any(|i| i == "fs"));
754 assert!(has_io, "expected import 'io' in {:?}", result.imports);
755 assert!(has_fs, "expected import 'fs' in {:?}", result.imports);
756 }
757
758 #[test]
759 fn test_rust_scoped_use_imports() {
760 let source = "use std::{io, fs};\n";
762 let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
764 assert!(
766 !result.imports.is_empty(),
767 "expected imports in {:?}",
768 result.imports
769 );
770 }
771
772 #[test]
773 fn test_rust_wildcard_imports() {
774 let source = "use std::*;\n";
776 let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
778 let wildcard = result
780 .imports
781 .iter()
782 .find(|imp| imp.items.iter().any(|i| i == "*"));
783 assert!(
784 wildcard.is_some(),
785 "expected wildcard import in {:?}",
786 result.imports
787 );
788 }
789
790 #[test]
791 fn test_extract_impl_traits_standalone() {
792 let source = r#"
794 trait MyTrait {
795 fn method(&self);
796 }
797 impl MyTrait for MyType {
798 fn method(&self) {}
799 }
800 "#;
801 let result = extract_impl_traits(source, Path::new("test.rs"));
803 assert!(
805 !result.is_empty(),
806 "expected impl trait in result, got {:?}",
807 result
808 );
809 }
810
811 #[test]
812 fn test_ast_recursion_limit_overflow() {
813 let source = r#"fn hello() -> u32 { 42 }"#;
815 let result = SemanticExtractor::extract(source, "rust", Some(usize::MAX), None);
817 assert!(
819 result.is_ok(),
820 "extract with limit=usize::MAX should succeed"
821 );
822 }
823
824 #[test]
825 fn test_ast_recursion_limit_some() {
826 let source = r#"fn hello() -> u32 { 42 }"#;
828 let result = SemanticExtractor::extract(source, "rust", Some(10), None);
830 assert!(result.is_ok(), "extract with limit=10 should succeed");
832 let analysis = result.unwrap();
833 assert_eq!(
834 analysis.functions.len(),
835 1,
836 "should find exactly one function"
837 );
838 }
839
840 #[test]
841 fn test_extract_def_use_for_file_finds_write_and_read() {
842 let source = r#"
844 fn test() {
845 let mut x = 5;
846 x = 10;
847 let y = x;
848 }
849 "#;
850 let result =
852 SemanticExtractor::extract_def_use_for_file(source, "rust", "x", "test.rs", None);
853 let has_write = result
855 .iter()
856 .any(|s| s.kind == crate::types::DefUseKind::Write);
857 let has_read = result
858 .iter()
859 .any(|s| s.kind == crate::types::DefUseKind::Read);
860 assert!(has_write, "expected write site for 'x'");
861 assert!(has_read, "expected read site for 'x'");
862 }
863
864 #[test]
865 fn test_extract_def_use_for_file_no_match_returns_empty() {
866 let source = r#"
868 fn test() {
869 let x = 5;
870 }
871 "#;
872 let result = SemanticExtractor::extract_def_use_for_file(
874 source,
875 "rust",
876 "nonexistent",
877 "test.rs",
878 None,
879 );
880 assert!(
882 result.is_empty(),
883 "expected empty result for nonexistent symbol"
884 );
885 }
886
887 #[test]
888 fn extract_calls_does_not_panic_on_function_calls() {
889 let src = r#"
891 fn foo() {}
892 fn bar() {
893 foo();
894 }
895 "#;
896 let result = SemanticExtractor::extract(src, "rust", None, None);
898 assert!(
900 result.is_ok(),
901 "extract must succeed on source with function calls"
902 );
903 let output = result.unwrap();
904 assert!(
905 !output.calls.is_empty(),
906 "extract must return call entries for source with function calls"
907 );
908 }
909
910 #[test]
911 fn extract_calls_caps_arg_count_at_sixteen_hops() {
912 let src = r#"fn main() { f((((((((((((((((((((g())))))))))))))))))))); }"#;
918 let result = SemanticExtractor::extract(src, "rust", None, None);
919 assert!(
920 result.is_ok(),
921 "extract must succeed even with deeply nested parenthesized arguments"
922 );
923 let output = result.unwrap();
924 let g_calls: Vec<&CallInfo> = output.calls.iter().filter(|c| c.callee == "g").collect();
925 assert_eq!(
926 g_calls.len(),
927 1,
928 "expected exactly one CallInfo with callee 'g', got {}",
929 g_calls.len()
930 );
931 assert_eq!(
934 g_calls[0].arg_count,
935 Some(0),
936 "g() has 0 arguments, expected Some(0)"
937 );
938 }
939}
940
941#[cfg(test)]
942mod tests_python {
944 use super::*;
945
946 #[test]
947 fn test_python_relative_import() {
948 let source = "from . import foo\n";
950 let result = SemanticExtractor::extract(source, "python", None, None).unwrap();
952 let relative = result.imports.iter().find(|imp| imp.module.contains("."));
954 assert!(
955 relative.is_some(),
956 "expected relative import in {:?}",
957 result.imports
958 );
959 }
960
961 #[test]
962 fn test_python_aliased_import() {
963 let source = "from os import path as p\n";
966 let result = SemanticExtractor::extract(source, "python", None, None).unwrap();
968 let path_import = result
970 .imports
971 .iter()
972 .find(|imp| imp.module == "os" && imp.items.iter().any(|i| i == "path"));
973 assert!(
974 path_import.is_some(),
975 "expected import 'path' from module 'os' in {:?}",
976 result.imports
977 );
978 }
979
980 #[test]
981 fn test_parse_no_timeout_when_none() {
982 let source = r#"fn hello() -> u32 { 42 }"#;
984 let result = SemanticExtractor::extract(source, "rust", None, None);
986 assert!(result.is_ok(), "extract with deadline=None should succeed");
988 let analysis = result.unwrap();
989 assert!(
990 analysis.functions.len() >= 1,
991 "should find at least one function"
992 );
993 }
994
995 #[test]
996 fn test_parse_timeout_triggers_error() {
997 let source = r#"fn hello() -> u32 { 42 }"#;
999 let result = SemanticExtractor::extract(source, "rust", None, Some(1u64));
1001 assert!(
1003 matches!(result, Err(ParserError::Timeout(_))),
1004 "expected Timeout error, got {:?}",
1005 result
1006 );
1007 }
1008}
1009
1010#[cfg(test)]
1012mod tests_unsupported {
1013 use super::*;
1014
1015 #[test]
1016 fn test_element_extractor_unsupported_language() {
1017 let result = ElementExtractor::extract_with_depth("x = 1", "cobol");
1019 assert!(
1021 matches!(result, Err(ParserError::UnsupportedLanguage(ref lang)) if lang == "cobol"),
1022 "expected UnsupportedLanguage error, got {:?}",
1023 result
1024 );
1025 }
1026
1027 #[test]
1028 fn test_semantic_extractor_unsupported_language() {
1029 let result = SemanticExtractor::extract("x = 1", "cobol", None, None);
1031 assert!(
1033 matches!(result, Err(ParserError::UnsupportedLanguage(ref lang)) if lang == "cobol"),
1034 "expected UnsupportedLanguage error, got {:?}",
1035 result
1036 );
1037 }
1038}