Skip to main content

aptu_coder_core/
parser.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Tree-sitter-based parser for extracting semantic structure from source code.
4//!
5//! This module provides language-agnostic parsing using tree-sitter queries to extract
6//! functions, classes, imports, references, and other semantic elements from source files.
7//! Two main extractors handle different use cases:
8//!
9//! - [`ElementExtractor`]: Quick extraction of function and class counts.
10//! - [`SemanticExtractor`]: Detailed semantic analysis with calls, imports, and references.
11
12use crate::languages::{get_language_info, try_regex_fallback};
13use crate::types::{
14    CallInfo, ClassInfo, FunctionInfo, ImplTraitInfo, ImportInfo, ReferenceInfo, ReferenceType,
15    SemanticAnalysis,
16};
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::path::{Path, PathBuf};
20use std::sync::LazyLock;
21use thiserror::Error;
22use tracing::instrument;
23use tree_sitter::{Node, Parser, Query, QueryCursor, StreamingIterator};
24
25#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum ParserError {
28    #[error("Unsupported language: {0}")]
29    UnsupportedLanguage(String),
30    #[error("Failed to parse file: {0}")]
31    ParseError(String),
32    #[error("Invalid UTF-8 in file")]
33    InvalidUtf8,
34    #[error("Query error: {0}")]
35    QueryError(String),
36    #[error("Parse timeout exceeded: {0} microseconds")]
37    Timeout(u64),
38}
39
40/// Groups a query deadline with the configured timeout duration for use in private extract helpers.
41/// Avoids threading two separate values through every helper signature.
42#[derive(Clone, Copy)]
43struct TimeoutConfig {
44    /// Absolute deadline; `None` means no timeout.
45    pub deadline: Option<std::time::Instant>,
46    /// The configured timeout in microseconds (used in `ParserError::Timeout`).
47    pub micros: u64,
48}
49
50impl TimeoutConfig {
51    fn new(timeout_micros: Option<u64>) -> Self {
52        let deadline = timeout_micros
53            .map(|us| std::time::Instant::now() + std::time::Duration::from_micros(us));
54        Self {
55            deadline,
56            micros: timeout_micros.unwrap_or(0),
57        }
58    }
59
60    /// Returns `true` if the deadline has been reached.
61    fn is_exceeded(self) -> bool {
62        self.deadline
63            .is_some_and(|d| std::time::Instant::now() >= d)
64    }
65}
66
67/// Compiled tree-sitter queries for a language.
68/// Stores all query types: mandatory (element, call) and optional (import, impl, reference).
69struct CompiledQueries {
70    pub element: Query,
71    pub call: Query,
72    pub import: Option<Query>,
73    pub impl_block: Option<Query>,
74    pub reference: Option<Query>,
75    pub impl_trait: Option<Query>,
76    pub defuse: Option<Query>,
77}
78
79/// Build compiled queries for a given language.
80///
81/// The `map_err` closures inside are only reachable if a hardcoded query string is
82/// invalid, which cannot happen at runtime -- exclude them from coverage instrumentation.
83#[cfg_attr(coverage_nightly, coverage(off))]
84fn build_compiled_queries(
85    lang_info: &crate::languages::LanguageInfo,
86) -> Result<CompiledQueries, ParserError> {
87    let element = Query::new(&lang_info.language, lang_info.element_query).map_err(|e| {
88        ParserError::QueryError(format!(
89            "Failed to compile element query for {}: {}",
90            lang_info.name, e
91        ))
92    })?;
93
94    let call = Query::new(&lang_info.language, lang_info.call_query).map_err(|e| {
95        ParserError::QueryError(format!(
96            "Failed to compile call query for {}: {}",
97            lang_info.name, e
98        ))
99    })?;
100
101    let import = if let Some(import_query_str) = lang_info.import_query {
102        Some(
103            Query::new(&lang_info.language, import_query_str).map_err(|e| {
104                ParserError::QueryError(format!(
105                    "Failed to compile import query for {}: {}",
106                    lang_info.name, e
107                ))
108            })?,
109        )
110    } else {
111        None
112    };
113
114    let impl_block = if let Some(impl_query_str) = lang_info.impl_query {
115        Some(
116            Query::new(&lang_info.language, impl_query_str).map_err(|e| {
117                ParserError::QueryError(format!(
118                    "Failed to compile impl query for {}: {}",
119                    lang_info.name, e
120                ))
121            })?,
122        )
123    } else {
124        None
125    };
126
127    let reference = if let Some(ref_query_str) = lang_info.reference_query {
128        Some(Query::new(&lang_info.language, ref_query_str).map_err(|e| {
129            ParserError::QueryError(format!(
130                "Failed to compile reference query for {}: {}",
131                lang_info.name, e
132            ))
133        })?)
134    } else {
135        None
136    };
137
138    let impl_trait = if let Some(impl_trait_query_str) = lang_info.impl_trait_query {
139        Some(
140            Query::new(&lang_info.language, impl_trait_query_str).map_err(|e| {
141                ParserError::QueryError(format!(
142                    "Failed to compile impl_trait query for {}: {}",
143                    lang_info.name, e
144                ))
145            })?,
146        )
147    } else {
148        None
149    };
150
151    let defuse = if let Some(defuse_query_str) = lang_info.defuse_query {
152        Some(
153            Query::new(&lang_info.language, defuse_query_str).map_err(|e| {
154                ParserError::QueryError(format!(
155                    "Failed to compile defuse query for {}: {}",
156                    lang_info.name, e
157                ))
158            })?,
159        )
160    } else {
161        None
162    };
163
164    Ok(CompiledQueries {
165        element,
166        call,
167        import,
168        impl_block,
169        reference,
170        impl_trait,
171        defuse,
172    })
173}
174
175/// Initialize the query cache with compiled queries for all supported languages.
176///
177/// Excluded from coverage: the `Err` arm is unreachable because `build_compiled_queries`
178/// only fails on invalid hardcoded query strings.
179#[cfg_attr(coverage_nightly, coverage(off))]
180fn init_query_cache() -> HashMap<&'static str, CompiledQueries> {
181    let mut cache = HashMap::new();
182
183    for lang_name in crate::lang::supported_languages() {
184        if let Some(lang_info) = get_language_info(lang_name) {
185            match build_compiled_queries(&lang_info) {
186                Ok(compiled) => {
187                    cache.insert(*lang_name, compiled);
188                }
189                Err(e) => {
190                    tracing::error!(
191                        "Failed to compile queries for language {}: {}",
192                        lang_name,
193                        e
194                    );
195                }
196            }
197        }
198    }
199
200    cache
201}
202
203/// Lazily initialized cache of compiled queries per language.
204static QUERY_CACHE: LazyLock<HashMap<&'static str, CompiledQueries>> =
205    LazyLock::new(init_query_cache);
206
207/// Get compiled queries for a language from the cache.
208fn get_compiled_queries(language: &str) -> Result<&'static CompiledQueries, ParserError> {
209    QUERY_CACHE
210        .get(language)
211        .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))
212}
213
214thread_local! {
215    static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
216    static QUERY_CURSOR: RefCell<QueryCursor> = RefCell::new(QueryCursor::new());
217}
218
219/// Canonical API for extracting element counts from source code.
220pub struct ElementExtractor;
221
222impl ElementExtractor {
223    /// Extract function and class counts from source code.
224    ///
225    /// # Errors
226    ///
227    /// Returns `ParserError::UnsupportedLanguage` if the language is not recognized.
228    /// Returns `ParserError::ParseError` if the source code cannot be parsed.
229    /// Returns `ParserError::QueryError` if the tree-sitter query fails.
230    #[instrument(skip_all, fields(language))]
231    pub fn extract_with_depth(source: &str, language: &str) -> Result<(usize, usize), ParserError> {
232        let lang_info = get_language_info(language)
233            .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
234
235        let tree = PARSER.with(|p| {
236            let mut parser = p.borrow_mut();
237            parser
238                .set_language(&lang_info.language)
239                .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
240            parser
241                .parse(source, None)
242                .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
243        })?;
244
245        let compiled = get_compiled_queries(language)?;
246
247        let (function_count, class_count) = QUERY_CURSOR.with(|c| {
248            let mut cursor = c.borrow_mut();
249            cursor.set_max_start_depth(None);
250            let mut function_count = 0;
251            let mut class_count = 0;
252
253            let mut matches =
254                cursor.matches(&compiled.element, tree.root_node(), source.as_bytes());
255            while let Some(mat) = matches.next() {
256                for capture in mat.captures {
257                    let capture_name = compiled.element.capture_names()[capture.index as usize];
258                    match capture_name {
259                        "function" => function_count += 1,
260                        "class" => class_count += 1,
261                        _ => {}
262                    }
263                }
264            }
265            (function_count, class_count)
266        });
267
268        tracing::debug!(language = %language, functions = function_count, classes = class_count, "parse complete");
269
270        Ok((function_count, class_count))
271    }
272}
273
274/// Recursively extract `ImportInfo` entries from a use-clause node, respecting all Rust
275/// use-declaration forms (`scoped_identifier`, `scoped_use_list`, `use_list`,
276/// `use_as_clause`, `use_wildcard`, bare `identifier`).
277#[allow(clippy::too_many_lines)] // exhaustive match over all supported Rust use-clause forms; splitting harms readability
278fn extract_imports_from_node(
279    node: &Node,
280    source: &str,
281    prefix: &str,
282    line: usize,
283    imports: &mut Vec<ImportInfo>,
284) {
285    match node.kind() {
286        // Simple identifier: `use foo;` or an item inside `{foo, bar}`
287        "identifier" | "self" | "super" | "crate" => {
288            let name = source[node.start_byte()..node.end_byte()].to_string();
289            imports.push(ImportInfo {
290                module: prefix.to_string(),
291                items: vec![name],
292                line,
293            });
294        }
295        // Qualified path: `std::collections::HashMap`
296        "scoped_identifier" => {
297            let item = node
298                .child_by_field_name("name")
299                .map(|n| source[n.start_byte()..n.end_byte()].to_string())
300                .unwrap_or_default();
301            let module = node.child_by_field_name("path").map_or_else(
302                || prefix.to_string(),
303                |p| {
304                    let path_text = source[p.start_byte()..p.end_byte()].to_string();
305                    if prefix.is_empty() {
306                        path_text
307                    } else {
308                        format!("{prefix}::{path_text}")
309                    }
310                },
311            );
312            if !item.is_empty() {
313                imports.push(ImportInfo {
314                    module,
315                    items: vec![item],
316                    line,
317                });
318            }
319        }
320        // `std::{io, fs}` — path prefix followed by a brace list
321        "scoped_use_list" => {
322            let new_prefix = node.child_by_field_name("path").map_or_else(
323                || prefix.to_string(),
324                |p| {
325                    let path_text = source[p.start_byte()..p.end_byte()].to_string();
326                    if prefix.is_empty() {
327                        path_text
328                    } else {
329                        format!("{prefix}::{path_text}")
330                    }
331                },
332            );
333            if let Some(list) = node.child_by_field_name("list") {
334                extract_imports_from_node(&list, source, &new_prefix, line, imports);
335            }
336        }
337        // `{HashMap, HashSet}` — brace-enclosed list of items
338        "use_list" => {
339            let mut cursor = node.walk();
340            for child in node.children(&mut cursor) {
341                match child.kind() {
342                    "{" | "}" | "," => {}
343                    _ => extract_imports_from_node(&child, source, prefix, line, imports),
344                }
345            }
346        }
347        // `std::io::*` — glob import
348        "use_wildcard" => {
349            let text = source[node.start_byte()..node.end_byte()].to_string();
350            let module = if let Some(stripped) = text.strip_suffix("::*") {
351                if prefix.is_empty() {
352                    stripped.to_string()
353                } else {
354                    format!("{prefix}::{stripped}")
355                }
356            } else {
357                prefix.to_string()
358            };
359            imports.push(ImportInfo {
360                module,
361                items: vec!["*".to_string()],
362                line,
363            });
364        }
365        // `io as stdio` or `std::io as stdio`
366        "use_as_clause" => {
367            let alias = node
368                .child_by_field_name("alias")
369                .map(|n| source[n.start_byte()..n.end_byte()].to_string())
370                .unwrap_or_default();
371            let module = if let Some(path_node) = node.child_by_field_name("path") {
372                match path_node.kind() {
373                    "scoped_identifier" => path_node.child_by_field_name("path").map_or_else(
374                        || prefix.to_string(),
375                        |p| {
376                            let p_text = source[p.start_byte()..p.end_byte()].to_string();
377                            if prefix.is_empty() {
378                                p_text
379                            } else {
380                                format!("{prefix}::{p_text}")
381                            }
382                        },
383                    ),
384                    _ => prefix.to_string(),
385                }
386            } else {
387                prefix.to_string()
388            };
389            if !alias.is_empty() {
390                imports.push(ImportInfo {
391                    module,
392                    items: vec![alias],
393                    line,
394                });
395            }
396        }
397        // Python import_from_statement: `from module import name` or `from . import *`
398        "import_from_statement" => {
399            extract_python_import_from(node, source, line, imports);
400        }
401        // Fallback for non-Rust import nodes: capture full text as module
402        _ => {
403            let text = source[node.start_byte()..node.end_byte()]
404                .trim()
405                .to_string();
406            if !text.is_empty() {
407                imports.push(ImportInfo {
408                    module: text,
409                    items: vec![],
410                    line,
411                });
412            }
413        }
414    }
415}
416
417/// Extract an item name from a `dotted_name` or `aliased_import` child node.
418fn extract_import_item_name(child: &Node, source: &str) -> Option<String> {
419    match child.kind() {
420        "dotted_name" => {
421            let name = source[child.start_byte()..child.end_byte()]
422                .trim()
423                .to_string();
424            if name.is_empty() { None } else { Some(name) }
425        }
426        "aliased_import" => child.child_by_field_name("name").and_then(|n| {
427            let name = source[n.start_byte()..n.end_byte()].trim().to_string();
428            if name.is_empty() { None } else { Some(name) }
429        }),
430        _ => None,
431    }
432}
433
434/// Collect wildcard/named imports from an `import_list` node or from direct named children.
435fn collect_import_items(
436    node: &Node,
437    source: &str,
438    is_wildcard: &mut bool,
439    items: &mut Vec<String>,
440) {
441    // Prefer import_list child (wraps `from x import a, b`)
442    if let Some(import_list) = node.child_by_field_name("import_list") {
443        let mut cursor = import_list.walk();
444        for child in import_list.named_children(&mut cursor) {
445            if child.kind() == "wildcard_import" {
446                *is_wildcard = true;
447            } else if let Some(name) = extract_import_item_name(&child, source) {
448                items.push(name);
449            }
450        }
451        return;
452    }
453    // No import_list: single-name or wildcard as direct child (skip first named child = module_name)
454    let mut cursor = node.walk();
455    let mut first = true;
456    for child in node.named_children(&mut cursor) {
457        if first {
458            first = false;
459            continue;
460        }
461        if child.kind() == "wildcard_import" {
462            *is_wildcard = true;
463        } else if let Some(name) = extract_import_item_name(&child, source) {
464            items.push(name);
465        }
466    }
467}
468
469/// Handle Python `import_from_statement` node.
470fn extract_python_import_from(
471    node: &Node,
472    source: &str,
473    line: usize,
474    imports: &mut Vec<ImportInfo>,
475) {
476    let module = if let Some(m) = node.child_by_field_name("module_name") {
477        source[m.start_byte()..m.end_byte()].trim().to_string()
478    } else if let Some(r) = node.child_by_field_name("relative_import") {
479        source[r.start_byte()..r.end_byte()].trim().to_string()
480    } else {
481        String::new()
482    };
483
484    let mut is_wildcard = false;
485    let mut items = Vec::new();
486    collect_import_items(node, source, &mut is_wildcard, &mut items);
487
488    if !module.is_empty() {
489        imports.push(ImportInfo {
490            module,
491            items: if is_wildcard {
492                vec!["*".to_string()]
493            } else {
494                items
495            },
496            line,
497        });
498    }
499}
500
501pub struct SemanticExtractor;
502
503impl SemanticExtractor {
504    /// Extract semantic information from source code.
505    ///
506    /// # Errors
507    ///
508    /// Returns `ParserError::UnsupportedLanguage` if the language is not recognized.
509    /// Returns `ParserError::ParseError` if the source code cannot be parsed.
510    /// Returns `ParserError::QueryError` if the tree-sitter query fails.
511    #[instrument(skip_all, fields(language))]
512    pub fn extract(
513        source: &str,
514        language: &str,
515        ast_recursion_limit: Option<usize>,
516        timeout_micros: Option<u64>,
517    ) -> Result<SemanticAnalysis, ParserError> {
518        let tc = TimeoutConfig::new(timeout_micros);
519
520        // Check deadline at the start before any parsing work.
521        if tc.is_exceeded() {
522            return Err(ParserError::Timeout(tc.micros));
523        }
524
525        // Try regex-based fallback for formats without a tree-sitter grammar.
526        if let Some(analysis) = try_regex_fallback(source, language) {
527            return Ok(analysis);
528        }
529
530        let lang_info = get_language_info(language)
531            .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
532
533        let tree = PARSER.with(|p| {
534            let mut parser = p.borrow_mut();
535            parser
536                .set_language(&lang_info.language)
537                .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
538            parser
539                .parse(source, None)
540                .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
541        })?;
542
543        // 0 is not a useful depth (visits root node only, returning zero results).
544        // Treat 0 as None (unlimited). See #339.
545        let max_depth: Option<u32> = ast_recursion_limit
546            .filter(|&limit| limit > 0)
547            .map(|limit| {
548                u32::try_from(limit).map_err(|_| {
549                    ParserError::ParseError(format!(
550                        "ast_recursion_limit {} exceeds maximum supported value {}",
551                        limit,
552                        u32::MAX
553                    ))
554                })
555            })
556            .transpose()?;
557
558        let compiled = get_compiled_queries(language)?;
559        let root = tree.root_node();
560
561        let mut functions = Vec::new();
562        let mut classes = Vec::new();
563        let mut imports = Vec::new();
564        let mut references = Vec::new();
565        let mut call_frequency = HashMap::new();
566        let mut calls = Vec::new();
567
568        Self::extract_elements(
569            source,
570            compiled,
571            root,
572            max_depth,
573            &lang_info,
574            &mut functions,
575            &mut classes,
576            tc,
577        )?;
578        Self::extract_calls(
579            source,
580            compiled,
581            root,
582            max_depth,
583            &mut calls,
584            &mut call_frequency,
585            tc,
586        )?;
587        Self::extract_imports(source, compiled, root, max_depth, &mut imports, tc)?;
588        Self::extract_impl_methods(source, compiled, root, max_depth, &mut classes, tc)?;
589        Self::extract_references(source, compiled, root, max_depth, &mut references, tc)?;
590
591        // Extract impl-trait blocks for Rust files (empty for other languages)
592        let impl_traits = if language == "rust" {
593            Self::extract_impl_traits_from_tree(source, compiled, root, tc)?
594        } else {
595            vec![]
596        };
597
598        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");
599
600        Ok(SemanticAnalysis {
601            functions,
602            classes,
603            imports,
604            references,
605            call_frequency,
606            calls,
607            impl_traits,
608            def_use_sites: Vec::new(),
609        })
610    }
611
612    /// Fast path for extracting module metadata: functions and imports only.
613    ///
614    /// This method is optimized for the `analyze_module` tool, which only needs function
615    /// definitions and import statements. It skips the more expensive extractors (calls,
616    /// references, impl traits) and returns a lightweight `ModuleInfo` directly.
617    ///
618    /// # Arguments
619    ///
620    /// * `source` - The source code as a string
621    /// * `language` - The programming language (e.g., "rust", "python")
622    /// * `timeout` - Optional timeout configuration in microseconds
623    ///
624    /// # Returns
625    ///
626    /// A `ModuleInfo` containing the file name, line count, language, functions, and imports.
627    ///
628    /// # Errors
629    ///
630    /// Returns a `ParserError` if:
631    /// * `ParserError::Timeout` - The operation exceeds the specified timeout
632    /// * `ParserError::UnsupportedLanguage` - The language is not supported
633    /// * `ParserError::ParseError` - Tree-sitter parsing fails
634    #[instrument(skip_all, fields(language))]
635    pub fn extract_module_info(
636        source: &str,
637        language: &str,
638        timeout_micros: Option<u64>,
639    ) -> Result<crate::types::ModuleInfo, ParserError> {
640        let tc = TimeoutConfig::new(timeout_micros);
641
642        // Check deadline at the start before any parsing work.
643        if tc.is_exceeded() {
644            return Err(ParserError::Timeout(tc.micros));
645        }
646
647        let lang_info = get_language_info(language)
648            .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
649
650        let tree = PARSER.with(|p| {
651            let mut parser = p.borrow_mut();
652            parser
653                .set_language(&lang_info.language)
654                .map_err(|e| ParserError::ParseError(format!("Failed to set language: {e}")))?;
655            parser
656                .parse(source, None)
657                .ok_or_else(|| ParserError::ParseError("Failed to parse".to_string()))
658        })?;
659
660        // Check deadline after parsing
661        if tc.is_exceeded() {
662            return Err(ParserError::Timeout(tc.micros));
663        }
664
665        let compiled = get_compiled_queries(language)?;
666        let root = tree.root_node();
667
668        let mut functions = Vec::new();
669        let mut classes = Vec::new();
670        let mut imports = Vec::new();
671
672        // Extract functions and classes
673        Self::extract_elements(
674            source,
675            compiled,
676            root,
677            None,
678            &lang_info,
679            &mut functions,
680            &mut classes,
681            tc,
682        )?;
683
684        // Check deadline after extract_elements
685        if tc.is_exceeded() {
686            return Err(ParserError::Timeout(tc.micros));
687        }
688
689        // Extract imports
690        Self::extract_imports(source, compiled, root, None, &mut imports, tc)?;
691
692        // Check deadline after extract_imports
693        if tc.is_exceeded() {
694            return Err(ParserError::Timeout(tc.micros));
695        }
696
697        // Map to ModuleInfo
698        let module_functions = functions
699            .into_iter()
700            .map(|f| crate::types::ModuleFunctionInfo {
701                name: f.name,
702                line: f.line,
703            })
704            .collect();
705
706        let module_imports = imports
707            .into_iter()
708            .map(|i| crate::types::ModuleImportInfo {
709                module: i.module,
710                items: i.items,
711            })
712            .collect();
713
714        let line_count = source.lines().count();
715
716        Ok(crate::types::ModuleInfo::new(
717            String::new(), // Will be set by caller
718            line_count,
719            language.to_string(),
720            module_functions,
721            module_imports,
722        ))
723    }
724
725    // Extracts function and class definitions from a pre-parsed syntax tree.
726    #[allow(clippy::too_many_arguments)]
727    fn extract_elements(
728        source: &str,
729        compiled: &CompiledQueries,
730        root: Node<'_>,
731        max_depth: Option<u32>,
732        lang_info: &crate::languages::LanguageInfo,
733        functions: &mut Vec<FunctionInfo>,
734        classes: &mut Vec<ClassInfo>,
735        tc: TimeoutConfig,
736    ) -> Result<(), ParserError> {
737        let mut seen_functions = std::collections::HashSet::new();
738        let mut timed_out = false;
739
740        QUERY_CURSOR.with(|c| {
741            let mut cursor = c.borrow_mut();
742            cursor.set_max_start_depth(None);
743            if let Some(depth) = max_depth {
744                cursor.set_max_start_depth(Some(depth));
745            }
746
747            let mut matches = cursor.matches(&compiled.element, root, source.as_bytes());
748
749            while let Some(mat) = matches.next() {
750                // Check if we've hit the deadline
751                if tc.is_exceeded() {
752                    timed_out = true;
753                    break;
754                }
755                let mut func_node: Option<Node> = None;
756                let mut func_name_text: Option<String> = None;
757                let mut class_node: Option<Node> = None;
758                let mut class_name_text: Option<String> = None;
759
760                for capture in mat.captures {
761                    let capture_name = compiled.element.capture_names()[capture.index as usize];
762                    let node = capture.node;
763                    match capture_name {
764                        "function" => func_node = Some(node),
765                        "func_name" | "method_name" => {
766                            func_name_text =
767                                Some(source[node.start_byte()..node.end_byte()].to_string());
768                        }
769                        "class" => class_node = Some(node),
770                        "class_name" | "type_name" => {
771                            class_name_text =
772                                Some(source[node.start_byte()..node.end_byte()].to_string());
773                        }
774                        _ => {}
775                    }
776                }
777
778                if let Some(func_node) = func_node {
779                    // When a plain function_definition is nested inside a template_declaration
780                    // or decorated_definition, it is also matched by the explicit wrapper pattern.
781                    // Skip it here to avoid duplicates; the wrapper match will emit it.
782                    let parent_kind = func_node.parent().map(|p| p.kind());
783                    let parent_is_wrapper = parent_kind
784                        .map(|k| k == "template_declaration" || k == "decorated_definition")
785                        .unwrap_or(false);
786                    if func_node.kind() == "function_definition" && parent_is_wrapper {
787                        // Handled by the template_declaration or decorated_definition @function match instead.
788                    } else {
789                        // Resolve template_declaration or decorated_definition to inner function_definition
790                        // for declarator/field walks. The captured node may be a wrapper.
791                        let func_def = if func_node.kind() == "template_declaration" {
792                            let mut cursor = func_node.walk();
793                            func_node
794                                .children(&mut cursor)
795                                .find(|n| n.kind() == "function_definition")
796                                .unwrap_or(func_node)
797                        } else if func_node.kind() == "decorated_definition" {
798                            func_node
799                                .child_by_field_name("definition")
800                                .unwrap_or(func_node)
801                        } else {
802                            func_node
803                        };
804
805                        let name = func_name_text
806                            .or_else(|| {
807                                func_def
808                                    .child_by_field_name("name")
809                                    .map(|n| source[n.start_byte()..n.end_byte()].to_string())
810                            })
811                            .unwrap_or_default();
812
813                        let func_key = (name.clone(), func_node.start_position().row);
814                        if !name.is_empty() && seen_functions.insert(func_key) {
815                            // For C/C++: parameters live under declarator -> parameters.
816                            // For other languages: parameters is a direct child field.
817                            let params = func_def
818                                .child_by_field_name("declarator")
819                                .and_then(|d| d.child_by_field_name("parameters"))
820                                .or_else(|| func_def.child_by_field_name("parameters"))
821                                .map(|p| source[p.start_byte()..p.end_byte()].to_string())
822                                .unwrap_or_default();
823
824                            // Try "type" first (C/C++ uses this field for the return type);
825                            // fall back to "return_type" (Rust, Python, TypeScript, etc.).
826                            let return_type = func_def
827                                .child_by_field_name("type")
828                                .or_else(|| func_def.child_by_field_name("return_type"))
829                                .map(|r| source[r.start_byte()..r.end_byte()].to_string());
830
831                            // Walk backward through contiguous attribute_item siblings
832                            // to find the first attribute line (Rust only).
833                            let first_line = if func_node.kind() == "function_item" {
834                                let mut attrs: Vec<Node> = Vec::new();
835                                let mut sib = func_node.prev_named_sibling();
836                                while let Some(s) = sib {
837                                    if s.kind() == "attribute_item" {
838                                        attrs.push(s);
839                                        sib = s.prev_named_sibling();
840                                    } else {
841                                        break;
842                                    }
843                                }
844                                attrs
845                                    .last()
846                                    .map(|n| n.start_position().row + 1)
847                                    .unwrap_or_else(|| func_node.start_position().row + 1)
848                            } else {
849                                func_node.start_position().row + 1
850                            };
851
852                            functions.push(FunctionInfo {
853                                name,
854                                line: first_line,
855                                end_line: func_node.end_position().row + 1,
856                                parameters: if params.is_empty() {
857                                    Vec::new()
858                                } else {
859                                    vec![params]
860                                },
861                                return_type,
862                            });
863                        }
864                    }
865                }
866
867                if let Some(class_node) = class_node {
868                    let name = class_name_text
869                        .or_else(|| {
870                            class_node
871                                .child_by_field_name("name")
872                                .map(|n| source[n.start_byte()..n.end_byte()].to_string())
873                        })
874                        .unwrap_or_default();
875
876                    if !name.is_empty() {
877                        let inherits = if let Some(handler) = lang_info.extract_inheritance {
878                            handler(&class_node, source)
879                        } else {
880                            Vec::new()
881                        };
882                        classes.push(ClassInfo {
883                            name,
884                            line: class_node.start_position().row + 1,
885                            end_line: class_node.end_position().row + 1,
886                            methods: Vec::new(),
887                            fields: Vec::new(),
888                            inherits,
889                        });
890                    }
891                }
892            }
893        });
894
895        if timed_out {
896            return Err(ParserError::Timeout(tc.micros));
897        }
898
899        Ok(())
900    }
901
902    /// Returns the name of the enclosing function/method/subroutine for a given AST node,
903    /// by walking ancestors and matching all language-specific function container kinds.
904    fn enclosing_function_name(mut node: tree_sitter::Node<'_>, source: &str) -> Option<String> {
905        let mut depth = 0u32;
906        while let Some(parent) = node.parent() {
907            depth += 1;
908            // Cap at 64 hops: real function nesting rarely exceeds ~10 levels; 64 is a generous
909            // upper bound that guards against pathological/malformed ASTs without false negatives
910            // on legitimate code. Returns None (treated as <module>) when the cap is hit.
911            if depth > 64 {
912                return None;
913            }
914            let name_node = match parent.kind() {
915                // Direct name field: Rust, Python, Go, Java, TypeScript/TSX
916                "function_item"
917                | "method_item"
918                | "function_definition"
919                | "function_declaration"
920                | "method_declaration"
921                | "method_definition" => parent.child_by_field_name("name"),
922                // Fortran subroutine: name is inside subroutine_statement child
923                "subroutine" => {
924                    let mut cursor = parent.walk();
925                    parent
926                        .children(&mut cursor)
927                        .find(|c| c.kind() == "subroutine_statement")
928                        .and_then(|s| s.child_by_field_name("name"))
929                }
930                // Fortran function: name is inside function_statement child
931                "function" => {
932                    let mut cursor = parent.walk();
933                    parent
934                        .children(&mut cursor)
935                        .find(|c| c.kind() == "function_statement")
936                        .and_then(|s| s.child_by_field_name("name"))
937                }
938                _ => {
939                    node = parent;
940                    continue;
941                }
942            };
943            return name_node.map(|n| source[n.start_byte()..n.end_byte()].to_string());
944        }
945        // The loop exits here only when no parent was found (i.e., we reached the tree root
946        // without finding a function container). If the depth cap fired, we returned None early
947        // above. Nothing to assert here.
948        None
949    }
950
951    #[allow(clippy::too_many_arguments)]
952    fn extract_calls(
953        source: &str,
954        compiled: &CompiledQueries,
955        root: Node<'_>,
956        max_depth: Option<u32>,
957        calls: &mut Vec<CallInfo>,
958        call_frequency: &mut HashMap<String, usize>,
959        tc: TimeoutConfig,
960    ) -> Result<(), ParserError> {
961        let mut timed_out = false;
962
963        QUERY_CURSOR.with(|c| {
964            let mut cursor = c.borrow_mut();
965            cursor.set_max_start_depth(None);
966            if let Some(depth) = max_depth {
967                cursor.set_max_start_depth(Some(depth));
968            }
969
970            let mut matches = cursor.matches(&compiled.call, root, source.as_bytes());
971
972            while let Some(mat) = matches.next() {
973                // Check if we've hit the deadline
974                if tc.is_exceeded() {
975                    timed_out = true;
976                    break;
977                }
978                for capture in mat.captures {
979                    let capture_name = compiled.call.capture_names()[capture.index as usize];
980                    if capture_name != "call" {
981                        continue;
982                    }
983                    let node = capture.node;
984                    let call_name = source[node.start_byte()..node.end_byte()].to_string();
985                    *call_frequency.entry(call_name.clone()).or_insert(0) += 1;
986
987                    let caller = Self::enclosing_function_name(node, source)
988                        .unwrap_or_else(|| "<module>".to_string());
989
990                    let mut arg_count = None;
991                    let mut arg_node = node;
992                    let mut hop = 0u32;
993                    while let Some(parent) = arg_node.parent() {
994                        hop += 1;
995                        // Bounded parent traversal: cap at 16 hops to guard against pathological
996                        // walks on malformed/degenerate trees. Real call-expression nesting is
997                        // shallow (typically 1-3 levels). When the cap is hit we stop searching and
998                        // leave arg_count as None; the caller is still recorded, just without
999                        // argument-count information.
1000                        if hop > 16 {
1001                            tracing::debug!(hop, callee = %call_name, "extract_calls: parent traversal cap reached; arg_count will be None");
1002                            break;
1003                        }
1004                        if parent.kind() == "call_expression" {
1005                            if let Some(args) = parent.child_by_field_name("arguments") {
1006                                arg_count = Some(args.named_child_count());
1007                            }
1008                            break;
1009                        }
1010                        arg_node = parent;
1011                    }
1012                    calls.push(CallInfo {
1013                        caller,
1014                        callee: call_name,
1015                        line: node.start_position().row + 1,
1016                        column: node.start_position().column,
1017                        arg_count,
1018                    });
1019                }
1020            }
1021        });
1022
1023        if timed_out {
1024            return Err(ParserError::Timeout(tc.micros));
1025        }
1026
1027        Ok(())
1028    }
1029
1030    // Extracts import statements from a pre-parsed syntax tree.
1031    fn extract_imports(
1032        source: &str,
1033        compiled: &CompiledQueries,
1034        root: Node<'_>,
1035        max_depth: Option<u32>,
1036        imports: &mut Vec<ImportInfo>,
1037        tc: TimeoutConfig,
1038    ) -> Result<(), ParserError> {
1039        let Some(ref import_query) = compiled.import else {
1040            return Ok(());
1041        };
1042        let mut timed_out = false;
1043
1044        QUERY_CURSOR.with(|c| {
1045            let mut cursor = c.borrow_mut();
1046            cursor.set_max_start_depth(None);
1047            if let Some(depth) = max_depth {
1048                cursor.set_max_start_depth(Some(depth));
1049            }
1050
1051            let mut matches = cursor.matches(import_query, root, source.as_bytes());
1052
1053            while let Some(mat) = matches.next() {
1054                // Check if we've hit the deadline
1055                if tc.is_exceeded() {
1056                    timed_out = true;
1057                    break;
1058                }
1059                for capture in mat.captures {
1060                    let capture_name = import_query.capture_names()[capture.index as usize];
1061                    if capture_name == "import_path" {
1062                        let node = capture.node;
1063                        let line = node.start_position().row + 1;
1064                        extract_imports_from_node(&node, source, "", line, imports);
1065                    }
1066                }
1067            }
1068        });
1069
1070        if timed_out {
1071            return Err(ParserError::Timeout(tc.micros));
1072        }
1073
1074        Ok(())
1075    }
1076
1077    fn extract_impl_methods(
1078        source: &str,
1079        compiled: &CompiledQueries,
1080        root: Node<'_>,
1081        max_depth: Option<u32>,
1082        classes: &mut [ClassInfo],
1083        tc: TimeoutConfig,
1084    ) -> Result<(), ParserError> {
1085        let Some(ref impl_query) = compiled.impl_block else {
1086            return Ok(());
1087        };
1088        let mut timed_out = false;
1089
1090        QUERY_CURSOR.with(|c| {
1091            let mut cursor = c.borrow_mut();
1092            cursor.set_max_start_depth(None);
1093            if let Some(depth) = max_depth {
1094                cursor.set_max_start_depth(Some(depth));
1095            }
1096
1097            let mut matches = cursor.matches(impl_query, root, source.as_bytes());
1098
1099            while let Some(mat) = matches.next() {
1100                // Check if we've hit the deadline
1101                if tc.is_exceeded() {
1102                    timed_out = true;
1103                    break;
1104                }
1105
1106                let mut impl_type_name = String::new();
1107                let mut method_name = String::new();
1108                let mut method_line = 0usize;
1109                let mut method_end_line = 0usize;
1110                let mut method_params = String::new();
1111                let mut method_return_type: Option<String> = None;
1112
1113                for capture in mat.captures {
1114                    let capture_name = impl_query.capture_names()[capture.index as usize];
1115                    let node = capture.node;
1116                    match capture_name {
1117                        "impl_type" => {
1118                            impl_type_name = source[node.start_byte()..node.end_byte()].to_string();
1119                        }
1120                        "method_name" => {
1121                            method_name = source[node.start_byte()..node.end_byte()].to_string();
1122                        }
1123                        "method_params" => {
1124                            method_params = source[node.start_byte()..node.end_byte()].to_string();
1125                        }
1126                        "method" => {
1127                            let mut method_attrs: Vec<Node> = Vec::new();
1128                            let mut msib = node.prev_named_sibling();
1129                            while let Some(s) = msib {
1130                                if s.kind() == "attribute_item" {
1131                                    method_attrs.push(s);
1132                                    msib = s.prev_named_sibling();
1133                                } else {
1134                                    break;
1135                                }
1136                            }
1137                            method_line = method_attrs
1138                                .last()
1139                                .map(|n| n.start_position().row + 1)
1140                                .unwrap_or_else(|| node.start_position().row + 1);
1141                            method_end_line = node.end_position().row + 1;
1142                            method_return_type = node
1143                                .child_by_field_name("return_type")
1144                                .map(|r| source[r.start_byte()..r.end_byte()].to_string());
1145                        }
1146                        _ => {}
1147                    }
1148                }
1149
1150                if !impl_type_name.is_empty() && !method_name.is_empty() {
1151                    let func = FunctionInfo {
1152                        name: method_name,
1153                        line: method_line,
1154                        end_line: method_end_line,
1155                        parameters: if method_params.is_empty() {
1156                            Vec::new()
1157                        } else {
1158                            vec![method_params]
1159                        },
1160                        return_type: method_return_type,
1161                    };
1162                    if let Some(class) = classes.iter_mut().find(|c| c.name == impl_type_name) {
1163                        class.methods.push(func);
1164                    }
1165                }
1166            }
1167        });
1168
1169        if timed_out {
1170            return Err(ParserError::Timeout(tc.micros));
1171        }
1172
1173        Ok(())
1174    }
1175
1176    fn extract_references(
1177        source: &str,
1178        compiled: &CompiledQueries,
1179        root: Node<'_>,
1180        max_depth: Option<u32>,
1181        references: &mut Vec<ReferenceInfo>,
1182        tc: TimeoutConfig,
1183    ) -> Result<(), ParserError> {
1184        let Some(ref ref_query) = compiled.reference else {
1185            return Ok(());
1186        };
1187        let mut seen_refs = std::collections::HashSet::new();
1188        let mut timed_out = false;
1189
1190        QUERY_CURSOR.with(|c| {
1191            let mut cursor = c.borrow_mut();
1192            cursor.set_max_start_depth(None);
1193            if let Some(depth) = max_depth {
1194                cursor.set_max_start_depth(Some(depth));
1195            }
1196
1197            let mut matches = cursor.matches(ref_query, root, source.as_bytes());
1198
1199            while let Some(mat) = matches.next() {
1200                // Check if we've hit the deadline
1201                if tc.is_exceeded() {
1202                    timed_out = true;
1203                    break;
1204                }
1205
1206                for capture in mat.captures {
1207                    let capture_name = ref_query.capture_names()[capture.index as usize];
1208                    if capture_name == "type_ref" {
1209                        let node = capture.node;
1210                        let type_ref = source[node.start_byte()..node.end_byte()].to_string();
1211                        if seen_refs.insert(type_ref.clone()) {
1212                            references.push(ReferenceInfo {
1213                                symbol: type_ref,
1214                                reference_type: ReferenceType::Usage,
1215                                // location is intentionally empty here; set by the caller (analyze_file)
1216                                location: String::new(),
1217                                line: node.start_position().row + 1,
1218                            });
1219                        }
1220                    }
1221                }
1222            }
1223        });
1224
1225        if timed_out {
1226            return Err(ParserError::Timeout(tc.micros));
1227        }
1228
1229        Ok(())
1230    }
1231
1232    /// Extract impl-trait blocks from an already-parsed tree.
1233    ///
1234    /// Called during `extract()` for Rust files to avoid a second parse.
1235    /// Returns an empty vec if the query is not available.
1236    fn extract_impl_traits_from_tree(
1237        source: &str,
1238        compiled: &CompiledQueries,
1239        root: Node<'_>,
1240        tc: TimeoutConfig,
1241    ) -> Result<Vec<ImplTraitInfo>, ParserError> {
1242        let Some(query) = &compiled.impl_trait else {
1243            return Ok(vec![]);
1244        };
1245
1246        let mut results = Vec::new();
1247        let mut timed_out = false;
1248
1249        QUERY_CURSOR.with(|c| {
1250            let mut cursor = c.borrow_mut();
1251            cursor.set_max_start_depth(None);
1252
1253            let mut matches = cursor.matches(query, root, source.as_bytes());
1254
1255            while let Some(mat) = matches.next() {
1256                // Check if we've hit the deadline
1257                if tc.is_exceeded() {
1258                    timed_out = true;
1259                    break;
1260                }
1261
1262                let mut trait_name = String::new();
1263                let mut impl_type = String::new();
1264                let mut line = 0usize;
1265
1266                for capture in mat.captures {
1267                    let capture_name = query.capture_names()[capture.index as usize];
1268                    let node = capture.node;
1269                    let text = source[node.start_byte()..node.end_byte()].to_string();
1270                    match capture_name {
1271                        "trait_name" => {
1272                            trait_name = text;
1273                            line = node.start_position().row + 1;
1274                        }
1275                        "impl_type" => {
1276                            impl_type = text;
1277                        }
1278                        _ => {}
1279                    }
1280                }
1281
1282                if !trait_name.is_empty() && !impl_type.is_empty() {
1283                    results.push(ImplTraitInfo {
1284                        trait_name,
1285                        impl_type,
1286                        path: PathBuf::new(), // Path will be set by caller
1287                        line,
1288                    });
1289                }
1290            }
1291        });
1292
1293        if timed_out {
1294            return Err(ParserError::Timeout(tc.micros));
1295        }
1296
1297        Ok(results)
1298    }
1299
1300    /// Extract def-use sites (write/read locations) for a given symbol within a file.
1301    ///
1302    /// Runs the defuse query to find all definition and use sites of a symbol.
1303    /// Returns empty vec if no defuse query is available for this language.
1304    ///
1305    /// # Arguments
1306    ///
1307    /// * `source` - The source code text
1308    /// * `compiled` - Compiled tree-sitter queries
1309    /// * `root` - Root node of the AST
1310    /// * `symbol_name` - The symbol to search for (must match exactly)
1311    /// * `file_path` - Relative file path for site reporting
1312    fn extract_def_use(
1313        source: &str,
1314        compiled: &CompiledQueries,
1315        root: Node<'_>,
1316        symbol_name: &str,
1317        file_path: &str,
1318        max_depth: Option<u32>,
1319    ) -> Vec<crate::types::DefUseSite> {
1320        let Some(ref defuse_query) = compiled.defuse else {
1321            return vec![];
1322        };
1323
1324        let mut sites = Vec::new();
1325        let source_lines: Vec<&str> = source.lines().collect();
1326        // Track byte offsets that already have a write or writeread capture so
1327        // duplicate read captures for the same identifier are suppressed.
1328        let mut write_offsets = std::collections::HashSet::new();
1329
1330        QUERY_CURSOR.with(|c| {
1331            let mut cursor = c.borrow_mut();
1332            cursor.set_max_start_depth(None);
1333            if let Some(depth) = max_depth {
1334                cursor.set_max_start_depth(Some(depth));
1335            }
1336            let mut matches = cursor.matches(defuse_query, root, source.as_bytes());
1337
1338            while let Some(mat) = matches.next() {
1339                for capture in mat.captures {
1340                    let capture_name = defuse_query.capture_names()[capture.index as usize];
1341                    let node = capture.node;
1342                    let node_text = node.utf8_text(source.as_bytes()).unwrap_or_default();
1343
1344                    // Only collect if the captured node matches the target symbol
1345                    if node_text != symbol_name {
1346                        continue;
1347                    }
1348
1349                    // Classify capture by prefix
1350                    let kind = if capture_name.starts_with("write.") {
1351                        crate::types::DefUseKind::Write
1352                    } else if capture_name.starts_with("read.") {
1353                        crate::types::DefUseKind::Read
1354                    } else if capture_name.starts_with("writeread.") {
1355                        crate::types::DefUseKind::WriteRead
1356                    } else {
1357                        continue;
1358                    };
1359
1360                    let byte_offset = node.start_byte();
1361
1362                    // De-duplicate: skip read captures for offsets already captured as write/writeread
1363                    if kind == crate::types::DefUseKind::Read
1364                        && write_offsets.contains(&byte_offset)
1365                    {
1366                        continue;
1367                    }
1368                    if kind != crate::types::DefUseKind::Read {
1369                        write_offsets.insert(byte_offset);
1370                    }
1371
1372                    // Get line number (1-indexed) and center-line snippet.
1373                    // Always produce a 3-line window so snippet_one_line (index 1) is safe.
1374                    let line = node.start_position().row + 1;
1375                    let snippet = {
1376                        let row = node.start_position().row;
1377                        let last_line = source_lines.len().saturating_sub(1);
1378                        let prev = if row > 0 { row - 1 } else { 0 };
1379                        let next = std::cmp::min(row + 1, last_line);
1380                        let prev_text = if row == 0 {
1381                            ""
1382                        } else {
1383                            source_lines[prev].trim_end()
1384                        };
1385                        let cur_text = source_lines[row].trim_end();
1386                        let next_text = if row >= last_line {
1387                            ""
1388                        } else {
1389                            source_lines[next].trim_end()
1390                        };
1391                        format!("{prev_text}\n{cur_text}\n{next_text}")
1392                    };
1393
1394                    // Get enclosing function scope
1395                    let enclosing_scope = Self::enclosing_function_name(node, source);
1396
1397                    let column = node.start_position().column;
1398                    sites.push(crate::types::DefUseSite {
1399                        kind,
1400                        symbol: node_text.to_string(),
1401                        file: file_path.to_string(),
1402                        line,
1403                        column,
1404                        snippet,
1405                        enclosing_scope,
1406                    });
1407                }
1408            }
1409        });
1410
1411        sites
1412    }
1413
1414    /// Parse `source` in `language`, run the defuse query for `symbol`, and return all sites.
1415    /// Returns an empty vec if the language has no defuse query or parsing fails.
1416    pub(crate) fn extract_def_use_for_file(
1417        source: &str,
1418        language: &str,
1419        symbol: &str,
1420        file_path: &str,
1421        ast_recursion_limit: Option<usize>,
1422    ) -> Vec<crate::types::DefUseSite> {
1423        let Some(lang_info) = crate::languages::get_language_info(language) else {
1424            return vec![];
1425        };
1426        let Ok(compiled) = get_compiled_queries(language) else {
1427            return vec![];
1428        };
1429        if compiled.defuse.is_none() {
1430            return vec![];
1431        }
1432
1433        let tree = match PARSER.with(|p| {
1434            let mut parser = p.borrow_mut();
1435            if parser.set_language(&lang_info.language).is_err() {
1436                return None;
1437            }
1438            parser.parse(source, None)
1439        }) {
1440            Some(t) => t,
1441            None => return vec![],
1442        };
1443
1444        let root = tree.root_node();
1445
1446        // Convert ast_recursion_limit the same way extract() does:
1447        // 0 means unlimited (None); positive values become Some(u32).
1448        let max_depth: Option<u32> = ast_recursion_limit
1449            .filter(|&limit| limit > 0)
1450            .and_then(|limit| u32::try_from(limit).ok());
1451
1452        Self::extract_def_use(source, compiled, root, symbol, file_path, max_depth)
1453    }
1454}
1455
1456/// Extract `impl Trait for Type` blocks from Rust source.
1457///
1458/// Runs independently of `extract_references` to avoid shared deduplication state.
1459/// Returns an empty vec for non-Rust source (no error; caller decides).
1460#[must_use]
1461pub fn extract_impl_traits(source: &str, path: &Path) -> Vec<ImplTraitInfo> {
1462    let Some(lang_info) = get_language_info("rust") else {
1463        return vec![];
1464    };
1465
1466    let Ok(compiled) = get_compiled_queries("rust") else {
1467        return vec![];
1468    };
1469
1470    let Some(query) = &compiled.impl_trait else {
1471        return vec![];
1472    };
1473
1474    let Some(tree) = PARSER.with(|p| {
1475        let mut parser = p.borrow_mut();
1476        let _ = parser.set_language(&lang_info.language);
1477        parser.parse(source, None)
1478    }) else {
1479        return vec![];
1480    };
1481
1482    let root = tree.root_node();
1483    let mut results = Vec::new();
1484
1485    QUERY_CURSOR.with(|c| {
1486        let mut cursor = c.borrow_mut();
1487        cursor.set_max_start_depth(None);
1488        let mut matches = cursor.matches(query, root, source.as_bytes());
1489
1490        while let Some(mat) = matches.next() {
1491            let mut trait_name = String::new();
1492            let mut impl_type = String::new();
1493            let mut line = 0usize;
1494
1495            for capture in mat.captures {
1496                let capture_name = query.capture_names()[capture.index as usize];
1497                let node = capture.node;
1498                let text = source[node.start_byte()..node.end_byte()].to_string();
1499                match capture_name {
1500                    "trait_name" => {
1501                        trait_name = text;
1502                        line = node.start_position().row + 1;
1503                    }
1504                    "impl_type" => {
1505                        impl_type = text;
1506                    }
1507                    _ => {}
1508                }
1509            }
1510
1511            if !trait_name.is_empty() && !impl_type.is_empty() {
1512                results.push(ImplTraitInfo {
1513                    trait_name,
1514                    impl_type,
1515                    path: path.to_path_buf(),
1516                    line,
1517                });
1518            }
1519        }
1520    });
1521
1522    results
1523}
1524
1525/// Execute a custom tree-sitter query against source code.
1526///
1527/// This is the internal implementation of the public `execute_query` function.
1528pub(crate) fn execute_query_impl(
1529    language: &str,
1530    source: &str,
1531    query_str: &str,
1532) -> Result<Vec<crate::QueryCapture>, ParserError> {
1533    // Get the tree-sitter language from the language name
1534    let ts_language = crate::languages::get_ts_language(language)
1535        .ok_or_else(|| ParserError::UnsupportedLanguage(language.to_string()))?;
1536
1537    let mut parser = Parser::new();
1538    parser
1539        .set_language(&ts_language)
1540        .map_err(|e| ParserError::QueryError(e.to_string()))?;
1541
1542    let tree = parser
1543        .parse(source.as_bytes(), None)
1544        .ok_or_else(|| ParserError::QueryError("failed to parse source".to_string()))?;
1545
1546    let query =
1547        Query::new(&ts_language, query_str).map_err(|e| ParserError::QueryError(e.to_string()))?;
1548
1549    let source_bytes = source.as_bytes();
1550
1551    let mut captures = Vec::new();
1552    QUERY_CURSOR.with(|c| {
1553        let mut cursor = c.borrow_mut();
1554        cursor.set_max_start_depth(None);
1555        let mut matches = cursor.matches(&query, tree.root_node(), source_bytes);
1556        while let Some(m) = matches.next() {
1557            for cap in m.captures {
1558                let node = cap.node;
1559                let capture_name = query.capture_names()[cap.index as usize].to_string();
1560                let text = node.utf8_text(source_bytes).unwrap_or("").to_string();
1561                captures.push(crate::QueryCapture {
1562                    capture_name,
1563                    text,
1564                    start_line: node.start_position().row,
1565                    end_line: node.end_position().row,
1566                    start_byte: node.start_byte(),
1567                    end_byte: node.end_byte(),
1568                });
1569            }
1570        }
1571    });
1572    Ok(captures)
1573}
1574
1575#[cfg(test)]
1576// Tests for parser functionality
1577mod tests {
1578    use super::*;
1579
1580    #[test]
1581    fn test_ast_recursion_limit_zero_is_unlimited() {
1582        let source = r#"fn hello() -> u32 { 42 }"#;
1583        let result_none = SemanticExtractor::extract(source, "rust", None, None);
1584        let result_zero = SemanticExtractor::extract(source, "rust", Some(0), None);
1585        assert!(result_none.is_ok(), "extract with None failed");
1586        assert!(result_zero.is_ok(), "extract with Some(0) failed");
1587        let analysis_none = result_none.unwrap();
1588        let analysis_zero = result_zero.unwrap();
1589        assert!(
1590            analysis_none.functions.len() >= 1,
1591            "extract with None should find at least one function in the test source"
1592        );
1593        assert_eq!(
1594            analysis_none.functions.len(),
1595            analysis_zero.functions.len(),
1596            "ast_recursion_limit=0 should behave identically to unset (unlimited)"
1597        );
1598    }
1599
1600    #[test]
1601    fn test_rust_use_as_imports() {
1602        // Arrange
1603        let source = "use std::io as stdio;";
1604        // Act
1605        let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
1606        // Assert: alias "stdio" is captured as an import item
1607        assert!(
1608            result
1609                .imports
1610                .iter()
1611                .any(|imp| imp.items.iter().any(|i| i == "stdio")),
1612            "expected import alias 'stdio' in {:?}",
1613            result.imports
1614        );
1615    }
1616
1617    #[test]
1618    fn test_rust_use_as_clause_plain_identifier() {
1619        // Arrange: use_as_clause with plain identifier (no scoped_identifier)
1620        // exercises the _ => prefix.to_string() arm
1621        let source = "use io as stdio;";
1622        // Act
1623        let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
1624        // Assert: alias "stdio" is captured as an import item
1625        assert!(
1626            result
1627                .imports
1628                .iter()
1629                .any(|imp| imp.items.iter().any(|i| i == "stdio")),
1630            "expected import alias 'stdio' from plain identifier in {:?}",
1631            result.imports
1632        );
1633    }
1634
1635    #[test]
1636    fn test_rust_scoped_use_with_prefix() {
1637        // Arrange: scoped_use_list with non-empty prefix
1638        let source = "use std::{io::Read, io::Write};";
1639        // Act
1640        let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
1641        // Assert: both Read and Write appear as items with std::io module
1642        let items: Vec<String> = result
1643            .imports
1644            .iter()
1645            .filter(|imp| imp.module.starts_with("std::io"))
1646            .flat_map(|imp| imp.items.clone())
1647            .collect();
1648        assert!(
1649            items.contains(&"Read".to_string()) && items.contains(&"Write".to_string()),
1650            "expected 'Read' and 'Write' items under module with std::io, got {:?}",
1651            result.imports
1652        );
1653    }
1654
1655    #[test]
1656    fn test_rust_scoped_use_imports() {
1657        // Arrange
1658        let source = "use std::{fs, io};";
1659        // Act
1660        let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
1661        // Assert: both "fs" and "io" appear as import items under module "std"
1662        let items: Vec<&str> = result
1663            .imports
1664            .iter()
1665            .filter(|imp| imp.module == "std")
1666            .flat_map(|imp| imp.items.iter().map(|s| s.as_str()))
1667            .collect();
1668        assert!(
1669            items.contains(&"fs") && items.contains(&"io"),
1670            "expected 'fs' and 'io' items under module 'std', got {:?}",
1671            items
1672        );
1673    }
1674
1675    #[test]
1676    fn test_rust_wildcard_imports() {
1677        // Arrange
1678        let source = "use std::io::*;";
1679        // Act
1680        let result = SemanticExtractor::extract(source, "rust", None, None).unwrap();
1681        // Assert: wildcard import with module "std::io"
1682        let wildcard = result
1683            .imports
1684            .iter()
1685            .find(|imp| imp.module == "std::io" && imp.items == vec!["*"]);
1686        assert!(
1687            wildcard.is_some(),
1688            "expected wildcard import with module 'std::io', got {:?}",
1689            result.imports
1690        );
1691    }
1692
1693    #[test]
1694    fn test_extract_impl_traits_standalone() {
1695        // Arrange: source with a simple impl Trait for Type
1696        let source = r#"
1697struct Foo;
1698trait Display {}
1699impl Display for Foo {}
1700"#;
1701        // Act
1702        let results = extract_impl_traits(source, Path::new("test.rs"));
1703        // Assert
1704        assert_eq!(
1705            results.len(),
1706            1,
1707            "expected one impl trait, got {:?}",
1708            results
1709        );
1710        assert_eq!(results[0].trait_name, "Display");
1711        assert_eq!(results[0].impl_type, "Foo");
1712    }
1713
1714    #[cfg(target_pointer_width = "64")]
1715    #[test]
1716    fn test_ast_recursion_limit_overflow() {
1717        // Arrange: limit larger than u32::MAX triggers a ParseError on 64-bit targets
1718        let source = "fn foo() {}";
1719        let big_limit = usize::try_from(u32::MAX).unwrap() + 1;
1720        // Act
1721        let result = SemanticExtractor::extract(source, "rust", Some(big_limit), None);
1722        // Assert
1723        assert!(
1724            matches!(result, Err(ParserError::ParseError(_))),
1725            "expected ParseError for oversized limit, got {:?}",
1726            result
1727        );
1728    }
1729
1730    #[test]
1731    fn test_ast_recursion_limit_some() {
1732        // Arrange: ast_recursion_limit with Some(depth) to exercise max_depth Some branch
1733        let source = r#"fn hello() -> u32 { 42 }"#;
1734        // Act
1735        let result = SemanticExtractor::extract(source, "rust", Some(5), None);
1736        // Assert: should succeed without error and extract functions
1737        assert!(result.is_ok(), "extract with Some(5) failed: {:?}", result);
1738        let analysis = result.unwrap();
1739        assert!(
1740            analysis.functions.len() >= 1,
1741            "expected at least one function with depth limit 5"
1742        );
1743    }
1744
1745    #[test]
1746    fn test_extract_def_use_for_file_finds_write_and_read() {
1747        // Arrange
1748        let source = r#"
1749fn main() {
1750    let count = 0;
1751    println!("{}", count);
1752}
1753"#;
1754        // Act
1755        let sites = SemanticExtractor::extract_def_use_for_file(
1756            source,
1757            "rust",
1758            "count",
1759            "src/main.rs",
1760            None,
1761        );
1762
1763        // Assert
1764        assert!(
1765            !sites.is_empty(),
1766            "expected at least one def-use site for 'count'"
1767        );
1768        let has_write = sites
1769            .iter()
1770            .any(|s| s.kind == crate::types::DefUseKind::Write);
1771        let has_read = sites
1772            .iter()
1773            .any(|s| s.kind == crate::types::DefUseKind::Read);
1774        assert!(has_write, "expected a write site for 'count'");
1775        assert!(has_read, "expected a read site for 'count'");
1776        assert_eq!(sites[0].file, "src/main.rs");
1777    }
1778
1779    #[test]
1780    fn test_extract_def_use_for_file_no_match_returns_empty() {
1781        // Arrange
1782        let source = "fn foo() { let x = 1; }";
1783
1784        // Act
1785        let sites = SemanticExtractor::extract_def_use_for_file(
1786            source,
1787            "rust",
1788            "nonexistent_symbol",
1789            "src/lib.rs",
1790            None,
1791        );
1792
1793        // Assert
1794        assert!(sites.is_empty(), "expected empty for nonexistent symbol");
1795    }
1796
1797    #[test]
1798    fn extract_calls_does_not_panic_on_function_calls() {
1799        // Regression test for #1251: debug_assert in extract_calls fired on degenerate
1800        // ASTs and killed the server process via panic=abort. Verify extract() succeeds
1801        // and returns call info for normal source.
1802        let src = r#"
1803fn caller() {
1804    callee_a();
1805    callee_b(1, 2);
1806    nested::callee_c();
1807}
1808"#;
1809        let result = SemanticExtractor::extract(src, "rust", None, None);
1810        assert!(
1811            result.is_ok(),
1812            "extract must not panic or error on valid source"
1813        );
1814        let output = result.unwrap();
1815        assert!(
1816            !output.calls.is_empty(),
1817            "extract must return call entries for source with function calls"
1818        );
1819    }
1820
1821    #[test]
1822    fn extract_calls_caps_arg_count_at_sixteen_hops() {
1823        // Regression test for #1251: verify deeply nested parenthesized arguments
1824        // (20 levels) do not cause a panic. The inner call `g()` is always at 1 hop
1825        // from its enclosing call_expression (function identifier is a direct child),
1826        // so arg_count is Some(0) -- the cap only fires when the captured node is
1827        // deeper in the AST than the direct function child of a call_expression.
1828        let src = r#"fn main() { f((((((((((((((((((((g())))))))))))))))))))); }"#;
1829        let result = SemanticExtractor::extract(src, "rust", None, None);
1830        assert!(
1831            result.is_ok(),
1832            "extract must succeed even with deeply nested parenthesized arguments"
1833        );
1834        let output = result.unwrap();
1835        let g_calls: Vec<&CallInfo> = output.calls.iter().filter(|c| c.callee == "g").collect();
1836        assert_eq!(
1837            g_calls.len(),
1838            1,
1839            "expected exactly one CallInfo with callee 'g', got {}",
1840            g_calls.len()
1841        );
1842        // arg_count is Some(0) because g() has 0 arguments; the >16 hop cap is
1843        // a parent-traversal guard on the captured node, not on the call nesting.
1844        assert_eq!(
1845            g_calls[0].arg_count,
1846            Some(0),
1847            "g() has 0 arguments, expected Some(0)"
1848        );
1849    }
1850}
1851
1852#[cfg(test)]
1853// Tests for Python language parsing
1854mod tests_python {
1855    use super::*;
1856
1857    #[test]
1858    fn test_python_relative_import() {
1859        // Arrange: relative import (from . import foo)
1860        let source = "from . import foo\n";
1861        // Act
1862        let result = SemanticExtractor::extract(source, "python", None, None).unwrap();
1863        // Assert: relative import should be captured
1864        let relative = result.imports.iter().find(|imp| imp.module.contains("."));
1865        assert!(
1866            relative.is_some(),
1867            "expected relative import in {:?}",
1868            result.imports
1869        );
1870    }
1871
1872    #[test]
1873    fn test_python_aliased_import() {
1874        // Arrange: aliased import (from os import path as p)
1875        // Note: tree-sitter-python extracts "path" (the original name), not the alias "p"
1876        let source = "from os import path as p\n";
1877        // Act
1878        let result = SemanticExtractor::extract(source, "python", None, None).unwrap();
1879        // Assert: "path" should be in items (alias is captured separately by aliased_import node)
1880        let path_import = result
1881            .imports
1882            .iter()
1883            .find(|imp| imp.module == "os" && imp.items.iter().any(|i| i == "path"));
1884        assert!(
1885            path_import.is_some(),
1886            "expected import 'path' from module 'os' in {:?}",
1887            result.imports
1888        );
1889    }
1890
1891    #[test]
1892    fn test_parse_no_timeout_when_none() {
1893        // Arrange: simple Rust source with no deadline
1894        let source = r#"fn hello() -> u32 { 42 }"#;
1895        // Act: extract with deadline=None (no timeout)
1896        let result = SemanticExtractor::extract(source, "rust", None, None);
1897        // Assert: should succeed normally
1898        assert!(result.is_ok(), "extract with deadline=None should succeed");
1899        let analysis = result.unwrap();
1900        assert!(
1901            analysis.functions.len() >= 1,
1902            "should find at least one function"
1903        );
1904    }
1905
1906    #[test]
1907    fn test_parse_timeout_triggers_error() {
1908        // Arrange: simple Rust source with a very short timeout (1 microsecond)
1909        let source = r#"fn hello() -> u32 { 42 }"#;
1910        // Act: extract with a very short timeout that will expire immediately
1911        let result = SemanticExtractor::extract(source, "rust", None, Some(1u64));
1912        // Assert: should return a Timeout error
1913        assert!(
1914            matches!(result, Err(ParserError::Timeout(_))),
1915            "expected Timeout error, got {:?}",
1916            result
1917        );
1918    }
1919}
1920
1921// Tests that do not require any language feature gate
1922#[cfg(test)]
1923mod tests_unsupported {
1924    use super::*;
1925
1926    #[test]
1927    fn test_element_extractor_unsupported_language() {
1928        // Arrange + Act
1929        let result = ElementExtractor::extract_with_depth("x = 1", "cobol");
1930        // Assert
1931        assert!(
1932            matches!(result, Err(ParserError::UnsupportedLanguage(ref lang)) if lang == "cobol"),
1933            "expected UnsupportedLanguage error, got {:?}",
1934            result
1935        );
1936    }
1937
1938    #[test]
1939    fn test_semantic_extractor_unsupported_language() {
1940        // Arrange + Act
1941        let result = SemanticExtractor::extract("x = 1", "cobol", None, None);
1942        // Assert
1943        assert!(
1944            matches!(result, Err(ParserError::UnsupportedLanguage(ref lang)) if lang == "cobol"),
1945            "expected UnsupportedLanguage error, got {:?}",
1946            result
1947        );
1948    }
1949}