Skip to main content

aft/
parser.rs

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