Skip to main content

aft/
parser.rs

1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use streaming_iterator::StreamingIterator;
7use tree_sitter::{Language, Node, Parser, Query, QueryCursor, Tree};
8
9use crate::callgraph::resolve_module_path;
10use crate::error::AftError;
11use crate::symbols::{Range, Symbol, SymbolKind, SymbolMatch};
12
13const MAX_REEXPORT_DEPTH: usize = 10;
14
15// --- Query patterns embedded at compile time ---
16
17const TS_QUERY: &str = r#"
18;; function declarations
19(function_declaration
20  name: (identifier) @fn.name) @fn.def
21
22;; arrow functions assigned to const/let/var
23(lexical_declaration
24  (variable_declarator
25    name: (identifier) @arrow.name
26    value: (arrow_function) @arrow.body)) @arrow.def
27
28;; class declarations
29(class_declaration
30  name: (type_identifier) @class.name) @class.def
31
32;; method definitions inside classes
33(class_declaration
34  name: (type_identifier) @method.class_name
35  body: (class_body
36    (method_definition
37      name: (property_identifier) @method.name) @method.def))
38
39;; interface declarations
40(interface_declaration
41  name: (type_identifier) @interface.name) @interface.def
42
43;; enum declarations
44(enum_declaration
45  name: (identifier) @enum.name) @enum.def
46
47;; type alias declarations
48(type_alias_declaration
49  name: (type_identifier) @type_alias.name) @type_alias.def
50
51;; top-level const/let variable declarations
52(lexical_declaration
53  (variable_declarator
54    name: (identifier) @var.name)) @var.def
55
56;; export statement wrappers (top-level only)
57(export_statement) @export.stmt
58"#;
59
60const JS_QUERY: &str = r#"
61;; function declarations
62(function_declaration
63  name: (identifier) @fn.name) @fn.def
64
65;; arrow functions assigned to const/let/var
66(lexical_declaration
67  (variable_declarator
68    name: (identifier) @arrow.name
69    value: (arrow_function) @arrow.body)) @arrow.def
70
71;; class declarations
72(class_declaration
73  name: (identifier) @class.name) @class.def
74
75;; method definitions inside classes
76(class_declaration
77  name: (identifier) @method.class_name
78  body: (class_body
79    (method_definition
80      name: (property_identifier) @method.name) @method.def))
81
82;; top-level const/let variable declarations
83(lexical_declaration
84  (variable_declarator
85    name: (identifier) @var.name)) @var.def
86
87;; export statement wrappers (top-level only)
88(export_statement) @export.stmt
89"#;
90
91const PY_QUERY: &str = r#"
92;; function definitions (top-level and nested)
93(function_definition
94  name: (identifier) @fn.name) @fn.def
95
96;; class definitions
97(class_definition
98  name: (identifier) @class.name) @class.def
99
100;; decorated definitions (wraps function_definition or class_definition)
101(decorated_definition
102  (decorator) @dec.decorator) @dec.def
103"#;
104
105const RS_QUERY: &str = r#"
106;; free functions (with optional visibility)
107(function_item
108  name: (identifier) @fn.name) @fn.def
109
110;; struct items
111(struct_item
112  name: (type_identifier) @struct.name) @struct.def
113
114;; enum items
115(enum_item
116  name: (type_identifier) @enum.name) @enum.def
117
118;; trait items
119(trait_item
120  name: (type_identifier) @trait.name) @trait.def
121
122;; impl blocks — capture the whole block to find methods
123(impl_item) @impl.def
124
125;; visibility modifiers on any item
126(visibility_modifier) @vis.mod
127"#;
128
129const GO_QUERY: &str = r#"
130;; function declarations
131(function_declaration
132  name: (identifier) @fn.name) @fn.def
133
134;; method declarations (with receiver)
135(method_declaration
136  name: (field_identifier) @method.name) @method.def
137
138;; type declarations (struct and interface)
139(type_declaration
140  (type_spec
141    name: (type_identifier) @type.name
142    type: (_) @type.body)) @type.def
143"#;
144
145const C_QUERY: &str = r#"
146;; function definitions
147(function_definition
148  declarator: (function_declarator
149    declarator: (identifier) @fn.name)) @fn.def
150
151;; function declarations / prototypes
152(declaration
153  declarator: (function_declarator
154    declarator: (identifier) @fn.name)) @fn.def
155
156;; struct declarations
157(struct_specifier
158  name: (type_identifier) @struct.name
159  body: (field_declaration_list)) @struct.def
160
161;; enum declarations
162(enum_specifier
163  name: (type_identifier) @enum.name
164  body: (enumerator_list)) @enum.def
165
166;; typedef aliases
167(type_definition
168  declarator: (type_identifier) @type.name) @type.def
169
170;; macros
171(preproc_def
172  name: (identifier) @macro.name) @macro.def
173
174(preproc_function_def
175  name: (identifier) @macro.name) @macro.def
176"#;
177
178const CPP_QUERY: &str = r#"
179;; free function definitions
180(function_definition
181  declarator: (function_declarator
182    declarator: (identifier) @fn.name)) @fn.def
183
184;; free function declarations
185(declaration
186  declarator: (function_declarator
187    declarator: (identifier) @fn.name)) @fn.def
188
189;; inline method definitions / declarations inside class bodies
190(function_definition
191  declarator: (function_declarator
192    declarator: (field_identifier) @method.name)) @method.def
193
194(field_declaration
195  declarator: (function_declarator
196    declarator: (field_identifier) @method.name)) @method.def
197
198;; qualified functions / methods
199(function_definition
200  declarator: (function_declarator
201    declarator: (qualified_identifier
202      scope: (_) @qual.scope
203      name: (identifier) @qual.name))) @qual.def
204
205(declaration
206  declarator: (function_declarator
207    declarator: (qualified_identifier
208      scope: (_) @qual.scope
209      name: (identifier) @qual.name))) @qual.def
210
211;; class / struct / enum / namespace declarations
212(class_specifier
213  name: (_) @class.name) @class.def
214
215(struct_specifier
216  name: (_) @struct.name) @struct.def
217
218(enum_specifier
219  name: (_) @enum.name) @enum.def
220
221(namespace_definition
222  name: (_) @namespace.name) @namespace.def
223
224;; template declarations
225(template_declaration
226  (class_specifier
227    name: (_) @template.class.name) @template.class.item) @template.class.def
228
229(template_declaration
230  (struct_specifier
231    name: (_) @template.struct.name) @template.struct.item) @template.struct.def
232
233(template_declaration
234  (function_definition
235    declarator: (function_declarator
236      declarator: (identifier) @template.fn.name)) @template.fn.item) @template.fn.def
237
238(template_declaration
239  (function_definition
240    declarator: (function_declarator
241      declarator: (qualified_identifier
242        scope: (_) @template.qual.scope
243        name: (identifier) @template.qual.name))) @template.qual.item) @template.qual.def
244"#;
245
246const ZIG_QUERY: &str = r#"
247;; functions
248(function_declaration
249  name: (identifier) @fn.name) @fn.def
250
251;; container declarations bound to const names
252(variable_declaration
253  (identifier) @struct.name
254  "="
255  (struct_declaration) @struct.body) @struct.def
256
257(variable_declaration
258  (identifier) @enum.name
259  "="
260  (enum_declaration) @enum.body) @enum.def
261
262(variable_declaration
263  (identifier) @union.name
264  "="
265  (union_declaration) @union.body) @union.def
266
267;; const declarations
268(variable_declaration
269  (identifier) @const.name) @const.def
270
271;; tests
272(test_declaration
273  (string) @test.name) @test.def
274
275(test_declaration
276  (identifier) @test.name) @test.def
277"#;
278
279const CSHARP_QUERY: &str = r#"
280;; types
281(class_declaration
282  name: (identifier) @class.name) @class.def
283
284(interface_declaration
285  name: (identifier) @interface.name) @interface.def
286
287(struct_declaration
288  name: (identifier) @struct.name) @struct.def
289
290(enum_declaration
291  name: (identifier) @enum.name) @enum.def
292
293;; members
294(method_declaration
295  name: (identifier) @method.name) @method.def
296
297(property_declaration
298  name: (identifier) @property.name) @property.def
299
300;; namespaces
301(namespace_declaration
302  name: (_) @namespace.name) @namespace.def
303
304(file_scoped_namespace_declaration
305  name: (_) @namespace.name) @namespace.def
306"#;
307
308// --- Bash query ---
309
310const BASH_QUERY: &str = r#"
311;; function definitions (both `function foo()` and `foo()` styles)
312(function_definition
313  name: (word) @fn.name) @fn.def
314"#;
315
316/// Supported language identifier.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
318pub enum LangId {
319    TypeScript,
320    Tsx,
321    JavaScript,
322    Python,
323    Rust,
324    Go,
325    C,
326    Cpp,
327    Zig,
328    CSharp,
329    Bash,
330    Html,
331    Markdown,
332}
333
334/// Maps file extension to language identifier.
335pub fn detect_language(path: &Path) -> Option<LangId> {
336    let ext = path.extension()?.to_str()?;
337    match ext {
338        "ts" => Some(LangId::TypeScript),
339        "tsx" => Some(LangId::Tsx),
340        "js" | "jsx" => Some(LangId::JavaScript),
341        "py" => Some(LangId::Python),
342        "rs" => Some(LangId::Rust),
343        "go" => Some(LangId::Go),
344        "c" | "h" => Some(LangId::C),
345        "cc" | "cpp" | "cxx" | "hpp" | "hh" => Some(LangId::Cpp),
346        "zig" => Some(LangId::Zig),
347        "cs" => Some(LangId::CSharp),
348        "sh" | "bash" | "zsh" => Some(LangId::Bash),
349        "html" | "htm" => Some(LangId::Html),
350        "md" | "markdown" | "mdx" => Some(LangId::Markdown),
351        _ => None,
352    }
353}
354
355/// Returns the tree-sitter Language grammar for a given LangId.
356pub fn grammar_for(lang: LangId) -> Language {
357    match lang {
358        LangId::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
359        LangId::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
360        LangId::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
361        LangId::Python => tree_sitter_python::LANGUAGE.into(),
362        LangId::Rust => tree_sitter_rust::LANGUAGE.into(),
363        LangId::Go => tree_sitter_go::LANGUAGE.into(),
364        LangId::C => tree_sitter_c::LANGUAGE.into(),
365        LangId::Cpp => tree_sitter_cpp::LANGUAGE.into(),
366        LangId::Zig => tree_sitter_zig::LANGUAGE.into(),
367        LangId::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
368        LangId::Bash => tree_sitter_bash::LANGUAGE.into(),
369        LangId::Html => tree_sitter_html::LANGUAGE.into(),
370        LangId::Markdown => tree_sitter_md::LANGUAGE.into(),
371    }
372}
373
374/// Returns the query pattern string for a given LangId, if implemented.
375fn query_for(lang: LangId) -> Option<&'static str> {
376    match lang {
377        LangId::TypeScript | LangId::Tsx => Some(TS_QUERY),
378        LangId::JavaScript => Some(JS_QUERY),
379        LangId::Python => Some(PY_QUERY),
380        LangId::Rust => Some(RS_QUERY),
381        LangId::Go => Some(GO_QUERY),
382        LangId::C => Some(C_QUERY),
383        LangId::Cpp => Some(CPP_QUERY),
384        LangId::Zig => Some(ZIG_QUERY),
385        LangId::CSharp => Some(CSHARP_QUERY),
386        LangId::Bash => Some(BASH_QUERY),
387        LangId::Html => None, // HTML uses direct tree walking like Markdown
388        LangId::Markdown => None,
389    }
390}
391
392/// Cached parse result: mtime at parse time + the tree.
393struct CachedTree {
394    mtime: SystemTime,
395    tree: Tree,
396}
397
398/// Cached symbol extraction result: mtime at extraction time + symbols.
399#[derive(Clone)]
400struct CachedSymbols {
401    mtime: SystemTime,
402    symbols: Vec<Symbol>,
403}
404
405/// Shared symbol cache that can be pre-warmed in a background thread
406/// and merged into the main thread. Thread-safe for building, then
407/// transferred to the single-threaded main loop.
408#[derive(Clone, Default)]
409pub struct SymbolCache {
410    entries: HashMap<PathBuf, CachedSymbols>,
411}
412
413impl SymbolCache {
414    pub fn new() -> Self {
415        Self {
416            entries: HashMap::new(),
417        }
418    }
419
420    /// Insert pre-warmed symbols for a file.
421    pub fn insert(&mut self, path: PathBuf, mtime: SystemTime, symbols: Vec<Symbol>) {
422        self.entries.insert(path, CachedSymbols { mtime, symbols });
423    }
424
425    /// Merge another cache into this one (newer entries win by mtime).
426    pub fn merge(&mut self, other: SymbolCache) {
427        for (path, entry) in other.entries {
428            match self.entries.get(&path) {
429                Some(existing) if existing.mtime >= entry.mtime => {}
430                _ => {
431                    self.entries.insert(path, entry);
432                }
433            }
434        }
435    }
436
437    /// Number of cached entries.
438    pub fn len(&self) -> usize {
439        self.entries.len()
440    }
441}
442
443/// Core parsing engine. Handles language detection, parse tree caching,
444/// symbol table caching, and query pattern execution via tree-sitter.
445pub struct FileParser {
446    cache: HashMap<PathBuf, CachedTree>,
447    symbol_cache: HashMap<PathBuf, CachedSymbols>,
448    /// Shared pre-warmed cache from background indexing
449    warm_cache: Option<SymbolCache>,
450}
451
452impl FileParser {
453    /// Create a new `FileParser` with an empty parse cache.
454    pub fn new() -> Self {
455        Self {
456            cache: HashMap::new(),
457            symbol_cache: HashMap::new(),
458            warm_cache: None,
459        }
460    }
461
462    /// Attach a pre-warmed symbol cache from background indexing.
463    pub fn set_warm_cache(&mut self, cache: SymbolCache) {
464        self.warm_cache = Some(cache);
465    }
466
467    /// Number of entries in the local symbol cache.
468    pub fn symbol_cache_len(&self) -> usize {
469        self.symbol_cache.len()
470    }
471
472    /// Number of entries in the warm (pre-warmed) symbol cache.
473    pub fn warm_cache_len(&self) -> usize {
474        self.warm_cache.as_ref().map_or(0, |c| c.len())
475    }
476
477    /// Parse a file, returning the tree and detected language. Uses cache if
478    /// the file hasn't been modified since last parse.
479    pub fn parse(&mut self, path: &Path) -> Result<(&Tree, LangId), AftError> {
480        let lang = detect_language(path).ok_or_else(|| AftError::InvalidRequest {
481            message: format!(
482                "unsupported file extension: {}",
483                path.extension()
484                    .and_then(|e| e.to_str())
485                    .unwrap_or("<none>")
486            ),
487        })?;
488
489        let canon = path.to_path_buf();
490        let current_mtime = std::fs::metadata(path)
491            .and_then(|m| m.modified())
492            .map_err(|e| AftError::FileNotFound {
493                path: format!("{}: {}", path.display(), e),
494            })?;
495
496        // Check cache validity
497        let needs_reparse = match self.cache.get(&canon) {
498            Some(cached) => cached.mtime != current_mtime,
499            None => true,
500        };
501
502        if needs_reparse {
503            let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
504                path: format!("{}: {}", path.display(), e),
505            })?;
506
507            let grammar = grammar_for(lang);
508            let mut parser = Parser::new();
509            parser.set_language(&grammar).map_err(|e| {
510                log::error!("grammar init failed for {:?}: {}", lang, e);
511                AftError::ParseError {
512                    message: format!("grammar init failed for {:?}: {}", lang, e),
513                }
514            })?;
515
516            let tree = parser.parse(&source, None).ok_or_else(|| {
517                log::error!("parse failed for {}", path.display());
518                AftError::ParseError {
519                    message: format!("tree-sitter parse returned None for {}", path.display()),
520                }
521            })?;
522
523            self.cache.insert(
524                canon.clone(),
525                CachedTree {
526                    mtime: current_mtime,
527                    tree,
528                },
529            );
530        }
531
532        let cached = self.cache.get(&canon).ok_or_else(|| AftError::ParseError {
533            message: format!("parser cache missing entry for {}", path.display()),
534        })?;
535        Ok((&cached.tree, lang))
536    }
537
538    /// Like [`FileParser::parse`] but returns an owned `Tree` clone.
539    ///
540    /// Useful when the caller needs to hold the tree while also calling
541    /// other mutable methods on this parser.
542    pub fn parse_cloned(&mut self, path: &Path) -> Result<(Tree, LangId), AftError> {
543        let (tree, lang) = self.parse(path)?;
544        Ok((tree.clone(), lang))
545    }
546
547    /// Extract symbols from a file using language-specific query patterns.
548    /// Results are cached by `(path, mtime)` — subsequent calls for unchanged
549    /// files return the cached symbol table without re-parsing.
550    pub fn extract_symbols(&mut self, path: &Path) -> Result<Vec<Symbol>, AftError> {
551        let canon = path.to_path_buf();
552        let current_mtime = std::fs::metadata(path)
553            .and_then(|m| m.modified())
554            .map_err(|e| AftError::FileNotFound {
555                path: format!("{}: {}", path.display(), e),
556            })?;
557
558        // Return cached symbols if file hasn't changed (local cache first, then warm cache)
559        if let Some(cached) = self.symbol_cache.get(&canon) {
560            if cached.mtime == current_mtime {
561                return Ok(cached.symbols.clone());
562            }
563        }
564        if let Some(warm) = &self.warm_cache {
565            if let Some(cached) = warm.entries.get(&canon) {
566                if cached.mtime == current_mtime {
567                    // Promote to local cache for future lookups
568                    self.symbol_cache.insert(canon, cached.clone());
569                    return Ok(cached.symbols.clone());
570                }
571            }
572        }
573
574        let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
575            path: format!("{}: {}", path.display(), e),
576        })?;
577
578        let (tree, lang) = self.parse(path)?;
579        let root = tree.root_node();
580
581        // HTML and Markdown use direct tree walking, not query patterns
582        let symbols = if lang == LangId::Html {
583            extract_html_symbols(&source, &root)?
584        } else if lang == LangId::Markdown {
585            extract_md_symbols(&source, &root)?
586        } else {
587            let query_src = query_for(lang).ok_or_else(|| AftError::InvalidRequest {
588                message: format!("no query patterns implemented for {:?} yet", lang),
589            })?;
590
591            let grammar = grammar_for(lang);
592            let query = Query::new(&grammar, query_src).map_err(|e| {
593                log::error!("query compile failed for {:?}: {}", lang, e);
594                AftError::ParseError {
595                    message: format!("query compile error for {:?}: {}", lang, e),
596                }
597            })?;
598
599            match lang {
600                LangId::TypeScript | LangId::Tsx => extract_ts_symbols(&source, &root, &query)?,
601                LangId::JavaScript => extract_js_symbols(&source, &root, &query)?,
602                LangId::Python => extract_py_symbols(&source, &root, &query)?,
603                LangId::Rust => extract_rs_symbols(&source, &root, &query)?,
604                LangId::Go => extract_go_symbols(&source, &root, &query)?,
605                LangId::C => extract_c_symbols(&source, &root, &query)?,
606                LangId::Cpp => extract_cpp_symbols(&source, &root, &query)?,
607                LangId::Zig => extract_zig_symbols(&source, &root, &query)?,
608                LangId::CSharp => extract_csharp_symbols(&source, &root, &query)?,
609                LangId::Bash => extract_bash_symbols(&source, &root, &query)?,
610                LangId::Html | LangId::Markdown => vec![],
611            }
612        };
613
614        // Cache the result
615        self.symbol_cache.insert(
616            canon,
617            CachedSymbols {
618                mtime: current_mtime,
619                symbols: symbols.clone(),
620            },
621        );
622
623        Ok(symbols)
624    }
625
626    /// Invalidate cached symbols for a specific file (e.g., after an edit).
627    pub fn invalidate_symbols(&mut self, path: &Path) {
628        self.symbol_cache.remove(path);
629        self.cache.remove(path);
630    }
631}
632
633/// Build a Range from a tree-sitter Node.
634pub(crate) fn node_range(node: &Node) -> Range {
635    let start = node.start_position();
636    let end = node.end_position();
637    Range {
638        start_line: start.row as u32,
639        start_col: start.column as u32,
640        end_line: end.row as u32,
641        end_col: end.column as u32,
642    }
643}
644
645/// Build a Range from a tree-sitter Node, expanding upward to include
646/// preceding attributes, decorators, and doc comments that belong to the symbol.
647///
648/// This ensures that when agents edit/replace a symbol, they get the full
649/// declaration including `#[test]`, `#[derive(...)]`, `/// doc`, `@decorator`, etc.
650pub(crate) fn node_range_with_decorators(node: &Node, source: &str, lang: LangId) -> Range {
651    let mut range = node_range(node);
652
653    let mut current = *node;
654    while let Some(prev) = current.prev_sibling() {
655        let kind = prev.kind();
656        let should_include = match lang {
657            LangId::Rust => {
658                // Include #[...] attributes
659                kind == "attribute_item"
660                    // Include /// doc comments (but not regular // comments)
661                    || (kind == "line_comment"
662                        && node_text(source, &prev).starts_with("///"))
663                    // Include /** ... */ doc comments
664                    || (kind == "block_comment"
665                        && node_text(source, &prev).starts_with("/**"))
666            }
667            LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
668                // Include @decorator
669                kind == "decorator"
670                    // Include /** JSDoc */ comments
671                    || (kind == "comment"
672                        && node_text(source, &prev).starts_with("/**"))
673            }
674            LangId::Go | LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp | LangId::Bash => {
675                // Include doc comments only if immediately above (no blank line gap)
676                kind == "comment" && is_adjacent_line(&prev, &current, source)
677            }
678            LangId::Python => {
679                // Decorators are handled by decorated_definition capture
680                false
681            }
682            LangId::Html | LangId::Markdown => false,
683        };
684
685        if should_include {
686            range.start_line = prev.start_position().row as u32;
687            range.start_col = prev.start_position().column as u32;
688            current = prev;
689        } else {
690            break;
691        }
692    }
693
694    range
695}
696
697/// Check if two nodes are on adjacent lines (no blank line between them).
698fn is_adjacent_line(upper: &Node, lower: &Node, source: &str) -> bool {
699    let upper_end = upper.end_position().row;
700    let lower_start = lower.start_position().row;
701
702    if lower_start == 0 || lower_start <= upper_end {
703        return true;
704    }
705
706    // Check that there's no blank line between them
707    let lines: Vec<&str> = source.lines().collect();
708    for row in (upper_end + 1)..lower_start {
709        if row < lines.len() && lines[row].trim().is_empty() {
710            return false;
711        }
712    }
713    true
714}
715
716/// Extract the text of a node from source.
717pub(crate) fn node_text<'a>(source: &'a str, node: &Node) -> &'a str {
718    &source[node.byte_range()]
719}
720
721fn lexical_declaration_has_function_value(node: &Node) -> bool {
722    let mut cursor = node.walk();
723    if !cursor.goto_first_child() {
724        return false;
725    }
726
727    loop {
728        let child = cursor.node();
729        if matches!(
730            child.kind(),
731            "arrow_function" | "function_expression" | "generator_function"
732        ) {
733            return true;
734        }
735
736        if lexical_declaration_has_function_value(&child) {
737            return true;
738        }
739
740        if !cursor.goto_next_sibling() {
741            break;
742        }
743    }
744
745    false
746}
747
748/// Collect byte ranges of all export_statement nodes from query matches.
749fn collect_export_ranges(source: &str, root: &Node, query: &Query) -> Vec<std::ops::Range<usize>> {
750    let export_idx = query
751        .capture_names()
752        .iter()
753        .position(|n| *n == "export.stmt");
754    let export_idx = match export_idx {
755        Some(i) => i as u32,
756        None => return vec![],
757    };
758
759    let mut cursor = QueryCursor::new();
760    let mut ranges = Vec::new();
761    let mut matches = cursor.matches(query, *root, source.as_bytes());
762
763    while let Some(m) = {
764        matches.advance();
765        matches.get()
766    } {
767        for cap in m.captures {
768            if cap.index == export_idx {
769                ranges.push(cap.node.byte_range());
770            }
771        }
772    }
773    ranges
774}
775
776/// Check if a node's byte range is contained within any export statement.
777fn is_exported(node: &Node, export_ranges: &[std::ops::Range<usize>]) -> bool {
778    let r = node.byte_range();
779    export_ranges
780        .iter()
781        .any(|er| er.start <= r.start && r.end <= er.end)
782}
783
784/// Extract the first line of a node as its signature.
785fn extract_signature(source: &str, node: &Node) -> String {
786    let text = node_text(source, node);
787    let first_line = text.lines().next().unwrap_or(text);
788    // Trim trailing opening brace if present
789    let trimmed = first_line.trim_end();
790    let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
791    trimmed.to_string()
792}
793
794/// Extract symbols from TypeScript / TSX source.
795fn extract_ts_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
796    let lang = LangId::TypeScript;
797    let capture_names = query.capture_names();
798
799    let export_ranges = collect_export_ranges(source, root, query);
800
801    let mut symbols = Vec::new();
802    let mut cursor = QueryCursor::new();
803    let mut matches = cursor.matches(query, *root, source.as_bytes());
804
805    while let Some(m) = {
806        matches.advance();
807        matches.get()
808    } {
809        // Determine what kind of match this is by looking at capture names
810        let mut fn_name_node = None;
811        let mut fn_def_node = None;
812        let mut arrow_name_node = None;
813        let mut arrow_def_node = None;
814        let mut class_name_node = None;
815        let mut class_def_node = None;
816        let mut method_class_name_node = None;
817        let mut method_name_node = None;
818        let mut method_def_node = None;
819        let mut interface_name_node = None;
820        let mut interface_def_node = None;
821        let mut enum_name_node = None;
822        let mut enum_def_node = None;
823        let mut type_alias_name_node = None;
824        let mut type_alias_def_node = None;
825        let mut var_name_node = None;
826        let mut var_def_node = None;
827
828        for cap in m.captures {
829            let Some(&name) = capture_names.get(cap.index as usize) else {
830                continue;
831            };
832            match name {
833                "fn.name" => fn_name_node = Some(cap.node),
834                "fn.def" => fn_def_node = Some(cap.node),
835                "arrow.name" => arrow_name_node = Some(cap.node),
836                "arrow.def" => arrow_def_node = Some(cap.node),
837                "class.name" => class_name_node = Some(cap.node),
838                "class.def" => class_def_node = Some(cap.node),
839                "method.class_name" => method_class_name_node = Some(cap.node),
840                "method.name" => method_name_node = Some(cap.node),
841                "method.def" => method_def_node = Some(cap.node),
842                "interface.name" => interface_name_node = Some(cap.node),
843                "interface.def" => interface_def_node = Some(cap.node),
844                "enum.name" => enum_name_node = Some(cap.node),
845                "enum.def" => enum_def_node = Some(cap.node),
846                "type_alias.name" => type_alias_name_node = Some(cap.node),
847                "type_alias.def" => type_alias_def_node = Some(cap.node),
848                "var.name" => var_name_node = Some(cap.node),
849                "var.def" => var_def_node = Some(cap.node),
850                // var.value/var.decl removed — not needed
851                _ => {}
852            }
853        }
854
855        // Function declaration
856        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
857            symbols.push(Symbol {
858                name: node_text(source, &name_node).to_string(),
859                kind: SymbolKind::Function,
860                range: node_range_with_decorators(&def_node, source, lang),
861                signature: Some(extract_signature(source, &def_node)),
862                scope_chain: vec![],
863                exported: is_exported(&def_node, &export_ranges),
864                parent: None,
865            });
866        }
867
868        // Arrow function
869        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
870            symbols.push(Symbol {
871                name: node_text(source, &name_node).to_string(),
872                kind: SymbolKind::Function,
873                range: node_range_with_decorators(&def_node, source, lang),
874                signature: Some(extract_signature(source, &def_node)),
875                scope_chain: vec![],
876                exported: is_exported(&def_node, &export_ranges),
877                parent: None,
878            });
879        }
880
881        // Class declaration
882        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
883            symbols.push(Symbol {
884                name: node_text(source, &name_node).to_string(),
885                kind: SymbolKind::Class,
886                range: node_range_with_decorators(&def_node, source, lang),
887                signature: Some(extract_signature(source, &def_node)),
888                scope_chain: vec![],
889                exported: is_exported(&def_node, &export_ranges),
890                parent: None,
891            });
892        }
893
894        // Method definition
895        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
896            (method_class_name_node, method_name_node, method_def_node)
897        {
898            let class_name = node_text(source, &class_name_node).to_string();
899            symbols.push(Symbol {
900                name: node_text(source, &name_node).to_string(),
901                kind: SymbolKind::Method,
902                range: node_range_with_decorators(&def_node, source, lang),
903                signature: Some(extract_signature(source, &def_node)),
904                scope_chain: vec![class_name.clone()],
905                exported: false, // methods inherit export from class
906                parent: Some(class_name),
907            });
908        }
909
910        // Interface declaration
911        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
912            symbols.push(Symbol {
913                name: node_text(source, &name_node).to_string(),
914                kind: SymbolKind::Interface,
915                range: node_range_with_decorators(&def_node, source, lang),
916                signature: Some(extract_signature(source, &def_node)),
917                scope_chain: vec![],
918                exported: is_exported(&def_node, &export_ranges),
919                parent: None,
920            });
921        }
922
923        // Enum declaration
924        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
925            symbols.push(Symbol {
926                name: node_text(source, &name_node).to_string(),
927                kind: SymbolKind::Enum,
928                range: node_range_with_decorators(&def_node, source, lang),
929                signature: Some(extract_signature(source, &def_node)),
930                scope_chain: vec![],
931                exported: is_exported(&def_node, &export_ranges),
932                parent: None,
933            });
934        }
935
936        // Type alias
937        if let (Some(name_node), Some(def_node)) = (type_alias_name_node, type_alias_def_node) {
938            symbols.push(Symbol {
939                name: node_text(source, &name_node).to_string(),
940                kind: SymbolKind::TypeAlias,
941                range: node_range_with_decorators(&def_node, source, lang),
942                signature: Some(extract_signature(source, &def_node)),
943                scope_chain: vec![],
944                exported: is_exported(&def_node, &export_ranges),
945                parent: None,
946            });
947        }
948
949        // Top-level const/let variable declaration (not arrow functions — those are handled above)
950        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
951            // Only include module-scope variables (parent is program/export_statement, not inside a function)
952            let is_top_level = def_node
953                .parent()
954                .map(|p| p.kind() == "program" || p.kind() == "export_statement")
955                .unwrap_or(false);
956            let is_function_like = lexical_declaration_has_function_value(&def_node);
957            let name = node_text(source, &name_node).to_string();
958            let already_captured = symbols.iter().any(|s| s.name == name);
959            if is_top_level && !is_function_like && !already_captured {
960                symbols.push(Symbol {
961                    name,
962                    kind: SymbolKind::Variable,
963                    range: node_range_with_decorators(&def_node, source, lang),
964                    signature: Some(extract_signature(source, &def_node)),
965                    scope_chain: vec![],
966                    exported: is_exported(&def_node, &export_ranges),
967                    parent: None,
968                });
969            }
970        }
971    }
972
973    // Deduplicate: methods can appear as both class and method captures
974    dedup_symbols(&mut symbols);
975    Ok(symbols)
976}
977
978/// Extract symbols from JavaScript source.
979fn extract_js_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
980    let lang = LangId::JavaScript;
981    let capture_names = query.capture_names();
982
983    let export_ranges = collect_export_ranges(source, root, query);
984
985    let mut symbols = Vec::new();
986    let mut cursor = QueryCursor::new();
987    let mut matches = cursor.matches(query, *root, source.as_bytes());
988
989    while let Some(m) = {
990        matches.advance();
991        matches.get()
992    } {
993        let mut fn_name_node = None;
994        let mut fn_def_node = None;
995        let mut arrow_name_node = None;
996        let mut arrow_def_node = None;
997        let mut class_name_node = None;
998        let mut class_def_node = None;
999        let mut method_class_name_node = None;
1000        let mut method_name_node = None;
1001        let mut method_def_node = None;
1002
1003        for cap in m.captures {
1004            let Some(&name) = capture_names.get(cap.index as usize) else {
1005                continue;
1006            };
1007            match name {
1008                "fn.name" => fn_name_node = Some(cap.node),
1009                "fn.def" => fn_def_node = Some(cap.node),
1010                "arrow.name" => arrow_name_node = Some(cap.node),
1011                "arrow.def" => arrow_def_node = Some(cap.node),
1012                "class.name" => class_name_node = Some(cap.node),
1013                "class.def" => class_def_node = Some(cap.node),
1014                "method.class_name" => method_class_name_node = Some(cap.node),
1015                "method.name" => method_name_node = Some(cap.node),
1016                "method.def" => method_def_node = Some(cap.node),
1017                _ => {}
1018            }
1019        }
1020
1021        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1022            symbols.push(Symbol {
1023                name: node_text(source, &name_node).to_string(),
1024                kind: SymbolKind::Function,
1025                range: node_range_with_decorators(&def_node, source, lang),
1026                signature: Some(extract_signature(source, &def_node)),
1027                scope_chain: vec![],
1028                exported: is_exported(&def_node, &export_ranges),
1029                parent: None,
1030            });
1031        }
1032
1033        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
1034            symbols.push(Symbol {
1035                name: node_text(source, &name_node).to_string(),
1036                kind: SymbolKind::Function,
1037                range: node_range_with_decorators(&def_node, source, lang),
1038                signature: Some(extract_signature(source, &def_node)),
1039                scope_chain: vec![],
1040                exported: is_exported(&def_node, &export_ranges),
1041                parent: None,
1042            });
1043        }
1044
1045        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
1046            symbols.push(Symbol {
1047                name: node_text(source, &name_node).to_string(),
1048                kind: SymbolKind::Class,
1049                range: node_range_with_decorators(&def_node, source, lang),
1050                signature: Some(extract_signature(source, &def_node)),
1051                scope_chain: vec![],
1052                exported: is_exported(&def_node, &export_ranges),
1053                parent: None,
1054            });
1055        }
1056
1057        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
1058            (method_class_name_node, method_name_node, method_def_node)
1059        {
1060            let class_name = node_text(source, &class_name_node).to_string();
1061            symbols.push(Symbol {
1062                name: node_text(source, &name_node).to_string(),
1063                kind: SymbolKind::Method,
1064                range: node_range_with_decorators(&def_node, source, lang),
1065                signature: Some(extract_signature(source, &def_node)),
1066                scope_chain: vec![class_name.clone()],
1067                exported: false,
1068                parent: Some(class_name),
1069            });
1070        }
1071    }
1072
1073    dedup_symbols(&mut symbols);
1074    Ok(symbols)
1075}
1076
1077/// Walk parent nodes to build a scope chain for Python symbols.
1078/// A function inside `class_definition > block` gets the class name in its scope.
1079fn py_scope_chain(node: &Node, source: &str) -> Vec<String> {
1080    let mut chain = Vec::new();
1081    let mut current = node.parent();
1082    while let Some(parent) = current {
1083        if parent.kind() == "class_definition" {
1084            if let Some(name_node) = parent.child_by_field_name("name") {
1085                chain.push(node_text(source, &name_node).to_string());
1086            }
1087        }
1088        current = parent.parent();
1089    }
1090    chain.reverse();
1091    chain
1092}
1093
1094/// Extract symbols from Python source.
1095fn extract_py_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1096    let lang = LangId::Python;
1097    let capture_names = query.capture_names();
1098
1099    let mut symbols = Vec::new();
1100    let mut cursor = QueryCursor::new();
1101    let mut matches = cursor.matches(query, *root, source.as_bytes());
1102
1103    // Track decorated definitions to avoid double-counting
1104    let mut decorated_fn_lines = std::collections::HashSet::new();
1105
1106    // First pass: collect decorated definition info
1107    {
1108        let mut cursor2 = QueryCursor::new();
1109        let mut matches2 = cursor2.matches(query, *root, source.as_bytes());
1110        while let Some(m) = {
1111            matches2.advance();
1112            matches2.get()
1113        } {
1114            let mut dec_def_node = None;
1115            let mut dec_decorator_node = None;
1116
1117            for cap in m.captures {
1118                let Some(&name) = capture_names.get(cap.index as usize) else {
1119                    continue;
1120                };
1121                match name {
1122                    "dec.def" => dec_def_node = Some(cap.node),
1123                    "dec.decorator" => dec_decorator_node = Some(cap.node),
1124                    _ => {}
1125                }
1126            }
1127
1128            if let (Some(def_node), Some(_dec_node)) = (dec_def_node, dec_decorator_node) {
1129                // Find the inner function_definition or class_definition
1130                let mut child_cursor = def_node.walk();
1131                if child_cursor.goto_first_child() {
1132                    loop {
1133                        let child = child_cursor.node();
1134                        if child.kind() == "function_definition"
1135                            || child.kind() == "class_definition"
1136                        {
1137                            decorated_fn_lines.insert(child.start_position().row);
1138                        }
1139                        if !child_cursor.goto_next_sibling() {
1140                            break;
1141                        }
1142                    }
1143                }
1144            }
1145        }
1146    }
1147
1148    while let Some(m) = {
1149        matches.advance();
1150        matches.get()
1151    } {
1152        let mut fn_name_node = None;
1153        let mut fn_def_node = None;
1154        let mut class_name_node = None;
1155        let mut class_def_node = None;
1156
1157        for cap in m.captures {
1158            let Some(&name) = capture_names.get(cap.index as usize) else {
1159                continue;
1160            };
1161            match name {
1162                "fn.name" => fn_name_node = Some(cap.node),
1163                "fn.def" => fn_def_node = Some(cap.node),
1164                "class.name" => class_name_node = Some(cap.node),
1165                "class.def" => class_def_node = Some(cap.node),
1166                _ => {}
1167            }
1168        }
1169
1170        // Function definition
1171        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1172            let scope = py_scope_chain(&def_node, source);
1173            let is_method = !scope.is_empty();
1174            let name = node_text(source, &name_node).to_string();
1175            // Skip __init__ and other dunders as separate symbols — they're methods
1176            let kind = if is_method {
1177                SymbolKind::Method
1178            } else {
1179                SymbolKind::Function
1180            };
1181
1182            // Build signature — include decorator if this is a decorated function
1183            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
1184                // Find the decorated_definition parent to get decorator text
1185                let mut sig_parts = Vec::new();
1186                let mut parent = def_node.parent();
1187                while let Some(p) = parent {
1188                    if p.kind() == "decorated_definition" {
1189                        // Get decorator lines
1190                        let mut dc = p.walk();
1191                        if dc.goto_first_child() {
1192                            loop {
1193                                if dc.node().kind() == "decorator" {
1194                                    sig_parts.push(node_text(source, &dc.node()).to_string());
1195                                }
1196                                if !dc.goto_next_sibling() {
1197                                    break;
1198                                }
1199                            }
1200                        }
1201                        break;
1202                    }
1203                    parent = p.parent();
1204                }
1205                sig_parts.push(extract_signature(source, &def_node));
1206                Some(sig_parts.join("\n"))
1207            } else {
1208                Some(extract_signature(source, &def_node))
1209            };
1210
1211            symbols.push(Symbol {
1212                name,
1213                kind,
1214                range: node_range_with_decorators(&def_node, source, lang),
1215                signature: sig,
1216                scope_chain: scope.clone(),
1217                exported: false, // Python has no export concept
1218                parent: scope.last().cloned(),
1219            });
1220        }
1221
1222        // Class definition
1223        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
1224            let scope = py_scope_chain(&def_node, source);
1225
1226            // Build signature — include decorator if decorated
1227            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
1228                let mut sig_parts = Vec::new();
1229                let mut parent = def_node.parent();
1230                while let Some(p) = parent {
1231                    if p.kind() == "decorated_definition" {
1232                        let mut dc = p.walk();
1233                        if dc.goto_first_child() {
1234                            loop {
1235                                if dc.node().kind() == "decorator" {
1236                                    sig_parts.push(node_text(source, &dc.node()).to_string());
1237                                }
1238                                if !dc.goto_next_sibling() {
1239                                    break;
1240                                }
1241                            }
1242                        }
1243                        break;
1244                    }
1245                    parent = p.parent();
1246                }
1247                sig_parts.push(extract_signature(source, &def_node));
1248                Some(sig_parts.join("\n"))
1249            } else {
1250                Some(extract_signature(source, &def_node))
1251            };
1252
1253            symbols.push(Symbol {
1254                name: node_text(source, &name_node).to_string(),
1255                kind: SymbolKind::Class,
1256                range: node_range_with_decorators(&def_node, source, lang),
1257                signature: sig,
1258                scope_chain: scope.clone(),
1259                exported: false,
1260                parent: scope.last().cloned(),
1261            });
1262        }
1263    }
1264
1265    dedup_symbols(&mut symbols);
1266    Ok(symbols)
1267}
1268
1269/// Extract symbols from Rust source.
1270/// Handles: free functions, struct, enum, trait (as Interface), impl methods with scope chains.
1271fn extract_rs_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1272    let lang = LangId::Rust;
1273    let capture_names = query.capture_names();
1274
1275    // Collect all visibility_modifier byte ranges first
1276    let mut vis_ranges: Vec<std::ops::Range<usize>> = Vec::new();
1277    {
1278        let vis_idx = capture_names.iter().position(|n| *n == "vis.mod");
1279        if let Some(idx) = vis_idx {
1280            let idx = idx as u32;
1281            let mut cursor = QueryCursor::new();
1282            let mut matches = cursor.matches(query, *root, source.as_bytes());
1283            while let Some(m) = {
1284                matches.advance();
1285                matches.get()
1286            } {
1287                for cap in m.captures {
1288                    if cap.index == idx {
1289                        vis_ranges.push(cap.node.byte_range());
1290                    }
1291                }
1292            }
1293        }
1294    }
1295
1296    let is_pub = |node: &Node| -> bool {
1297        // Check if the node has a visibility_modifier as a direct child
1298        let mut child_cursor = node.walk();
1299        if child_cursor.goto_first_child() {
1300            loop {
1301                if child_cursor.node().kind() == "visibility_modifier" {
1302                    return true;
1303                }
1304                if !child_cursor.goto_next_sibling() {
1305                    break;
1306                }
1307            }
1308        }
1309        false
1310    };
1311
1312    let mut symbols = Vec::new();
1313    let mut cursor = QueryCursor::new();
1314    let mut matches = cursor.matches(query, *root, source.as_bytes());
1315
1316    while let Some(m) = {
1317        matches.advance();
1318        matches.get()
1319    } {
1320        let mut fn_name_node = None;
1321        let mut fn_def_node = None;
1322        let mut struct_name_node = None;
1323        let mut struct_def_node = None;
1324        let mut enum_name_node = None;
1325        let mut enum_def_node = None;
1326        let mut trait_name_node = None;
1327        let mut trait_def_node = None;
1328        let mut impl_def_node = None;
1329
1330        for cap in m.captures {
1331            let Some(&name) = capture_names.get(cap.index as usize) else {
1332                continue;
1333            };
1334            match name {
1335                "fn.name" => fn_name_node = Some(cap.node),
1336                "fn.def" => fn_def_node = Some(cap.node),
1337                "struct.name" => struct_name_node = Some(cap.node),
1338                "struct.def" => struct_def_node = Some(cap.node),
1339                "enum.name" => enum_name_node = Some(cap.node),
1340                "enum.def" => enum_def_node = Some(cap.node),
1341                "trait.name" => trait_name_node = Some(cap.node),
1342                "trait.def" => trait_def_node = Some(cap.node),
1343                "impl.def" => impl_def_node = Some(cap.node),
1344                _ => {}
1345            }
1346        }
1347
1348        // Free function (not inside impl block — check parent)
1349        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1350            let parent = def_node.parent();
1351            let in_impl = parent
1352                .map(|p| p.kind() == "declaration_list")
1353                .unwrap_or(false);
1354            if !in_impl {
1355                symbols.push(Symbol {
1356                    name: node_text(source, &name_node).to_string(),
1357                    kind: SymbolKind::Function,
1358                    range: node_range_with_decorators(&def_node, source, lang),
1359                    signature: Some(extract_signature(source, &def_node)),
1360                    scope_chain: vec![],
1361                    exported: is_pub(&def_node),
1362                    parent: None,
1363                });
1364            }
1365        }
1366
1367        // Struct
1368        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
1369            symbols.push(Symbol {
1370                name: node_text(source, &name_node).to_string(),
1371                kind: SymbolKind::Struct,
1372                range: node_range_with_decorators(&def_node, source, lang),
1373                signature: Some(extract_signature(source, &def_node)),
1374                scope_chain: vec![],
1375                exported: is_pub(&def_node),
1376                parent: None,
1377            });
1378        }
1379
1380        // Enum
1381        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
1382            symbols.push(Symbol {
1383                name: node_text(source, &name_node).to_string(),
1384                kind: SymbolKind::Enum,
1385                range: node_range_with_decorators(&def_node, source, lang),
1386                signature: Some(extract_signature(source, &def_node)),
1387                scope_chain: vec![],
1388                exported: is_pub(&def_node),
1389                parent: None,
1390            });
1391        }
1392
1393        // Trait (mapped to Interface kind)
1394        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
1395            symbols.push(Symbol {
1396                name: node_text(source, &name_node).to_string(),
1397                kind: SymbolKind::Interface,
1398                range: node_range_with_decorators(&def_node, source, lang),
1399                signature: Some(extract_signature(source, &def_node)),
1400                scope_chain: vec![],
1401                exported: is_pub(&def_node),
1402                parent: None,
1403            });
1404        }
1405
1406        // Impl block — extract methods from inside
1407        if let Some(impl_node) = impl_def_node {
1408            // Find the type name(s) from the impl
1409            // `impl TypeName { ... }` → scope = ["TypeName"]
1410            // `impl Trait for TypeName { ... }` → scope = ["Trait for TypeName"]
1411            let mut type_names: Vec<String> = Vec::new();
1412            let mut child_cursor = impl_node.walk();
1413            if child_cursor.goto_first_child() {
1414                loop {
1415                    let child = child_cursor.node();
1416                    if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1417                        type_names.push(node_text(source, &child).to_string());
1418                    }
1419                    if !child_cursor.goto_next_sibling() {
1420                        break;
1421                    }
1422                }
1423            }
1424
1425            let scope_name = if type_names.len() >= 2 {
1426                // impl Trait for Type
1427                format!("{} for {}", type_names[0], type_names[1])
1428            } else if type_names.len() == 1 {
1429                type_names[0].clone()
1430            } else {
1431                String::new()
1432            };
1433
1434            let parent_name = type_names.last().cloned().unwrap_or_default();
1435
1436            // Find declaration_list and extract function_items
1437            let mut child_cursor = impl_node.walk();
1438            if child_cursor.goto_first_child() {
1439                loop {
1440                    let child = child_cursor.node();
1441                    if child.kind() == "declaration_list" {
1442                        let mut fn_cursor = child.walk();
1443                        if fn_cursor.goto_first_child() {
1444                            loop {
1445                                let fn_node = fn_cursor.node();
1446                                if fn_node.kind() == "function_item" {
1447                                    if let Some(name_node) = fn_node.child_by_field_name("name") {
1448                                        symbols.push(Symbol {
1449                                            name: node_text(source, &name_node).to_string(),
1450                                            kind: SymbolKind::Method,
1451                                            range: node_range_with_decorators(
1452                                                &fn_node, source, lang,
1453                                            ),
1454                                            signature: Some(extract_signature(source, &fn_node)),
1455                                            scope_chain: if scope_name.is_empty() {
1456                                                vec![]
1457                                            } else {
1458                                                vec![scope_name.clone()]
1459                                            },
1460                                            exported: is_pub(&fn_node),
1461                                            parent: if parent_name.is_empty() {
1462                                                None
1463                                            } else {
1464                                                Some(parent_name.clone())
1465                                            },
1466                                        });
1467                                    }
1468                                }
1469                                if !fn_cursor.goto_next_sibling() {
1470                                    break;
1471                                }
1472                            }
1473                        }
1474                    }
1475                    if !child_cursor.goto_next_sibling() {
1476                        break;
1477                    }
1478                }
1479            }
1480        }
1481    }
1482
1483    dedup_symbols(&mut symbols);
1484    Ok(symbols)
1485}
1486
1487/// Extract symbols from Go source.
1488/// Handles: functions, methods (with receiver scope chain), struct/interface types,
1489/// uppercase-first-letter export detection.
1490fn extract_go_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1491    let lang = LangId::Go;
1492    let capture_names = query.capture_names();
1493
1494    let is_go_exported = |name: &str| -> bool {
1495        name.chars()
1496            .next()
1497            .map(|c| c.is_uppercase())
1498            .unwrap_or(false)
1499    };
1500
1501    let mut symbols = Vec::new();
1502    let mut cursor = QueryCursor::new();
1503    let mut matches = cursor.matches(query, *root, source.as_bytes());
1504
1505    while let Some(m) = {
1506        matches.advance();
1507        matches.get()
1508    } {
1509        let mut fn_name_node = None;
1510        let mut fn_def_node = None;
1511        let mut method_name_node = None;
1512        let mut method_def_node = None;
1513        let mut type_name_node = None;
1514        let mut type_body_node = None;
1515        let mut type_def_node = None;
1516
1517        for cap in m.captures {
1518            let Some(&name) = capture_names.get(cap.index as usize) else {
1519                continue;
1520            };
1521            match name {
1522                "fn.name" => fn_name_node = Some(cap.node),
1523                "fn.def" => fn_def_node = Some(cap.node),
1524                "method.name" => method_name_node = Some(cap.node),
1525                "method.def" => method_def_node = Some(cap.node),
1526                "type.name" => type_name_node = Some(cap.node),
1527                "type.body" => type_body_node = Some(cap.node),
1528                "type.def" => type_def_node = Some(cap.node),
1529                _ => {}
1530            }
1531        }
1532
1533        // Function declaration
1534        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1535            let name = node_text(source, &name_node).to_string();
1536            symbols.push(Symbol {
1537                exported: is_go_exported(&name),
1538                name,
1539                kind: SymbolKind::Function,
1540                range: node_range_with_decorators(&def_node, source, lang),
1541                signature: Some(extract_signature(source, &def_node)),
1542                scope_chain: vec![],
1543                parent: None,
1544            });
1545        }
1546
1547        // Method declaration (with receiver)
1548        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
1549            let name = node_text(source, &name_node).to_string();
1550
1551            // Extract receiver type from the first parameter_list
1552            let receiver_type = extract_go_receiver_type(&def_node, source);
1553            let scope_chain = if let Some(ref rt) = receiver_type {
1554                vec![rt.clone()]
1555            } else {
1556                vec![]
1557            };
1558
1559            symbols.push(Symbol {
1560                exported: is_go_exported(&name),
1561                name,
1562                kind: SymbolKind::Method,
1563                range: node_range_with_decorators(&def_node, source, lang),
1564                signature: Some(extract_signature(source, &def_node)),
1565                scope_chain,
1566                parent: receiver_type,
1567            });
1568        }
1569
1570        // Type declarations (struct or interface)
1571        if let (Some(name_node), Some(body_node), Some(def_node)) =
1572            (type_name_node, type_body_node, type_def_node)
1573        {
1574            let name = node_text(source, &name_node).to_string();
1575            let kind = match body_node.kind() {
1576                "struct_type" => SymbolKind::Struct,
1577                "interface_type" => SymbolKind::Interface,
1578                _ => SymbolKind::TypeAlias,
1579            };
1580
1581            symbols.push(Symbol {
1582                exported: is_go_exported(&name),
1583                name,
1584                kind,
1585                range: node_range_with_decorators(&def_node, source, lang),
1586                signature: Some(extract_signature(source, &def_node)),
1587                scope_chain: vec![],
1588                parent: None,
1589            });
1590        }
1591    }
1592
1593    dedup_symbols(&mut symbols);
1594    Ok(symbols)
1595}
1596
1597/// Extract the receiver type from a Go method_declaration node.
1598/// e.g. `func (m *MyStruct) String()` → Some("MyStruct")
1599fn extract_go_receiver_type(method_node: &Node, source: &str) -> Option<String> {
1600    // The first parameter_list is the receiver
1601    let mut child_cursor = method_node.walk();
1602    if child_cursor.goto_first_child() {
1603        loop {
1604            let child = child_cursor.node();
1605            if child.kind() == "parameter_list" {
1606                // Walk into parameter_list to find type_identifier
1607                return find_type_identifier_recursive(&child, source);
1608            }
1609            if !child_cursor.goto_next_sibling() {
1610                break;
1611            }
1612        }
1613    }
1614    None
1615}
1616
1617fn split_scope_text(text: &str, separator: &str) -> Vec<String> {
1618    text.split(separator)
1619        .map(str::trim)
1620        .filter(|segment| !segment.is_empty())
1621        .map(ToString::to_string)
1622        .collect()
1623}
1624
1625fn last_scope_segment(text: &str, separator: &str) -> String {
1626    split_scope_text(text, separator)
1627        .pop()
1628        .unwrap_or_else(|| text.trim().to_string())
1629}
1630
1631fn zig_container_scope_chain(node: &Node, source: &str) -> Vec<String> {
1632    let mut chain = Vec::new();
1633    let mut current = node.parent();
1634
1635    while let Some(parent) = current {
1636        if matches!(
1637            parent.kind(),
1638            "struct_declaration" | "enum_declaration" | "union_declaration" | "opaque_declaration"
1639        ) {
1640            if let Some(container) = parent.parent() {
1641                if container.kind() == "variable_declaration" {
1642                    let mut cursor = container.walk();
1643                    if cursor.goto_first_child() {
1644                        loop {
1645                            let child = cursor.node();
1646                            if child.kind() == "identifier" {
1647                                chain.push(node_text(source, &child).to_string());
1648                                break;
1649                            }
1650                            if !cursor.goto_next_sibling() {
1651                                break;
1652                            }
1653                        }
1654                    }
1655                }
1656            }
1657        }
1658        current = parent.parent();
1659    }
1660
1661    chain.reverse();
1662    chain
1663}
1664
1665fn csharp_scope_chain(node: &Node, source: &str) -> Vec<String> {
1666    let mut chain = Vec::new();
1667    let mut current = node.parent();
1668
1669    while let Some(parent) = current {
1670        match parent.kind() {
1671            "namespace_declaration" | "file_scoped_namespace_declaration" => {
1672                if let Some(name_node) = parent.child_by_field_name("name") {
1673                    chain.push(node_text(source, &name_node).to_string());
1674                }
1675            }
1676            "class_declaration"
1677            | "interface_declaration"
1678            | "struct_declaration"
1679            | "record_declaration" => {
1680                if let Some(name_node) = parent.child_by_field_name("name") {
1681                    chain.push(node_text(source, &name_node).to_string());
1682                }
1683            }
1684            _ => {}
1685        }
1686        current = parent.parent();
1687    }
1688
1689    chain.reverse();
1690    chain
1691}
1692
1693fn cpp_parent_scope_chain(node: &Node, source: &str) -> Vec<String> {
1694    let mut chain = Vec::new();
1695    let mut current = node.parent();
1696
1697    while let Some(parent) = current {
1698        match parent.kind() {
1699            "namespace_definition" => {
1700                if let Some(name_node) = parent.child_by_field_name("name") {
1701                    chain.push(node_text(source, &name_node).to_string());
1702                }
1703            }
1704            "class_specifier" | "struct_specifier" => {
1705                if let Some(name_node) = parent.child_by_field_name("name") {
1706                    chain.push(last_scope_segment(node_text(source, &name_node), "::"));
1707                }
1708            }
1709            _ => {}
1710        }
1711        current = parent.parent();
1712    }
1713
1714    chain.reverse();
1715    chain
1716}
1717
1718fn template_signature(source: &str, template_node: &Node, item_node: &Node) -> String {
1719    format!(
1720        "{}\n{}",
1721        extract_signature(source, template_node),
1722        extract_signature(source, item_node)
1723    )
1724}
1725
1726/// Extract symbols from C source.
1727fn extract_c_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1728    let lang = LangId::C;
1729    let capture_names = query.capture_names();
1730
1731    let mut symbols = Vec::new();
1732    let mut cursor = QueryCursor::new();
1733    let mut matches = cursor.matches(query, *root, source.as_bytes());
1734
1735    while let Some(m) = {
1736        matches.advance();
1737        matches.get()
1738    } {
1739        let mut fn_name_node = None;
1740        let mut fn_def_node = None;
1741        let mut struct_name_node = None;
1742        let mut struct_def_node = None;
1743        let mut enum_name_node = None;
1744        let mut enum_def_node = None;
1745        let mut type_name_node = None;
1746        let mut type_def_node = None;
1747        let mut macro_name_node = None;
1748        let mut macro_def_node = None;
1749
1750        for cap in m.captures {
1751            let Some(&name) = capture_names.get(cap.index as usize) else {
1752                continue;
1753            };
1754            match name {
1755                "fn.name" => fn_name_node = Some(cap.node),
1756                "fn.def" => fn_def_node = Some(cap.node),
1757                "struct.name" => struct_name_node = Some(cap.node),
1758                "struct.def" => struct_def_node = Some(cap.node),
1759                "enum.name" => enum_name_node = Some(cap.node),
1760                "enum.def" => enum_def_node = Some(cap.node),
1761                "type.name" => type_name_node = Some(cap.node),
1762                "type.def" => type_def_node = Some(cap.node),
1763                "macro.name" => macro_name_node = Some(cap.node),
1764                "macro.def" => macro_def_node = Some(cap.node),
1765                _ => {}
1766            }
1767        }
1768
1769        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1770            symbols.push(Symbol {
1771                name: node_text(source, &name_node).to_string(),
1772                kind: SymbolKind::Function,
1773                range: node_range_with_decorators(&def_node, source, lang),
1774                signature: Some(extract_signature(source, &def_node)),
1775                scope_chain: vec![],
1776                exported: false,
1777                parent: None,
1778            });
1779        }
1780
1781        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
1782            symbols.push(Symbol {
1783                name: node_text(source, &name_node).to_string(),
1784                kind: SymbolKind::Struct,
1785                range: node_range_with_decorators(&def_node, source, lang),
1786                signature: Some(extract_signature(source, &def_node)),
1787                scope_chain: vec![],
1788                exported: false,
1789                parent: None,
1790            });
1791        }
1792
1793        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
1794            symbols.push(Symbol {
1795                name: node_text(source, &name_node).to_string(),
1796                kind: SymbolKind::Enum,
1797                range: node_range_with_decorators(&def_node, source, lang),
1798                signature: Some(extract_signature(source, &def_node)),
1799                scope_chain: vec![],
1800                exported: false,
1801                parent: None,
1802            });
1803        }
1804
1805        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
1806            symbols.push(Symbol {
1807                name: node_text(source, &name_node).to_string(),
1808                kind: SymbolKind::TypeAlias,
1809                range: node_range_with_decorators(&def_node, source, lang),
1810                signature: Some(extract_signature(source, &def_node)),
1811                scope_chain: vec![],
1812                exported: false,
1813                parent: None,
1814            });
1815        }
1816
1817        if let (Some(name_node), Some(def_node)) = (macro_name_node, macro_def_node) {
1818            symbols.push(Symbol {
1819                name: node_text(source, &name_node).to_string(),
1820                kind: SymbolKind::Variable,
1821                range: node_range(&def_node),
1822                signature: Some(extract_signature(source, &def_node)),
1823                scope_chain: vec![],
1824                exported: false,
1825                parent: None,
1826            });
1827        }
1828    }
1829
1830    dedup_symbols(&mut symbols);
1831    Ok(symbols)
1832}
1833
1834/// Extract symbols from C++ source.
1835fn extract_cpp_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1836    let lang = LangId::Cpp;
1837    let capture_names = query.capture_names();
1838
1839    let mut type_names = HashSet::new();
1840    {
1841        let mut cursor = QueryCursor::new();
1842        let mut matches = cursor.matches(query, *root, source.as_bytes());
1843        while let Some(m) = {
1844            matches.advance();
1845            matches.get()
1846        } {
1847            for cap in m.captures {
1848                let Some(&name) = capture_names.get(cap.index as usize) else {
1849                    continue;
1850                };
1851                match name {
1852                    "class.name"
1853                    | "struct.name"
1854                    | "template.class.name"
1855                    | "template.struct.name" => {
1856                        type_names.insert(last_scope_segment(node_text(source, &cap.node), "::"));
1857                    }
1858                    _ => {}
1859                }
1860            }
1861        }
1862    }
1863
1864    let mut symbols = Vec::new();
1865    let mut cursor = QueryCursor::new();
1866    let mut matches = cursor.matches(query, *root, source.as_bytes());
1867
1868    while let Some(m) = {
1869        matches.advance();
1870        matches.get()
1871    } {
1872        let mut fn_name_node = None;
1873        let mut fn_def_node = None;
1874        let mut method_name_node = None;
1875        let mut method_def_node = None;
1876        let mut qual_scope_node = None;
1877        let mut qual_name_node = None;
1878        let mut qual_def_node = None;
1879        let mut class_name_node = None;
1880        let mut class_def_node = None;
1881        let mut struct_name_node = None;
1882        let mut struct_def_node = None;
1883        let mut enum_name_node = None;
1884        let mut enum_def_node = None;
1885        let mut namespace_name_node = None;
1886        let mut namespace_def_node = None;
1887        let mut template_class_name_node = None;
1888        let mut template_class_def_node = None;
1889        let mut template_class_item_node = None;
1890        let mut template_struct_name_node = None;
1891        let mut template_struct_def_node = None;
1892        let mut template_struct_item_node = None;
1893        let mut template_fn_name_node = None;
1894        let mut template_fn_def_node = None;
1895        let mut template_fn_item_node = None;
1896        let mut template_qual_scope_node = None;
1897        let mut template_qual_name_node = None;
1898        let mut template_qual_def_node = None;
1899        let mut template_qual_item_node = None;
1900
1901        for cap in m.captures {
1902            let Some(&name) = capture_names.get(cap.index as usize) else {
1903                continue;
1904            };
1905            match name {
1906                "fn.name" => fn_name_node = Some(cap.node),
1907                "fn.def" => fn_def_node = Some(cap.node),
1908                "method.name" => method_name_node = Some(cap.node),
1909                "method.def" => method_def_node = Some(cap.node),
1910                "qual.scope" => qual_scope_node = Some(cap.node),
1911                "qual.name" => qual_name_node = Some(cap.node),
1912                "qual.def" => qual_def_node = Some(cap.node),
1913                "class.name" => class_name_node = Some(cap.node),
1914                "class.def" => class_def_node = Some(cap.node),
1915                "struct.name" => struct_name_node = Some(cap.node),
1916                "struct.def" => struct_def_node = Some(cap.node),
1917                "enum.name" => enum_name_node = Some(cap.node),
1918                "enum.def" => enum_def_node = Some(cap.node),
1919                "namespace.name" => namespace_name_node = Some(cap.node),
1920                "namespace.def" => namespace_def_node = Some(cap.node),
1921                "template.class.name" => template_class_name_node = Some(cap.node),
1922                "template.class.def" => template_class_def_node = Some(cap.node),
1923                "template.class.item" => template_class_item_node = Some(cap.node),
1924                "template.struct.name" => template_struct_name_node = Some(cap.node),
1925                "template.struct.def" => template_struct_def_node = Some(cap.node),
1926                "template.struct.item" => template_struct_item_node = Some(cap.node),
1927                "template.fn.name" => template_fn_name_node = Some(cap.node),
1928                "template.fn.def" => template_fn_def_node = Some(cap.node),
1929                "template.fn.item" => template_fn_item_node = Some(cap.node),
1930                "template.qual.scope" => template_qual_scope_node = Some(cap.node),
1931                "template.qual.name" => template_qual_name_node = Some(cap.node),
1932                "template.qual.def" => template_qual_def_node = Some(cap.node),
1933                "template.qual.item" => template_qual_item_node = Some(cap.node),
1934                _ => {}
1935            }
1936        }
1937
1938        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1939            let in_template = def_node
1940                .parent()
1941                .map(|parent| parent.kind() == "template_declaration")
1942                .unwrap_or(false);
1943            if !in_template {
1944                let scope_chain = cpp_parent_scope_chain(&def_node, source);
1945                symbols.push(Symbol {
1946                    name: node_text(source, &name_node).to_string(),
1947                    kind: SymbolKind::Function,
1948                    range: node_range_with_decorators(&def_node, source, lang),
1949                    signature: Some(extract_signature(source, &def_node)),
1950                    scope_chain: scope_chain.clone(),
1951                    exported: false,
1952                    parent: scope_chain.last().cloned(),
1953                });
1954            }
1955        }
1956
1957        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
1958            let scope_chain = cpp_parent_scope_chain(&def_node, source);
1959            symbols.push(Symbol {
1960                name: node_text(source, &name_node).to_string(),
1961                kind: SymbolKind::Method,
1962                range: node_range_with_decorators(&def_node, source, lang),
1963                signature: Some(extract_signature(source, &def_node)),
1964                scope_chain: scope_chain.clone(),
1965                exported: false,
1966                parent: scope_chain.last().cloned(),
1967            });
1968        }
1969
1970        if let (Some(scope_node), Some(name_node), Some(def_node)) =
1971            (qual_scope_node, qual_name_node, qual_def_node)
1972        {
1973            let in_template = def_node
1974                .parent()
1975                .map(|parent| parent.kind() == "template_declaration")
1976                .unwrap_or(false);
1977            if !in_template {
1978                let scope_text = node_text(source, &scope_node);
1979                let scope_chain = split_scope_text(scope_text, "::");
1980                let parent = scope_chain.last().cloned();
1981                let kind = if parent
1982                    .as_ref()
1983                    .map(|segment| type_names.contains(segment))
1984                    .unwrap_or(false)
1985                {
1986                    SymbolKind::Method
1987                } else {
1988                    SymbolKind::Function
1989                };
1990
1991                symbols.push(Symbol {
1992                    name: node_text(source, &name_node).to_string(),
1993                    kind,
1994                    range: node_range_with_decorators(&def_node, source, lang),
1995                    signature: Some(extract_signature(source, &def_node)),
1996                    scope_chain,
1997                    exported: false,
1998                    parent,
1999                });
2000            }
2001        }
2002
2003        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
2004            let in_template = def_node
2005                .parent()
2006                .map(|parent| parent.kind() == "template_declaration")
2007                .unwrap_or(false);
2008            if !in_template {
2009                let scope_chain = cpp_parent_scope_chain(&def_node, source);
2010                let name = last_scope_segment(node_text(source, &name_node), "::");
2011                symbols.push(Symbol {
2012                    name: name.clone(),
2013                    kind: SymbolKind::Class,
2014                    range: node_range_with_decorators(&def_node, source, lang),
2015                    signature: Some(extract_signature(source, &def_node)),
2016                    scope_chain: scope_chain.clone(),
2017                    exported: false,
2018                    parent: scope_chain.last().cloned(),
2019                });
2020            }
2021        }
2022
2023        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
2024            let in_template = def_node
2025                .parent()
2026                .map(|parent| parent.kind() == "template_declaration")
2027                .unwrap_or(false);
2028            if !in_template {
2029                let scope_chain = cpp_parent_scope_chain(&def_node, source);
2030                let name = last_scope_segment(node_text(source, &name_node), "::");
2031                symbols.push(Symbol {
2032                    name: name.clone(),
2033                    kind: SymbolKind::Struct,
2034                    range: node_range_with_decorators(&def_node, source, lang),
2035                    signature: Some(extract_signature(source, &def_node)),
2036                    scope_chain: scope_chain.clone(),
2037                    exported: false,
2038                    parent: scope_chain.last().cloned(),
2039                });
2040            }
2041        }
2042
2043        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
2044            let scope_chain = cpp_parent_scope_chain(&def_node, source);
2045            let name = last_scope_segment(node_text(source, &name_node), "::");
2046            symbols.push(Symbol {
2047                name: name.clone(),
2048                kind: SymbolKind::Enum,
2049                range: node_range_with_decorators(&def_node, source, lang),
2050                signature: Some(extract_signature(source, &def_node)),
2051                scope_chain: scope_chain.clone(),
2052                exported: false,
2053                parent: scope_chain.last().cloned(),
2054            });
2055        }
2056
2057        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
2058            let scope_chain = cpp_parent_scope_chain(&def_node, source);
2059            symbols.push(Symbol {
2060                name: node_text(source, &name_node).to_string(),
2061                kind: SymbolKind::TypeAlias,
2062                range: node_range_with_decorators(&def_node, source, lang),
2063                signature: Some(extract_signature(source, &def_node)),
2064                scope_chain: scope_chain.clone(),
2065                exported: false,
2066                parent: scope_chain.last().cloned(),
2067            });
2068        }
2069
2070        if let (Some(name_node), Some(def_node), Some(item_node)) = (
2071            template_class_name_node,
2072            template_class_def_node,
2073            template_class_item_node,
2074        ) {
2075            let scope_chain = cpp_parent_scope_chain(&def_node, source);
2076            let name = last_scope_segment(node_text(source, &name_node), "::");
2077            symbols.push(Symbol {
2078                name: name.clone(),
2079                kind: SymbolKind::Class,
2080                range: node_range_with_decorators(&def_node, source, lang),
2081                signature: Some(template_signature(source, &def_node, &item_node)),
2082                scope_chain: scope_chain.clone(),
2083                exported: false,
2084                parent: scope_chain.last().cloned(),
2085            });
2086        }
2087
2088        if let (Some(name_node), Some(def_node), Some(item_node)) = (
2089            template_struct_name_node,
2090            template_struct_def_node,
2091            template_struct_item_node,
2092        ) {
2093            let scope_chain = cpp_parent_scope_chain(&def_node, source);
2094            let name = last_scope_segment(node_text(source, &name_node), "::");
2095            symbols.push(Symbol {
2096                name: name.clone(),
2097                kind: SymbolKind::Struct,
2098                range: node_range_with_decorators(&def_node, source, lang),
2099                signature: Some(template_signature(source, &def_node, &item_node)),
2100                scope_chain: scope_chain.clone(),
2101                exported: false,
2102                parent: scope_chain.last().cloned(),
2103            });
2104        }
2105
2106        if let (Some(name_node), Some(def_node), Some(item_node)) = (
2107            template_fn_name_node,
2108            template_fn_def_node,
2109            template_fn_item_node,
2110        ) {
2111            let scope_chain = cpp_parent_scope_chain(&def_node, source);
2112            symbols.push(Symbol {
2113                name: node_text(source, &name_node).to_string(),
2114                kind: SymbolKind::Function,
2115                range: node_range_with_decorators(&def_node, source, lang),
2116                signature: Some(template_signature(source, &def_node, &item_node)),
2117                scope_chain: scope_chain.clone(),
2118                exported: false,
2119                parent: scope_chain.last().cloned(),
2120            });
2121        }
2122
2123        if let (Some(scope_node), Some(name_node), Some(def_node), Some(item_node)) = (
2124            template_qual_scope_node,
2125            template_qual_name_node,
2126            template_qual_def_node,
2127            template_qual_item_node,
2128        ) {
2129            let scope_chain = split_scope_text(node_text(source, &scope_node), "::");
2130            let parent = scope_chain.last().cloned();
2131            let kind = if parent
2132                .as_ref()
2133                .map(|segment| type_names.contains(segment))
2134                .unwrap_or(false)
2135            {
2136                SymbolKind::Method
2137            } else {
2138                SymbolKind::Function
2139            };
2140
2141            symbols.push(Symbol {
2142                name: node_text(source, &name_node).to_string(),
2143                kind,
2144                range: node_range_with_decorators(&def_node, source, lang),
2145                signature: Some(template_signature(source, &def_node, &item_node)),
2146                scope_chain,
2147                exported: false,
2148                parent,
2149            });
2150        }
2151    }
2152
2153    dedup_symbols(&mut symbols);
2154    Ok(symbols)
2155}
2156
2157/// Extract symbols from Zig source.
2158fn extract_zig_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2159    let lang = LangId::Zig;
2160    let capture_names = query.capture_names();
2161
2162    let mut symbols = Vec::new();
2163    let mut cursor = QueryCursor::new();
2164    let mut matches = cursor.matches(query, *root, source.as_bytes());
2165
2166    while let Some(m) = {
2167        matches.advance();
2168        matches.get()
2169    } {
2170        let mut fn_name_node = None;
2171        let mut fn_def_node = None;
2172        let mut struct_name_node = None;
2173        let mut struct_def_node = None;
2174        let mut enum_name_node = None;
2175        let mut enum_def_node = None;
2176        let mut union_name_node = None;
2177        let mut union_def_node = None;
2178        let mut const_name_node = None;
2179        let mut const_def_node = None;
2180        let mut test_name_node = None;
2181        let mut test_def_node = None;
2182
2183        for cap in m.captures {
2184            let Some(&name) = capture_names.get(cap.index as usize) else {
2185                continue;
2186            };
2187            match name {
2188                "fn.name" => fn_name_node = Some(cap.node),
2189                "fn.def" => fn_def_node = Some(cap.node),
2190                "struct.name" => struct_name_node = Some(cap.node),
2191                "struct.def" => struct_def_node = Some(cap.node),
2192                "enum.name" => enum_name_node = Some(cap.node),
2193                "enum.def" => enum_def_node = Some(cap.node),
2194                "union.name" => union_name_node = Some(cap.node),
2195                "union.def" => union_def_node = Some(cap.node),
2196                "const.name" => const_name_node = Some(cap.node),
2197                "const.def" => const_def_node = Some(cap.node),
2198                "test.name" => test_name_node = Some(cap.node),
2199                "test.def" => test_def_node = Some(cap.node),
2200                _ => {}
2201            }
2202        }
2203
2204        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2205            let scope_chain = zig_container_scope_chain(&def_node, source);
2206            let kind = if scope_chain.is_empty() {
2207                SymbolKind::Function
2208            } else {
2209                SymbolKind::Method
2210            };
2211            symbols.push(Symbol {
2212                name: node_text(source, &name_node).to_string(),
2213                kind,
2214                range: node_range_with_decorators(&def_node, source, lang),
2215                signature: Some(extract_signature(source, &def_node)),
2216                scope_chain: scope_chain.clone(),
2217                exported: false,
2218                parent: scope_chain.last().cloned(),
2219            });
2220        }
2221
2222        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
2223            symbols.push(Symbol {
2224                name: node_text(source, &name_node).to_string(),
2225                kind: SymbolKind::Struct,
2226                range: node_range_with_decorators(&def_node, source, lang),
2227                signature: Some(extract_signature(source, &def_node)),
2228                scope_chain: vec![],
2229                exported: false,
2230                parent: None,
2231            });
2232        }
2233
2234        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
2235            symbols.push(Symbol {
2236                name: node_text(source, &name_node).to_string(),
2237                kind: SymbolKind::Enum,
2238                range: node_range_with_decorators(&def_node, source, lang),
2239                signature: Some(extract_signature(source, &def_node)),
2240                scope_chain: vec![],
2241                exported: false,
2242                parent: None,
2243            });
2244        }
2245
2246        if let (Some(name_node), Some(def_node)) = (union_name_node, union_def_node) {
2247            symbols.push(Symbol {
2248                name: node_text(source, &name_node).to_string(),
2249                kind: SymbolKind::TypeAlias,
2250                range: node_range_with_decorators(&def_node, source, lang),
2251                signature: Some(extract_signature(source, &def_node)),
2252                scope_chain: vec![],
2253                exported: false,
2254                parent: None,
2255            });
2256        }
2257
2258        if let (Some(name_node), Some(def_node)) = (const_name_node, const_def_node) {
2259            let signature = extract_signature(source, &def_node);
2260            let is_container = signature.contains("= struct")
2261                || signature.contains("= enum")
2262                || signature.contains("= union")
2263                || signature.contains("= opaque");
2264            let is_const = signature.trim_start().starts_with("const ");
2265            let name = node_text(source, &name_node).to_string();
2266            let already_captured = symbols.iter().any(|symbol| symbol.name == name);
2267            if is_const && !is_container && !already_captured {
2268                symbols.push(Symbol {
2269                    name,
2270                    kind: SymbolKind::Variable,
2271                    range: node_range_with_decorators(&def_node, source, lang),
2272                    signature: Some(signature),
2273                    scope_chain: vec![],
2274                    exported: false,
2275                    parent: None,
2276                });
2277            }
2278        }
2279
2280        if let (Some(name_node), Some(def_node)) = (test_name_node, test_def_node) {
2281            let scope_chain = zig_container_scope_chain(&def_node, source);
2282            symbols.push(Symbol {
2283                name: node_text(source, &name_node).trim_matches('"').to_string(),
2284                kind: SymbolKind::Function,
2285                range: node_range_with_decorators(&def_node, source, lang),
2286                signature: Some(extract_signature(source, &def_node)),
2287                scope_chain: scope_chain.clone(),
2288                exported: false,
2289                parent: scope_chain.last().cloned(),
2290            });
2291        }
2292    }
2293
2294    dedup_symbols(&mut symbols);
2295    Ok(symbols)
2296}
2297
2298/// Extract symbols from C# source.
2299fn extract_csharp_symbols(
2300    source: &str,
2301    root: &Node,
2302    query: &Query,
2303) -> Result<Vec<Symbol>, AftError> {
2304    let lang = LangId::CSharp;
2305    let capture_names = query.capture_names();
2306
2307    let mut symbols = Vec::new();
2308    let mut cursor = QueryCursor::new();
2309    let mut matches = cursor.matches(query, *root, source.as_bytes());
2310
2311    while let Some(m) = {
2312        matches.advance();
2313        matches.get()
2314    } {
2315        let mut class_name_node = None;
2316        let mut class_def_node = None;
2317        let mut interface_name_node = None;
2318        let mut interface_def_node = None;
2319        let mut struct_name_node = None;
2320        let mut struct_def_node = None;
2321        let mut enum_name_node = None;
2322        let mut enum_def_node = None;
2323        let mut method_name_node = None;
2324        let mut method_def_node = None;
2325        let mut property_name_node = None;
2326        let mut property_def_node = None;
2327        let mut namespace_name_node = None;
2328        let mut namespace_def_node = None;
2329
2330        for cap in m.captures {
2331            let Some(&name) = capture_names.get(cap.index as usize) else {
2332                continue;
2333            };
2334            match name {
2335                "class.name" => class_name_node = Some(cap.node),
2336                "class.def" => class_def_node = Some(cap.node),
2337                "interface.name" => interface_name_node = Some(cap.node),
2338                "interface.def" => interface_def_node = Some(cap.node),
2339                "struct.name" => struct_name_node = Some(cap.node),
2340                "struct.def" => struct_def_node = Some(cap.node),
2341                "enum.name" => enum_name_node = Some(cap.node),
2342                "enum.def" => enum_def_node = Some(cap.node),
2343                "method.name" => method_name_node = Some(cap.node),
2344                "method.def" => method_def_node = Some(cap.node),
2345                "property.name" => property_name_node = Some(cap.node),
2346                "property.def" => property_def_node = Some(cap.node),
2347                "namespace.name" => namespace_name_node = Some(cap.node),
2348                "namespace.def" => namespace_def_node = Some(cap.node),
2349                _ => {}
2350            }
2351        }
2352
2353        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
2354            let scope_chain = csharp_scope_chain(&def_node, source);
2355            symbols.push(Symbol {
2356                name: node_text(source, &name_node).to_string(),
2357                kind: SymbolKind::Class,
2358                range: node_range_with_decorators(&def_node, source, lang),
2359                signature: Some(extract_signature(source, &def_node)),
2360                scope_chain: scope_chain.clone(),
2361                exported: false,
2362                parent: scope_chain.last().cloned(),
2363            });
2364        }
2365
2366        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
2367            let scope_chain = csharp_scope_chain(&def_node, source);
2368            symbols.push(Symbol {
2369                name: node_text(source, &name_node).to_string(),
2370                kind: SymbolKind::Interface,
2371                range: node_range_with_decorators(&def_node, source, lang),
2372                signature: Some(extract_signature(source, &def_node)),
2373                scope_chain: scope_chain.clone(),
2374                exported: false,
2375                parent: scope_chain.last().cloned(),
2376            });
2377        }
2378
2379        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
2380            let scope_chain = csharp_scope_chain(&def_node, source);
2381            symbols.push(Symbol {
2382                name: node_text(source, &name_node).to_string(),
2383                kind: SymbolKind::Struct,
2384                range: node_range_with_decorators(&def_node, source, lang),
2385                signature: Some(extract_signature(source, &def_node)),
2386                scope_chain: scope_chain.clone(),
2387                exported: false,
2388                parent: scope_chain.last().cloned(),
2389            });
2390        }
2391
2392        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
2393            let scope_chain = csharp_scope_chain(&def_node, source);
2394            symbols.push(Symbol {
2395                name: node_text(source, &name_node).to_string(),
2396                kind: SymbolKind::Enum,
2397                range: node_range_with_decorators(&def_node, source, lang),
2398                signature: Some(extract_signature(source, &def_node)),
2399                scope_chain: scope_chain.clone(),
2400                exported: false,
2401                parent: scope_chain.last().cloned(),
2402            });
2403        }
2404
2405        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
2406            let scope_chain = csharp_scope_chain(&def_node, source);
2407            symbols.push(Symbol {
2408                name: node_text(source, &name_node).to_string(),
2409                kind: SymbolKind::Method,
2410                range: node_range_with_decorators(&def_node, source, lang),
2411                signature: Some(extract_signature(source, &def_node)),
2412                scope_chain: scope_chain.clone(),
2413                exported: false,
2414                parent: scope_chain.last().cloned(),
2415            });
2416        }
2417
2418        if let (Some(name_node), Some(def_node)) = (property_name_node, property_def_node) {
2419            let scope_chain = csharp_scope_chain(&def_node, source);
2420            symbols.push(Symbol {
2421                name: node_text(source, &name_node).to_string(),
2422                kind: SymbolKind::Variable,
2423                range: node_range_with_decorators(&def_node, source, lang),
2424                signature: Some(extract_signature(source, &def_node)),
2425                scope_chain: scope_chain.clone(),
2426                exported: false,
2427                parent: scope_chain.last().cloned(),
2428            });
2429        }
2430
2431        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
2432            let scope_chain = csharp_scope_chain(&def_node, source);
2433            symbols.push(Symbol {
2434                name: node_text(source, &name_node).to_string(),
2435                kind: SymbolKind::TypeAlias,
2436                range: node_range_with_decorators(&def_node, source, lang),
2437                signature: Some(extract_signature(source, &def_node)),
2438                scope_chain: scope_chain.clone(),
2439                exported: false,
2440                parent: scope_chain.last().cloned(),
2441            });
2442        }
2443    }
2444
2445    dedup_symbols(&mut symbols);
2446    Ok(symbols)
2447}
2448
2449/// Recursively find the first type_identifier node in a subtree.
2450fn find_type_identifier_recursive(node: &Node, source: &str) -> Option<String> {
2451    if node.kind() == "type_identifier" {
2452        return Some(node_text(source, node).to_string());
2453    }
2454    let mut cursor = node.walk();
2455    if cursor.goto_first_child() {
2456        loop {
2457            if let Some(result) = find_type_identifier_recursive(&cursor.node(), source) {
2458                return Some(result);
2459            }
2460            if !cursor.goto_next_sibling() {
2461                break;
2462            }
2463        }
2464    }
2465    None
2466}
2467
2468/// Extract HTML headings (h1-h6) as symbols.
2469/// Each heading becomes a symbol with kind `Heading`, and its range covers
2470/// the element itself. Headings are nested based on their level.
2471fn extract_bash_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2472    let lang = LangId::Bash;
2473    let capture_names = query.capture_names();
2474
2475    let mut symbols = Vec::new();
2476    let mut cursor = QueryCursor::new();
2477    let mut matches = cursor.matches(query, *root, source.as_bytes());
2478
2479    while let Some(m) = {
2480        matches.advance();
2481        matches.get()
2482    } {
2483        let mut fn_name_node = None;
2484        let mut fn_def_node = None;
2485
2486        for cap in m.captures {
2487            let Some(&name) = capture_names.get(cap.index as usize) else {
2488                continue;
2489            };
2490            match name {
2491                "fn.name" => fn_name_node = Some(cap.node),
2492                "fn.def" => fn_def_node = Some(cap.node),
2493                _ => {}
2494            }
2495        }
2496
2497        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2498            symbols.push(Symbol {
2499                name: node_text(source, &name_node).to_string(),
2500                kind: SymbolKind::Function,
2501                range: node_range_with_decorators(&def_node, source, lang),
2502                signature: Some(extract_signature(source, &def_node)),
2503                scope_chain: vec![],
2504                exported: false,
2505                parent: None,
2506            });
2507        }
2508    }
2509
2510    Ok(symbols)
2511}
2512
2513fn extract_html_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
2514    let mut headings: Vec<(u8, Symbol)> = Vec::new();
2515    collect_html_headings(source, root, &mut headings);
2516
2517    // Build hierarchy: assign scope_chain and parent based on heading level
2518    let mut scope_stack: Vec<(u8, String)> = Vec::new(); // (level, name)
2519    for (level, symbol) in headings.iter_mut() {
2520        // Pop scope entries that are at the same level or deeper
2521        while scope_stack.last().is_some_and(|(l, _)| *l >= *level) {
2522            scope_stack.pop();
2523        }
2524        symbol.scope_chain = scope_stack.iter().map(|(_, name)| name.clone()).collect();
2525        symbol.parent = scope_stack.last().map(|(_, name)| name.clone());
2526        scope_stack.push((*level, symbol.name.clone()));
2527    }
2528
2529    Ok(headings.into_iter().map(|(_, s)| s).collect())
2530}
2531
2532/// Recursively collect h1-h6 elements from the HTML tree.
2533fn collect_html_headings(source: &str, node: &Node, headings: &mut Vec<(u8, Symbol)>) {
2534    let mut cursor = node.walk();
2535    if !cursor.goto_first_child() {
2536        return;
2537    }
2538
2539    loop {
2540        let child = cursor.node();
2541        if child.kind() == "element" {
2542            // Check if this element's start tag is h1-h6
2543            if let Some(start_tag) = child
2544                .child_by_field_name("start_tag")
2545                .or_else(|| child.child(0).filter(|c| c.kind() == "start_tag"))
2546            {
2547                if let Some(tag_name_node) = start_tag
2548                    .child_by_field_name("tag_name")
2549                    .or_else(|| start_tag.child(1).filter(|c| c.kind() == "tag_name"))
2550                {
2551                    let tag_name = node_text(source, &tag_name_node).to_lowercase();
2552                    if let Some(level) = match tag_name.as_str() {
2553                        "h1" => Some(1u8),
2554                        "h2" => Some(2),
2555                        "h3" => Some(3),
2556                        "h4" => Some(4),
2557                        "h5" => Some(5),
2558                        "h6" => Some(6),
2559                        _ => None,
2560                    } {
2561                        // Extract text content from the element
2562                        let text = extract_element_text(source, &child).trim().to_string();
2563                        if !text.is_empty() {
2564                            let range = node_range(&child);
2565                            let signature = format!("<h{}> {}", level, text);
2566                            headings.push((
2567                                level,
2568                                Symbol {
2569                                    name: text,
2570                                    kind: SymbolKind::Heading,
2571                                    range,
2572                                    signature: Some(signature),
2573                                    scope_chain: vec![], // filled later
2574                                    exported: false,
2575                                    parent: None, // filled later
2576                                },
2577                            ));
2578                        }
2579                    }
2580                }
2581            }
2582            // Recurse into element children (nested headings)
2583            collect_html_headings(source, &child, headings);
2584        } else {
2585            // Recurse into other node types (document, body, etc.)
2586            collect_html_headings(source, &child, headings);
2587        }
2588
2589        if !cursor.goto_next_sibling() {
2590            break;
2591        }
2592    }
2593}
2594
2595/// Extract text content from an HTML element, stripping tags.
2596fn extract_element_text(source: &str, node: &Node) -> String {
2597    let mut text = String::new();
2598    let mut cursor = node.walk();
2599    if !cursor.goto_first_child() {
2600        return text;
2601    }
2602    loop {
2603        let child = cursor.node();
2604        match child.kind() {
2605            "text" => {
2606                text.push_str(node_text(source, &child));
2607            }
2608            "element" => {
2609                // Recurse into nested elements (e.g., <strong>, <em>, <a>)
2610                text.push_str(&extract_element_text(source, &child));
2611            }
2612            _ => {}
2613        }
2614        if !cursor.goto_next_sibling() {
2615            break;
2616        }
2617    }
2618    text
2619}
2620
2621/// Extract markdown headings as symbols.
2622/// Each heading becomes a symbol with kind `Heading`, and its range covers the entire
2623/// section (from the heading to the next heading at the same or higher level, or EOF).
2624fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
2625    let mut symbols = Vec::new();
2626    extract_md_sections(source, root, &mut symbols, &[]);
2627    Ok(symbols)
2628}
2629
2630/// Recursively walk `section` nodes to build the heading hierarchy.
2631fn extract_md_sections(
2632    source: &str,
2633    node: &Node,
2634    symbols: &mut Vec<Symbol>,
2635    scope_chain: &[String],
2636) {
2637    let mut cursor = node.walk();
2638    if !cursor.goto_first_child() {
2639        return;
2640    }
2641
2642    loop {
2643        let child = cursor.node();
2644        match child.kind() {
2645            "section" => {
2646                // A section contains an atx_heading as its first child,
2647                // followed by content and possibly nested sections.
2648                let mut section_cursor = child.walk();
2649                let mut heading_name = String::new();
2650                let mut heading_level: u8 = 0;
2651
2652                if section_cursor.goto_first_child() {
2653                    loop {
2654                        let section_child = section_cursor.node();
2655                        if section_child.kind() == "atx_heading" {
2656                            // Extract heading level from marker type
2657                            let mut h_cursor = section_child.walk();
2658                            if h_cursor.goto_first_child() {
2659                                loop {
2660                                    let h_child = h_cursor.node();
2661                                    let kind = h_child.kind();
2662                                    if kind.starts_with("atx_h") && kind.ends_with("_marker") {
2663                                        // "atx_h1_marker" → level 1, "atx_h2_marker" → level 2, etc.
2664                                        heading_level = kind
2665                                            .strip_prefix("atx_h")
2666                                            .and_then(|s| s.strip_suffix("_marker"))
2667                                            .and_then(|s| s.parse::<u8>().ok())
2668                                            .unwrap_or(1);
2669                                    } else if h_child.kind() == "inline" {
2670                                        heading_name =
2671                                            node_text(source, &h_child).trim().to_string();
2672                                    }
2673                                    if !h_cursor.goto_next_sibling() {
2674                                        break;
2675                                    }
2676                                }
2677                            }
2678                        }
2679                        if !section_cursor.goto_next_sibling() {
2680                            break;
2681                        }
2682                    }
2683                }
2684
2685                if !heading_name.is_empty() {
2686                    let range = node_range(&child);
2687                    let signature = format!(
2688                        "{} {}",
2689                        "#".repeat((heading_level as usize).min(6)),
2690                        heading_name
2691                    );
2692
2693                    symbols.push(Symbol {
2694                        name: heading_name.clone(),
2695                        kind: SymbolKind::Heading,
2696                        range,
2697                        signature: Some(signature),
2698                        scope_chain: scope_chain.to_vec(),
2699                        exported: false,
2700                        parent: scope_chain.last().cloned(),
2701                    });
2702
2703                    // Recurse into the section for nested headings
2704                    let mut new_scope = scope_chain.to_vec();
2705                    new_scope.push(heading_name);
2706                    extract_md_sections(source, &child, symbols, &new_scope);
2707                }
2708            }
2709            _ => {}
2710        }
2711
2712        if !cursor.goto_next_sibling() {
2713            break;
2714        }
2715    }
2716}
2717
2718/// Remove duplicate symbols based on (name, kind, start_line).
2719/// Class declarations can match both "class" and "method" patterns,
2720/// producing duplicates.
2721fn dedup_symbols(symbols: &mut Vec<Symbol>) {
2722    let mut seen = std::collections::HashSet::new();
2723    symbols.retain(|s| {
2724        let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
2725        seen.insert(key)
2726    });
2727}
2728
2729/// Provider that uses tree-sitter for real symbol extraction.
2730/// Implements the `LanguageProvider` trait from `language.rs`.
2731pub struct TreeSitterProvider {
2732    parser: RefCell<FileParser>,
2733}
2734
2735#[derive(Debug, Clone)]
2736struct ReExportTarget {
2737    file: PathBuf,
2738    symbol_name: String,
2739}
2740
2741impl TreeSitterProvider {
2742    /// Create a new `TreeSitterProvider` backed by a fresh `FileParser`.
2743    pub fn new() -> Self {
2744        Self {
2745            parser: RefCell::new(FileParser::new()),
2746        }
2747    }
2748
2749    /// Merge a pre-warmed symbol cache into the parser.
2750    /// Called from the main loop when the background indexer completes.
2751    pub fn merge_warm_cache(&self, cache: SymbolCache) {
2752        let mut parser = self.parser.borrow_mut();
2753        parser.set_warm_cache(cache);
2754    }
2755
2756    /// Return (local_cache_entries, warm_cache_entries) for status reporting.
2757    pub fn symbol_cache_stats(&self) -> (usize, usize) {
2758        let parser = self.parser.borrow();
2759        let local = parser.symbol_cache_len();
2760        let warm = parser.warm_cache_len();
2761        (local, warm)
2762    }
2763
2764    fn resolve_symbol_inner(
2765        &self,
2766        file: &Path,
2767        name: &str,
2768        depth: usize,
2769        visited: &mut HashSet<(PathBuf, String)>,
2770    ) -> Result<Vec<SymbolMatch>, AftError> {
2771        if depth > MAX_REEXPORT_DEPTH {
2772            return Ok(Vec::new());
2773        }
2774
2775        let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
2776        if !visited.insert((canonical_file, name.to_string())) {
2777            return Ok(Vec::new());
2778        }
2779
2780        let symbols = self.parser.borrow_mut().extract_symbols(file)?;
2781        let local_matches = symbol_matches_in_file(file, &symbols, name);
2782        if !local_matches.is_empty() {
2783            return Ok(local_matches);
2784        }
2785
2786        if name == "default" {
2787            let default_matches = self.resolve_local_default_export(file, &symbols)?;
2788            if !default_matches.is_empty() {
2789                return Ok(default_matches);
2790            }
2791        }
2792
2793        let reexport_targets = self.collect_reexport_targets(file, name)?;
2794        let mut matches = Vec::new();
2795        let mut seen = HashSet::new();
2796        for target in reexport_targets {
2797            for resolved in
2798                self.resolve_symbol_inner(&target.file, &target.symbol_name, depth + 1, visited)?
2799            {
2800                let key = format!(
2801                    "{}:{}:{}:{}:{}:{}",
2802                    resolved.file,
2803                    resolved.symbol.name,
2804                    resolved.symbol.range.start_line,
2805                    resolved.symbol.range.start_col,
2806                    resolved.symbol.range.end_line,
2807                    resolved.symbol.range.end_col
2808                );
2809                if seen.insert(key) {
2810                    matches.push(resolved);
2811                }
2812            }
2813        }
2814
2815        Ok(matches)
2816    }
2817
2818    fn collect_reexport_targets(
2819        &self,
2820        file: &Path,
2821        requested_name: &str,
2822    ) -> Result<Vec<ReExportTarget>, AftError> {
2823        let (source, tree, lang) = self.read_parsed_file(file)?;
2824        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
2825            return Ok(Vec::new());
2826        }
2827
2828        let mut targets = Vec::new();
2829        let root = tree.root_node();
2830        let from_dir = file.parent().unwrap_or_else(|| Path::new("."));
2831
2832        let mut cursor = root.walk();
2833        if !cursor.goto_first_child() {
2834            return Ok(targets);
2835        }
2836
2837        loop {
2838            let node = cursor.node();
2839            if node.kind() == "export_statement" {
2840                let Some(source_node) = node.child_by_field_name("source") else {
2841                    if !cursor.goto_next_sibling() {
2842                        break;
2843                    }
2844                    continue;
2845                };
2846
2847                let Some(module_path) = string_content(&source, &source_node) else {
2848                    if !cursor.goto_next_sibling() {
2849                        break;
2850                    }
2851                    continue;
2852                };
2853
2854                let Some(target_file) = resolve_module_path(from_dir, &module_path) else {
2855                    if !cursor.goto_next_sibling() {
2856                        break;
2857                    }
2858                    continue;
2859                };
2860
2861                if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
2862                    if let Some(symbol_name) =
2863                        resolve_export_clause_name(&source, &export_clause, requested_name)
2864                    {
2865                        targets.push(ReExportTarget {
2866                            file: target_file,
2867                            symbol_name,
2868                        });
2869                    }
2870                } else if export_statement_has_wildcard(&source, &node) {
2871                    targets.push(ReExportTarget {
2872                        file: target_file,
2873                        symbol_name: requested_name.to_string(),
2874                    });
2875                }
2876            }
2877
2878            if !cursor.goto_next_sibling() {
2879                break;
2880            }
2881        }
2882
2883        Ok(targets)
2884    }
2885
2886    fn resolve_local_default_export(
2887        &self,
2888        file: &Path,
2889        symbols: &[Symbol],
2890    ) -> Result<Vec<SymbolMatch>, AftError> {
2891        let (source, tree, lang) = self.read_parsed_file(file)?;
2892        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
2893            return Ok(Vec::new());
2894        }
2895
2896        let root = tree.root_node();
2897        let mut matches = Vec::new();
2898        let mut seen = HashSet::new();
2899
2900        let mut cursor = root.walk();
2901        if !cursor.goto_first_child() {
2902            return Ok(matches);
2903        }
2904
2905        loop {
2906            let node = cursor.node();
2907            if node.kind() == "export_statement"
2908                && node.child_by_field_name("source").is_none()
2909                && node_contains_token(&source, &node, "default")
2910            {
2911                if let Some(target_name) = default_export_target_name(&source, &node) {
2912                    for symbol_match in symbol_matches_in_file(file, symbols, &target_name) {
2913                        let key = format!(
2914                            "{}:{}:{}:{}:{}:{}",
2915                            symbol_match.file,
2916                            symbol_match.symbol.name,
2917                            symbol_match.symbol.range.start_line,
2918                            symbol_match.symbol.range.start_col,
2919                            symbol_match.symbol.range.end_line,
2920                            symbol_match.symbol.range.end_col
2921                        );
2922                        if seen.insert(key) {
2923                            matches.push(symbol_match);
2924                        }
2925                    }
2926                }
2927            }
2928
2929            if !cursor.goto_next_sibling() {
2930                break;
2931            }
2932        }
2933
2934        Ok(matches)
2935    }
2936
2937    fn read_parsed_file(&self, file: &Path) -> Result<(String, Tree, LangId), AftError> {
2938        let source = std::fs::read_to_string(file).map_err(|e| AftError::FileNotFound {
2939            path: format!("{}: {}", file.display(), e),
2940        })?;
2941        let (tree, lang) = {
2942            let mut parser = self.parser.borrow_mut();
2943            parser.parse_cloned(file)?
2944        };
2945        Ok((source, tree, lang))
2946    }
2947}
2948
2949fn symbol_matches_in_file(file: &Path, symbols: &[Symbol], name: &str) -> Vec<SymbolMatch> {
2950    symbols
2951        .iter()
2952        .filter(|symbol| symbol.name == name)
2953        .cloned()
2954        .map(|symbol| SymbolMatch {
2955            file: file.display().to_string(),
2956            symbol,
2957        })
2958        .collect()
2959}
2960
2961fn string_content(source: &str, node: &Node) -> Option<String> {
2962    let text = node_text(source, node);
2963    if text.len() < 2 {
2964        return None;
2965    }
2966
2967    Some(
2968        text.trim_start_matches(|c| c == '\'' || c == '"')
2969            .trim_end_matches(|c| c == '\'' || c == '"')
2970            .to_string(),
2971    )
2972}
2973
2974fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
2975    let mut cursor = node.walk();
2976    if !cursor.goto_first_child() {
2977        return None;
2978    }
2979
2980    loop {
2981        let child = cursor.node();
2982        if child.kind() == kind {
2983            return Some(child);
2984        }
2985        if !cursor.goto_next_sibling() {
2986            break;
2987        }
2988    }
2989
2990    None
2991}
2992
2993fn resolve_export_clause_name(
2994    source: &str,
2995    export_clause: &Node,
2996    requested_name: &str,
2997) -> Option<String> {
2998    let mut cursor = export_clause.walk();
2999    if !cursor.goto_first_child() {
3000        return None;
3001    }
3002
3003    loop {
3004        let child = cursor.node();
3005        if child.kind() == "export_specifier" {
3006            let (source_name, exported_name) = export_specifier_names(source, &child)?;
3007            if exported_name == requested_name {
3008                return Some(source_name);
3009            }
3010        }
3011
3012        if !cursor.goto_next_sibling() {
3013            break;
3014        }
3015    }
3016
3017    None
3018}
3019
3020fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
3021    let source_name = specifier
3022        .child_by_field_name("name")
3023        .map(|node| node_text(source, &node).to_string());
3024    let alias_name = specifier
3025        .child_by_field_name("alias")
3026        .map(|node| node_text(source, &node).to_string());
3027
3028    if let Some(source_name) = source_name {
3029        let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
3030        return Some((source_name, exported_name));
3031    }
3032
3033    let mut names = Vec::new();
3034    let mut cursor = specifier.walk();
3035    if cursor.goto_first_child() {
3036        loop {
3037            let child = cursor.node();
3038            let child_text = node_text(source, &child).trim();
3039            if matches!(
3040                child.kind(),
3041                "identifier" | "type_identifier" | "property_identifier"
3042            ) || child_text == "default"
3043            {
3044                names.push(child_text.to_string());
3045            }
3046            if !cursor.goto_next_sibling() {
3047                break;
3048            }
3049        }
3050    }
3051
3052    match names.as_slice() {
3053        [name] => Some((name.clone(), name.clone())),
3054        [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
3055        _ => None,
3056    }
3057}
3058
3059fn export_statement_has_wildcard(source: &str, node: &Node) -> bool {
3060    let mut cursor = node.walk();
3061    if !cursor.goto_first_child() {
3062        return false;
3063    }
3064
3065    loop {
3066        if node_text(source, &cursor.node()).trim() == "*" {
3067            return true;
3068        }
3069        if !cursor.goto_next_sibling() {
3070            break;
3071        }
3072    }
3073
3074    false
3075}
3076
3077fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
3078    let mut cursor = node.walk();
3079    if !cursor.goto_first_child() {
3080        return false;
3081    }
3082
3083    loop {
3084        if node_text(source, &cursor.node()).trim() == token {
3085            return true;
3086        }
3087        if !cursor.goto_next_sibling() {
3088            break;
3089        }
3090    }
3091
3092    false
3093}
3094
3095fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
3096    let mut cursor = export_stmt.walk();
3097    if !cursor.goto_first_child() {
3098        return None;
3099    }
3100
3101    loop {
3102        let child = cursor.node();
3103        match child.kind() {
3104            "function_declaration"
3105            | "class_declaration"
3106            | "interface_declaration"
3107            | "enum_declaration"
3108            | "type_alias_declaration"
3109            | "lexical_declaration" => {
3110                if let Some(name_node) = child.child_by_field_name("name") {
3111                    return Some(node_text(source, &name_node).to_string());
3112                }
3113
3114                if child.kind() == "lexical_declaration" {
3115                    let mut child_cursor = child.walk();
3116                    if child_cursor.goto_first_child() {
3117                        loop {
3118                            let nested = child_cursor.node();
3119                            if nested.kind() == "variable_declarator" {
3120                                if let Some(name_node) = nested.child_by_field_name("name") {
3121                                    return Some(node_text(source, &name_node).to_string());
3122                                }
3123                            }
3124                            if !child_cursor.goto_next_sibling() {
3125                                break;
3126                            }
3127                        }
3128                    }
3129                }
3130            }
3131            "identifier" | "type_identifier" => {
3132                let text = node_text(source, &child);
3133                if text != "export" && text != "default" {
3134                    return Some(text.to_string());
3135                }
3136            }
3137            _ => {}
3138        }
3139
3140        if !cursor.goto_next_sibling() {
3141            break;
3142        }
3143    }
3144
3145    None
3146}
3147
3148impl crate::language::LanguageProvider for TreeSitterProvider {
3149    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
3150        let matches = self.resolve_symbol_inner(file, name, 0, &mut HashSet::new())?;
3151
3152        if matches.is_empty() {
3153            Err(AftError::SymbolNotFound {
3154                name: name.to_string(),
3155                file: file.display().to_string(),
3156            })
3157        } else {
3158            Ok(matches)
3159        }
3160    }
3161
3162    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
3163        self.parser.borrow_mut().extract_symbols(file)
3164    }
3165
3166    fn as_any(&self) -> &dyn std::any::Any {
3167        self
3168    }
3169}
3170
3171#[cfg(test)]
3172mod tests {
3173    use super::*;
3174    use crate::language::LanguageProvider;
3175    use std::path::PathBuf;
3176
3177    fn fixture_path(name: &str) -> PathBuf {
3178        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3179            .join("tests")
3180            .join("fixtures")
3181            .join(name)
3182    }
3183
3184    // --- Language detection ---
3185
3186    #[test]
3187    fn detect_ts() {
3188        assert_eq!(
3189            detect_language(Path::new("foo.ts")),
3190            Some(LangId::TypeScript)
3191        );
3192    }
3193
3194    #[test]
3195    fn detect_tsx() {
3196        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
3197    }
3198
3199    #[test]
3200    fn detect_js() {
3201        assert_eq!(
3202            detect_language(Path::new("foo.js")),
3203            Some(LangId::JavaScript)
3204        );
3205    }
3206
3207    #[test]
3208    fn detect_jsx() {
3209        assert_eq!(
3210            detect_language(Path::new("foo.jsx")),
3211            Some(LangId::JavaScript)
3212        );
3213    }
3214
3215    #[test]
3216    fn detect_py() {
3217        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
3218    }
3219
3220    #[test]
3221    fn detect_rs() {
3222        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
3223    }
3224
3225    #[test]
3226    fn detect_go() {
3227        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
3228    }
3229
3230    #[test]
3231    fn detect_c() {
3232        assert_eq!(detect_language(Path::new("foo.c")), Some(LangId::C));
3233    }
3234
3235    #[test]
3236    fn detect_h() {
3237        assert_eq!(detect_language(Path::new("foo.h")), Some(LangId::C));
3238    }
3239
3240    #[test]
3241    fn detect_cc() {
3242        assert_eq!(detect_language(Path::new("foo.cc")), Some(LangId::Cpp));
3243    }
3244
3245    #[test]
3246    fn detect_cpp() {
3247        assert_eq!(detect_language(Path::new("foo.cpp")), Some(LangId::Cpp));
3248    }
3249
3250    #[test]
3251    fn detect_cxx() {
3252        assert_eq!(detect_language(Path::new("foo.cxx")), Some(LangId::Cpp));
3253    }
3254
3255    #[test]
3256    fn detect_hpp() {
3257        assert_eq!(detect_language(Path::new("foo.hpp")), Some(LangId::Cpp));
3258    }
3259
3260    #[test]
3261    fn detect_hh() {
3262        assert_eq!(detect_language(Path::new("foo.hh")), Some(LangId::Cpp));
3263    }
3264
3265    #[test]
3266    fn detect_zig() {
3267        assert_eq!(detect_language(Path::new("foo.zig")), Some(LangId::Zig));
3268    }
3269
3270    #[test]
3271    fn detect_cs() {
3272        assert_eq!(detect_language(Path::new("foo.cs")), Some(LangId::CSharp));
3273    }
3274
3275    #[test]
3276    fn detect_unknown_returns_none() {
3277        assert_eq!(detect_language(Path::new("foo.txt")), None);
3278    }
3279
3280    // --- Unsupported extension error ---
3281
3282    #[test]
3283    fn unsupported_extension_returns_invalid_request() {
3284        // Use a file that exists but has an unsupported extension
3285        let path = fixture_path("sample.ts");
3286        let bad_path = path.with_extension("txt");
3287        // Create a dummy file so the error comes from language detection, not I/O
3288        std::fs::write(&bad_path, "hello").unwrap();
3289        let provider = TreeSitterProvider::new();
3290        let result = provider.list_symbols(&bad_path);
3291        std::fs::remove_file(&bad_path).ok();
3292        match result {
3293            Err(AftError::InvalidRequest { message }) => {
3294                assert!(
3295                    message.contains("unsupported file extension"),
3296                    "msg: {}",
3297                    message
3298                );
3299                assert!(message.contains("txt"), "msg: {}", message);
3300            }
3301            other => panic!("expected InvalidRequest, got {:?}", other),
3302        }
3303    }
3304
3305    // --- TypeScript extraction ---
3306
3307    #[test]
3308    fn ts_extracts_all_symbol_kinds() {
3309        let provider = TreeSitterProvider::new();
3310        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3311
3312        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3313        assert!(
3314            names.contains(&"greet"),
3315            "missing function greet: {:?}",
3316            names
3317        );
3318        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
3319        assert!(
3320            names.contains(&"UserService"),
3321            "missing class UserService: {:?}",
3322            names
3323        );
3324        assert!(
3325            names.contains(&"Config"),
3326            "missing interface Config: {:?}",
3327            names
3328        );
3329        assert!(
3330            names.contains(&"Status"),
3331            "missing enum Status: {:?}",
3332            names
3333        );
3334        assert!(
3335            names.contains(&"UserId"),
3336            "missing type alias UserId: {:?}",
3337            names
3338        );
3339        assert!(
3340            names.contains(&"internalHelper"),
3341            "missing non-exported fn: {:?}",
3342            names
3343        );
3344
3345        // At least 6 unique symbols as required
3346        assert!(
3347            symbols.len() >= 6,
3348            "expected ≥6 symbols, got {}: {:?}",
3349            symbols.len(),
3350            names
3351        );
3352    }
3353
3354    #[test]
3355    fn ts_symbol_kinds_correct() {
3356        let provider = TreeSitterProvider::new();
3357        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3358
3359        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3360
3361        assert_eq!(find("greet").kind, SymbolKind::Function);
3362        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
3363        assert_eq!(find("UserService").kind, SymbolKind::Class);
3364        assert_eq!(find("Config").kind, SymbolKind::Interface);
3365        assert_eq!(find("Status").kind, SymbolKind::Enum);
3366        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
3367    }
3368
3369    #[test]
3370    fn ts_export_detection() {
3371        let provider = TreeSitterProvider::new();
3372        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3373
3374        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3375
3376        assert!(find("greet").exported, "greet should be exported");
3377        assert!(find("add").exported, "add should be exported");
3378        assert!(
3379            find("UserService").exported,
3380            "UserService should be exported"
3381        );
3382        assert!(find("Config").exported, "Config should be exported");
3383        assert!(find("Status").exported, "Status should be exported");
3384        assert!(
3385            !find("internalHelper").exported,
3386            "internalHelper should not be exported"
3387        );
3388    }
3389
3390    #[test]
3391    fn ts_method_scope_chain() {
3392        let provider = TreeSitterProvider::new();
3393        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3394
3395        let methods: Vec<&Symbol> = symbols
3396            .iter()
3397            .filter(|s| s.kind == SymbolKind::Method)
3398            .collect();
3399        assert!(!methods.is_empty(), "should have at least one method");
3400
3401        for method in &methods {
3402            assert_eq!(
3403                method.scope_chain,
3404                vec!["UserService"],
3405                "method {} should have UserService in scope chain",
3406                method.name
3407            );
3408            assert_eq!(method.parent.as_deref(), Some("UserService"));
3409        }
3410    }
3411
3412    #[test]
3413    fn ts_signatures_present() {
3414        let provider = TreeSitterProvider::new();
3415        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3416
3417        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3418
3419        let greet_sig = find("greet").signature.as_ref().unwrap();
3420        assert!(
3421            greet_sig.contains("greet"),
3422            "signature should contain function name: {}",
3423            greet_sig
3424        );
3425    }
3426
3427    #[test]
3428    fn ts_ranges_valid() {
3429        let provider = TreeSitterProvider::new();
3430        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3431
3432        for s in &symbols {
3433            assert!(
3434                s.range.end_line >= s.range.start_line,
3435                "symbol {} has invalid range: {:?}",
3436                s.name,
3437                s.range
3438            );
3439        }
3440    }
3441
3442    // --- JavaScript extraction ---
3443
3444    #[test]
3445    fn js_extracts_core_symbols() {
3446        let provider = TreeSitterProvider::new();
3447        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3448
3449        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3450        assert!(
3451            names.contains(&"multiply"),
3452            "missing function multiply: {:?}",
3453            names
3454        );
3455        assert!(
3456            names.contains(&"divide"),
3457            "missing arrow fn divide: {:?}",
3458            names
3459        );
3460        assert!(
3461            names.contains(&"EventEmitter"),
3462            "missing class EventEmitter: {:?}",
3463            names
3464        );
3465        assert!(
3466            names.contains(&"main"),
3467            "missing default export fn main: {:?}",
3468            names
3469        );
3470
3471        assert!(
3472            symbols.len() >= 4,
3473            "expected ≥4 symbols, got {}: {:?}",
3474            symbols.len(),
3475            names
3476        );
3477    }
3478
3479    #[test]
3480    fn js_arrow_fn_correctly_named() {
3481        let provider = TreeSitterProvider::new();
3482        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3483
3484        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
3485        assert_eq!(divide.kind, SymbolKind::Function);
3486        assert!(divide.exported, "divide should be exported");
3487
3488        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
3489        assert_eq!(internal.kind, SymbolKind::Function);
3490        assert!(!internal.exported, "internalUtil should not be exported");
3491    }
3492
3493    #[test]
3494    fn js_method_scope_chain() {
3495        let provider = TreeSitterProvider::new();
3496        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3497
3498        let methods: Vec<&Symbol> = symbols
3499            .iter()
3500            .filter(|s| s.kind == SymbolKind::Method)
3501            .collect();
3502
3503        for method in &methods {
3504            assert_eq!(
3505                method.scope_chain,
3506                vec!["EventEmitter"],
3507                "method {} should have EventEmitter in scope chain",
3508                method.name
3509            );
3510        }
3511    }
3512
3513    // --- TSX extraction ---
3514
3515    #[test]
3516    fn tsx_extracts_react_component() {
3517        let provider = TreeSitterProvider::new();
3518        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
3519
3520        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3521        assert!(
3522            names.contains(&"Button"),
3523            "missing React component Button: {:?}",
3524            names
3525        );
3526        assert!(
3527            names.contains(&"Counter"),
3528            "missing class Counter: {:?}",
3529            names
3530        );
3531        assert!(
3532            names.contains(&"formatLabel"),
3533            "missing function formatLabel: {:?}",
3534            names
3535        );
3536
3537        assert!(
3538            symbols.len() >= 2,
3539            "expected ≥2 symbols, got {}: {:?}",
3540            symbols.len(),
3541            names
3542        );
3543    }
3544
3545    #[test]
3546    fn tsx_jsx_doesnt_break_parser() {
3547        // Main assertion: TSX grammar handles JSX without errors
3548        let provider = TreeSitterProvider::new();
3549        let result = provider.list_symbols(&fixture_path("sample.tsx"));
3550        assert!(
3551            result.is_ok(),
3552            "TSX parsing should succeed: {:?}",
3553            result.err()
3554        );
3555    }
3556
3557    // --- resolve_symbol ---
3558
3559    #[test]
3560    fn resolve_symbol_finds_match() {
3561        let provider = TreeSitterProvider::new();
3562        let matches = provider
3563            .resolve_symbol(&fixture_path("sample.ts"), "greet")
3564            .unwrap();
3565        assert_eq!(matches.len(), 1);
3566        assert_eq!(matches[0].symbol.name, "greet");
3567        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
3568    }
3569
3570    #[test]
3571    fn resolve_symbol_not_found() {
3572        let provider = TreeSitterProvider::new();
3573        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
3574        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
3575    }
3576
3577    #[test]
3578    fn resolve_symbol_follows_reexport_chains() {
3579        let dir = tempfile::tempdir().unwrap();
3580        let config = dir.path().join("config.ts");
3581        let barrel1 = dir.path().join("barrel1.ts");
3582        let barrel2 = dir.path().join("barrel2.ts");
3583        let barrel3 = dir.path().join("barrel3.ts");
3584        let index = dir.path().join("index.ts");
3585
3586        std::fs::write(
3587            &config,
3588            "export class Config {}\nexport default class DefaultConfig {}\n",
3589        )
3590        .unwrap();
3591        std::fs::write(
3592            &barrel1,
3593            "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
3594        )
3595        .unwrap();
3596        std::fs::write(
3597            &barrel2,
3598            "export { Config as RenamedConfig } from './barrel1';\n",
3599        )
3600        .unwrap();
3601        std::fs::write(
3602            &barrel3,
3603            "export * from './barrel2';\nexport * from './barrel1';\n",
3604        )
3605        .unwrap();
3606        std::fs::write(
3607            &index,
3608            "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
3609        )
3610        .unwrap();
3611
3612        let provider = TreeSitterProvider::new();
3613        let config_canon = std::fs::canonicalize(&config).unwrap();
3614
3615        let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
3616        assert_eq!(direct.len(), 1);
3617        assert_eq!(direct[0].symbol.name, "Config");
3618        assert_eq!(direct[0].file, config_canon.display().to_string());
3619
3620        let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
3621        assert_eq!(renamed.len(), 1);
3622        assert_eq!(renamed[0].symbol.name, "Config");
3623        assert_eq!(renamed[0].file, config_canon.display().to_string());
3624
3625        let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
3626        assert_eq!(wildcard_chain.len(), 1);
3627        assert_eq!(wildcard_chain[0].symbol.name, "Config");
3628        assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
3629
3630        let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
3631        assert_eq!(wildcard_default.len(), 1);
3632        assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
3633        assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
3634
3635        let local = provider.resolve_symbol(&index, "Config").unwrap();
3636        assert_eq!(local.len(), 1);
3637        assert_eq!(local[0].symbol.name, "Config");
3638        assert_eq!(local[0].file, index.display().to_string());
3639    }
3640
3641    // --- Parse tree caching ---
3642
3643    #[test]
3644    fn symbol_range_includes_rust_attributes() {
3645        let dir = tempfile::tempdir().unwrap();
3646        let path = dir.path().join("test_attrs.rs");
3647        std::fs::write(
3648            &path,
3649            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
3650        )
3651        .unwrap();
3652
3653        let provider = TreeSitterProvider::new();
3654        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
3655        assert_eq!(matches.len(), 1);
3656        assert_eq!(
3657            matches[0].symbol.range.start_line, 0,
3658            "symbol range should include preceding /// doc comment, got start_line={}",
3659            matches[0].symbol.range.start_line
3660        );
3661    }
3662
3663    #[test]
3664    fn symbol_range_includes_go_doc_comment() {
3665        let dir = tempfile::tempdir().unwrap();
3666        let path = dir.path().join("test_doc.go");
3667        std::fs::write(
3668            &path,
3669            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
3670        )
3671        .unwrap();
3672
3673        let provider = TreeSitterProvider::new();
3674        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
3675        assert_eq!(matches.len(), 1);
3676        assert_eq!(
3677            matches[0].symbol.range.start_line, 2,
3678            "symbol range should include preceding doc comments, got start_line={}",
3679            matches[0].symbol.range.start_line
3680        );
3681    }
3682
3683    #[test]
3684    fn symbol_range_skips_unrelated_comments() {
3685        let dir = tempfile::tempdir().unwrap();
3686        let path = dir.path().join("test_gap.go");
3687        std::fs::write(
3688            &path,
3689            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
3690        )
3691        .unwrap();
3692
3693        let provider = TreeSitterProvider::new();
3694        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
3695        assert_eq!(matches.len(), 1);
3696        assert_eq!(
3697            matches[0].symbol.range.start_line, 4,
3698            "symbol range should NOT include comment separated by blank line, got start_line={}",
3699            matches[0].symbol.range.start_line
3700        );
3701    }
3702
3703    #[test]
3704    fn parse_cache_returns_same_tree() {
3705        let mut parser = FileParser::new();
3706        let path = fixture_path("sample.ts");
3707
3708        let (tree1, _) = parser.parse(&path).unwrap();
3709        let tree1_root = tree1.root_node().byte_range();
3710
3711        let (tree2, _) = parser.parse(&path).unwrap();
3712        let tree2_root = tree2.root_node().byte_range();
3713
3714        // Same tree (cache hit) should return identical root node range
3715        assert_eq!(tree1_root, tree2_root);
3716    }
3717
3718    // --- Python extraction ---
3719
3720    #[test]
3721    fn py_extracts_all_symbols() {
3722        let provider = TreeSitterProvider::new();
3723        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3724
3725        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3726        assert!(
3727            names.contains(&"top_level_function"),
3728            "missing top_level_function: {:?}",
3729            names
3730        );
3731        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
3732        assert!(
3733            names.contains(&"instance_method"),
3734            "missing method instance_method: {:?}",
3735            names
3736        );
3737        assert!(
3738            names.contains(&"decorated_function"),
3739            "missing decorated_function: {:?}",
3740            names
3741        );
3742
3743        // Plan requires ≥4 symbols
3744        assert!(
3745            symbols.len() >= 4,
3746            "expected ≥4 symbols, got {}: {:?}",
3747            symbols.len(),
3748            names
3749        );
3750    }
3751
3752    #[test]
3753    fn py_symbol_kinds_correct() {
3754        let provider = TreeSitterProvider::new();
3755        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3756
3757        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3758
3759        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
3760        assert_eq!(find("MyClass").kind, SymbolKind::Class);
3761        assert_eq!(find("instance_method").kind, SymbolKind::Method);
3762        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
3763        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
3764        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
3765        assert_eq!(find("inner_method").kind, SymbolKind::Method);
3766        assert_eq!(find("outer_method").kind, SymbolKind::Method);
3767    }
3768
3769    #[test]
3770    fn py_method_scope_chain() {
3771        let provider = TreeSitterProvider::new();
3772        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3773
3774        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3775
3776        // Method inside MyClass
3777        assert_eq!(
3778            find("instance_method").scope_chain,
3779            vec!["MyClass"],
3780            "instance_method should have MyClass in scope chain"
3781        );
3782        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
3783
3784        // Method inside OuterClass > InnerClass
3785        assert_eq!(
3786            find("inner_method").scope_chain,
3787            vec!["OuterClass", "InnerClass"],
3788            "inner_method should have nested scope chain"
3789        );
3790
3791        // InnerClass itself should have OuterClass in scope
3792        assert_eq!(
3793            find("InnerClass").scope_chain,
3794            vec!["OuterClass"],
3795            "InnerClass should have OuterClass in scope"
3796        );
3797
3798        // Top-level function has empty scope
3799        assert!(
3800            find("top_level_function").scope_chain.is_empty(),
3801            "top-level function should have empty scope chain"
3802        );
3803    }
3804
3805    #[test]
3806    fn py_decorated_function_signature() {
3807        let provider = TreeSitterProvider::new();
3808        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3809
3810        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3811
3812        let sig = find("decorated_function").signature.as_ref().unwrap();
3813        assert!(
3814            sig.contains("@staticmethod"),
3815            "decorated function signature should include decorator: {}",
3816            sig
3817        );
3818        assert!(
3819            sig.contains("def decorated_function"),
3820            "signature should include function def: {}",
3821            sig
3822        );
3823    }
3824
3825    #[test]
3826    fn py_ranges_valid() {
3827        let provider = TreeSitterProvider::new();
3828        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3829
3830        for s in &symbols {
3831            assert!(
3832                s.range.end_line >= s.range.start_line,
3833                "symbol {} has invalid range: {:?}",
3834                s.name,
3835                s.range
3836            );
3837        }
3838    }
3839
3840    // --- Rust extraction ---
3841
3842    #[test]
3843    fn rs_extracts_all_symbols() {
3844        let provider = TreeSitterProvider::new();
3845        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3846
3847        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3848        assert!(
3849            names.contains(&"public_function"),
3850            "missing public_function: {:?}",
3851            names
3852        );
3853        assert!(
3854            names.contains(&"private_function"),
3855            "missing private_function: {:?}",
3856            names
3857        );
3858        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
3859        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
3860        assert!(
3861            names.contains(&"Drawable"),
3862            "missing trait Drawable: {:?}",
3863            names
3864        );
3865        // impl methods
3866        assert!(
3867            names.contains(&"new"),
3868            "missing impl method new: {:?}",
3869            names
3870        );
3871
3872        // Plan requires ≥6 symbols
3873        assert!(
3874            symbols.len() >= 6,
3875            "expected ≥6 symbols, got {}: {:?}",
3876            symbols.len(),
3877            names
3878        );
3879    }
3880
3881    #[test]
3882    fn rs_symbol_kinds_correct() {
3883        let provider = TreeSitterProvider::new();
3884        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3885
3886        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3887
3888        assert_eq!(find("public_function").kind, SymbolKind::Function);
3889        assert_eq!(find("private_function").kind, SymbolKind::Function);
3890        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
3891        assert_eq!(find("Color").kind, SymbolKind::Enum);
3892        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
3893        assert_eq!(find("new").kind, SymbolKind::Method);
3894    }
3895
3896    #[test]
3897    fn rs_pub_export_detection() {
3898        let provider = TreeSitterProvider::new();
3899        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3900
3901        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3902
3903        assert!(
3904            find("public_function").exported,
3905            "pub fn should be exported"
3906        );
3907        assert!(
3908            !find("private_function").exported,
3909            "non-pub fn should not be exported"
3910        );
3911        assert!(find("MyStruct").exported, "pub struct should be exported");
3912        assert!(find("Color").exported, "pub enum should be exported");
3913        assert!(find("Drawable").exported, "pub trait should be exported");
3914        assert!(
3915            find("new").exported,
3916            "pub fn inside impl should be exported"
3917        );
3918        assert!(
3919            !find("helper").exported,
3920            "non-pub fn inside impl should not be exported"
3921        );
3922    }
3923
3924    #[test]
3925    fn rs_impl_method_scope_chain() {
3926        let provider = TreeSitterProvider::new();
3927        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3928
3929        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3930
3931        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
3932        assert_eq!(
3933            find("new").scope_chain,
3934            vec!["MyStruct"],
3935            "impl method should have type in scope chain"
3936        );
3937        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
3938
3939        // Free function has empty scope chain
3940        assert!(
3941            find("public_function").scope_chain.is_empty(),
3942            "free function should have empty scope chain"
3943        );
3944    }
3945
3946    #[test]
3947    fn rs_trait_impl_scope_chain() {
3948        let provider = TreeSitterProvider::new();
3949        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3950
3951        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
3952        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
3953        assert_eq!(
3954            draw.scope_chain,
3955            vec!["Drawable for MyStruct"],
3956            "trait impl method should have 'Trait for Type' scope"
3957        );
3958        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
3959    }
3960
3961    #[test]
3962    fn rs_ranges_valid() {
3963        let provider = TreeSitterProvider::new();
3964        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3965
3966        for s in &symbols {
3967            assert!(
3968                s.range.end_line >= s.range.start_line,
3969                "symbol {} has invalid range: {:?}",
3970                s.name,
3971                s.range
3972            );
3973        }
3974    }
3975
3976    // --- Go extraction ---
3977
3978    #[test]
3979    fn go_extracts_all_symbols() {
3980        let provider = TreeSitterProvider::new();
3981        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
3982
3983        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3984        assert!(
3985            names.contains(&"ExportedFunction"),
3986            "missing ExportedFunction: {:?}",
3987            names
3988        );
3989        assert!(
3990            names.contains(&"unexportedFunction"),
3991            "missing unexportedFunction: {:?}",
3992            names
3993        );
3994        assert!(
3995            names.contains(&"MyStruct"),
3996            "missing struct MyStruct: {:?}",
3997            names
3998        );
3999        assert!(
4000            names.contains(&"Reader"),
4001            "missing interface Reader: {:?}",
4002            names
4003        );
4004        // receiver method
4005        assert!(
4006            names.contains(&"String"),
4007            "missing receiver method String: {:?}",
4008            names
4009        );
4010
4011        // Plan requires ≥4 symbols
4012        assert!(
4013            symbols.len() >= 4,
4014            "expected ≥4 symbols, got {}: {:?}",
4015            symbols.len(),
4016            names
4017        );
4018    }
4019
4020    #[test]
4021    fn go_symbol_kinds_correct() {
4022        let provider = TreeSitterProvider::new();
4023        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4024
4025        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4026
4027        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
4028        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
4029        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
4030        assert_eq!(find("Reader").kind, SymbolKind::Interface);
4031        assert_eq!(find("String").kind, SymbolKind::Method);
4032        assert_eq!(find("helper").kind, SymbolKind::Method);
4033    }
4034
4035    #[test]
4036    fn go_uppercase_export_detection() {
4037        let provider = TreeSitterProvider::new();
4038        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4039
4040        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4041
4042        assert!(
4043            find("ExportedFunction").exported,
4044            "ExportedFunction (uppercase) should be exported"
4045        );
4046        assert!(
4047            !find("unexportedFunction").exported,
4048            "unexportedFunction (lowercase) should not be exported"
4049        );
4050        assert!(
4051            find("MyStruct").exported,
4052            "MyStruct (uppercase) should be exported"
4053        );
4054        assert!(
4055            find("Reader").exported,
4056            "Reader (uppercase) should be exported"
4057        );
4058        assert!(
4059            find("String").exported,
4060            "String method (uppercase) should be exported"
4061        );
4062        assert!(
4063            !find("helper").exported,
4064            "helper method (lowercase) should not be exported"
4065        );
4066    }
4067
4068    #[test]
4069    fn go_receiver_method_scope_chain() {
4070        let provider = TreeSitterProvider::new();
4071        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4072
4073        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4074
4075        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
4076        assert_eq!(
4077            find("String").scope_chain,
4078            vec!["MyStruct"],
4079            "receiver method should have type in scope chain"
4080        );
4081        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
4082
4083        // Regular function has empty scope chain
4084        assert!(
4085            find("ExportedFunction").scope_chain.is_empty(),
4086            "regular function should have empty scope chain"
4087        );
4088    }
4089
4090    #[test]
4091    fn go_ranges_valid() {
4092        let provider = TreeSitterProvider::new();
4093        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4094
4095        for s in &symbols {
4096            assert!(
4097                s.range.end_line >= s.range.start_line,
4098                "symbol {} has invalid range: {:?}",
4099                s.name,
4100                s.range
4101            );
4102        }
4103    }
4104
4105    // --- Cross-language ---
4106
4107    #[test]
4108    fn cross_language_all_six_produce_symbols() {
4109        let provider = TreeSitterProvider::new();
4110
4111        let fixtures = [
4112            ("sample.ts", "TypeScript"),
4113            ("sample.tsx", "TSX"),
4114            ("sample.js", "JavaScript"),
4115            ("sample.py", "Python"),
4116            ("sample.rs", "Rust"),
4117            ("sample.go", "Go"),
4118        ];
4119
4120        for (fixture, lang) in &fixtures {
4121            let symbols = provider
4122                .list_symbols(&fixture_path(fixture))
4123                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
4124            assert!(
4125                symbols.len() >= 2,
4126                "{} should produce ≥2 symbols, got {}: {:?}",
4127                lang,
4128                symbols.len(),
4129                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
4130            );
4131        }
4132    }
4133
4134    // --- Symbol cache tests ---
4135
4136    #[test]
4137    fn symbol_cache_returns_cached_results_on_second_call() {
4138        let dir = tempfile::tempdir().unwrap();
4139        let file = dir.path().join("test.rs");
4140        std::fs::write(&file, "pub fn hello() {}\npub fn world() {}").unwrap();
4141
4142        let mut parser = FileParser::new();
4143
4144        let symbols1 = parser.extract_symbols(&file).unwrap();
4145        assert_eq!(symbols1.len(), 2);
4146
4147        // Second call should return cached result
4148        let symbols2 = parser.extract_symbols(&file).unwrap();
4149        assert_eq!(symbols2.len(), 2);
4150        assert_eq!(symbols1[0].name, symbols2[0].name);
4151
4152        // Verify cache is populated
4153        assert!(parser.symbol_cache.contains_key(&file));
4154    }
4155
4156    #[test]
4157    fn symbol_cache_invalidates_on_file_change() {
4158        let dir = tempfile::tempdir().unwrap();
4159        let file = dir.path().join("test.rs");
4160        std::fs::write(&file, "pub fn hello() {}").unwrap();
4161
4162        let mut parser = FileParser::new();
4163
4164        let symbols1 = parser.extract_symbols(&file).unwrap();
4165        assert_eq!(symbols1.len(), 1);
4166        assert_eq!(symbols1[0].name, "hello");
4167
4168        // Wait to ensure mtime changes (filesystem resolution can be 1s on some OS)
4169        std::thread::sleep(std::time::Duration::from_millis(50));
4170
4171        // Modify file — add a second function
4172        std::fs::write(&file, "pub fn hello() {}\npub fn goodbye() {}").unwrap();
4173
4174        // Should detect mtime change and re-extract
4175        let symbols2 = parser.extract_symbols(&file).unwrap();
4176        assert_eq!(symbols2.len(), 2);
4177        assert!(symbols2.iter().any(|s| s.name == "goodbye"));
4178    }
4179
4180    #[test]
4181    fn symbol_cache_invalidate_method_clears_entry() {
4182        let dir = tempfile::tempdir().unwrap();
4183        let file = dir.path().join("test.rs");
4184        std::fs::write(&file, "pub fn hello() {}").unwrap();
4185
4186        let mut parser = FileParser::new();
4187        parser.extract_symbols(&file).unwrap();
4188        assert!(parser.symbol_cache.contains_key(&file));
4189
4190        parser.invalidate_symbols(&file);
4191        assert!(!parser.symbol_cache.contains_key(&file));
4192        // Parse tree cache should also be cleared
4193        assert!(!parser.cache.contains_key(&file));
4194    }
4195
4196    #[test]
4197    fn symbol_cache_works_for_multiple_languages() {
4198        let dir = tempfile::tempdir().unwrap();
4199        let rs_file = dir.path().join("lib.rs");
4200        let ts_file = dir.path().join("app.ts");
4201        let py_file = dir.path().join("main.py");
4202
4203        std::fs::write(&rs_file, "pub fn rust_fn() {}").unwrap();
4204        std::fs::write(&ts_file, "export function tsFn() {}").unwrap();
4205        std::fs::write(&py_file, "def py_fn():\n    pass").unwrap();
4206
4207        let mut parser = FileParser::new();
4208
4209        let rs_syms = parser.extract_symbols(&rs_file).unwrap();
4210        let ts_syms = parser.extract_symbols(&ts_file).unwrap();
4211        let py_syms = parser.extract_symbols(&py_file).unwrap();
4212
4213        assert!(rs_syms.iter().any(|s| s.name == "rust_fn"));
4214        assert!(ts_syms.iter().any(|s| s.name == "tsFn"));
4215        assert!(py_syms.iter().any(|s| s.name == "py_fn"));
4216
4217        // All should be cached now
4218        assert_eq!(parser.symbol_cache.len(), 3);
4219
4220        // Re-extract should return same results from cache
4221        let rs_syms2 = parser.extract_symbols(&rs_file).unwrap();
4222        assert_eq!(rs_syms.len(), rs_syms2.len());
4223    }
4224}