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    let total_lines = source.lines().count() as u32;
2591
2592    // Extend each heading's end_line to just before the next heading at the
2593    // same or shallower level (or EOF). This makes aft_zoom return the full
2594    // section content rather than just the heading element's single line.
2595    for i in 0..headings.len() {
2596        let level = headings[i].0;
2597        let section_end = headings[i + 1..]
2598            .iter()
2599            .find(|(l, _)| *l <= level)
2600            .map(|(_, s)| s.range.start_line.saturating_sub(1))
2601            .unwrap_or_else(|| total_lines.saturating_sub(1));
2602        headings[i].1.range.end_line = section_end;
2603        if section_end != headings[i].1.range.start_line {
2604            headings[i].1.range.end_col = 0;
2605        }
2606    }
2607
2608    // Build hierarchy: assign scope_chain and parent based on heading level
2609    let mut scope_stack: Vec<(u8, String)> = Vec::new(); // (level, name)
2610    for (level, symbol) in headings.iter_mut() {
2611        // Pop scope entries that are at the same level or deeper
2612        while scope_stack.last().is_some_and(|(l, _)| *l >= *level) {
2613            scope_stack.pop();
2614        }
2615        symbol.scope_chain = scope_stack.iter().map(|(_, name)| name.clone()).collect();
2616        symbol.parent = scope_stack.last().map(|(_, name)| name.clone());
2617        scope_stack.push((*level, symbol.name.clone()));
2618    }
2619
2620    Ok(headings.into_iter().map(|(_, s)| s).collect())
2621}
2622
2623/// Recursively collect h1-h6 elements from the HTML tree.
2624fn collect_html_headings(source: &str, node: &Node, headings: &mut Vec<(u8, Symbol)>) {
2625    let mut cursor = node.walk();
2626    if !cursor.goto_first_child() {
2627        return;
2628    }
2629
2630    loop {
2631        let child = cursor.node();
2632        if child.kind() == "element" {
2633            // Check if this element's start tag is h1-h6
2634            if let Some(start_tag) = child
2635                .child_by_field_name("start_tag")
2636                .or_else(|| child.child(0).filter(|c| c.kind() == "start_tag"))
2637            {
2638                if let Some(tag_name_node) = start_tag
2639                    .child_by_field_name("tag_name")
2640                    .or_else(|| start_tag.child(1).filter(|c| c.kind() == "tag_name"))
2641                {
2642                    let tag_name = node_text(source, &tag_name_node).to_lowercase();
2643                    if let Some(level) = match tag_name.as_str() {
2644                        "h1" => Some(1u8),
2645                        "h2" => Some(2),
2646                        "h3" => Some(3),
2647                        "h4" => Some(4),
2648                        "h5" => Some(5),
2649                        "h6" => Some(6),
2650                        _ => None,
2651                    } {
2652                        // Extract text content from the element
2653                        let text = extract_element_text(source, &child).trim().to_string();
2654                        if !text.is_empty() {
2655                            let range = node_range(&child);
2656                            let signature = format!("<h{}> {}", level, text);
2657                            headings.push((
2658                                level,
2659                                Symbol {
2660                                    name: text,
2661                                    kind: SymbolKind::Heading,
2662                                    range,
2663                                    signature: Some(signature),
2664                                    scope_chain: vec![], // filled later
2665                                    exported: false,
2666                                    parent: None, // filled later
2667                                },
2668                            ));
2669                        }
2670                    }
2671                }
2672            }
2673            // Recurse into element children (nested headings)
2674            collect_html_headings(source, &child, headings);
2675        } else {
2676            // Recurse into other node types (document, body, etc.)
2677            collect_html_headings(source, &child, headings);
2678        }
2679
2680        if !cursor.goto_next_sibling() {
2681            break;
2682        }
2683    }
2684}
2685
2686/// Extract text content from an HTML element, stripping tags.
2687fn extract_element_text(source: &str, node: &Node) -> String {
2688    let mut text = String::new();
2689    let mut cursor = node.walk();
2690    if !cursor.goto_first_child() {
2691        return text;
2692    }
2693    loop {
2694        let child = cursor.node();
2695        match child.kind() {
2696            "text" => {
2697                text.push_str(node_text(source, &child));
2698            }
2699            "element" => {
2700                // Recurse into nested elements (e.g., <strong>, <em>, <a>)
2701                text.push_str(&extract_element_text(source, &child));
2702            }
2703            _ => {}
2704        }
2705        if !cursor.goto_next_sibling() {
2706            break;
2707        }
2708    }
2709    text
2710}
2711
2712/// Extract markdown headings as symbols.
2713/// Each heading becomes a symbol with kind `Heading`, and its range covers the entire
2714/// section (from the heading to the next heading at the same or higher level, or EOF).
2715fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
2716    let mut symbols = Vec::new();
2717    extract_md_sections(source, root, &mut symbols, &[]);
2718    Ok(symbols)
2719}
2720
2721/// Recursively walk `section` nodes to build the heading hierarchy.
2722fn extract_md_sections(
2723    source: &str,
2724    node: &Node,
2725    symbols: &mut Vec<Symbol>,
2726    scope_chain: &[String],
2727) {
2728    let mut cursor = node.walk();
2729    if !cursor.goto_first_child() {
2730        return;
2731    }
2732
2733    loop {
2734        let child = cursor.node();
2735        match child.kind() {
2736            "section" => {
2737                // A section contains an atx_heading as its first child,
2738                // followed by content and possibly nested sections.
2739                let mut section_cursor = child.walk();
2740                let mut heading_name = String::new();
2741                let mut heading_level: u8 = 0;
2742
2743                if section_cursor.goto_first_child() {
2744                    loop {
2745                        let section_child = section_cursor.node();
2746                        if section_child.kind() == "atx_heading" {
2747                            // Extract heading level from marker type
2748                            let mut h_cursor = section_child.walk();
2749                            if h_cursor.goto_first_child() {
2750                                loop {
2751                                    let h_child = h_cursor.node();
2752                                    let kind = h_child.kind();
2753                                    if kind.starts_with("atx_h") && kind.ends_with("_marker") {
2754                                        // "atx_h1_marker" → level 1, "atx_h2_marker" → level 2, etc.
2755                                        heading_level = kind
2756                                            .strip_prefix("atx_h")
2757                                            .and_then(|s| s.strip_suffix("_marker"))
2758                                            .and_then(|s| s.parse::<u8>().ok())
2759                                            .unwrap_or(1);
2760                                    } else if h_child.kind() == "inline" {
2761                                        heading_name =
2762                                            node_text(source, &h_child).trim().to_string();
2763                                    }
2764                                    if !h_cursor.goto_next_sibling() {
2765                                        break;
2766                                    }
2767                                }
2768                            }
2769                        }
2770                        if !section_cursor.goto_next_sibling() {
2771                            break;
2772                        }
2773                    }
2774                }
2775
2776                if !heading_name.is_empty() {
2777                    let range = node_range(&child);
2778                    let signature = format!(
2779                        "{} {}",
2780                        "#".repeat((heading_level as usize).min(6)),
2781                        heading_name
2782                    );
2783
2784                    symbols.push(Symbol {
2785                        name: heading_name.clone(),
2786                        kind: SymbolKind::Heading,
2787                        range,
2788                        signature: Some(signature),
2789                        scope_chain: scope_chain.to_vec(),
2790                        exported: false,
2791                        parent: scope_chain.last().cloned(),
2792                    });
2793
2794                    // Recurse into the section for nested headings
2795                    let mut new_scope = scope_chain.to_vec();
2796                    new_scope.push(heading_name);
2797                    extract_md_sections(source, &child, symbols, &new_scope);
2798                }
2799            }
2800            _ => {}
2801        }
2802
2803        if !cursor.goto_next_sibling() {
2804            break;
2805        }
2806    }
2807}
2808
2809/// Remove duplicate symbols based on (name, kind, start_line).
2810/// Class declarations can match both "class" and "method" patterns,
2811/// producing duplicates.
2812fn dedup_symbols(symbols: &mut Vec<Symbol>) {
2813    let mut seen = std::collections::HashSet::new();
2814    symbols.retain(|s| {
2815        let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
2816        seen.insert(key)
2817    });
2818}
2819
2820/// Provider that uses tree-sitter for real symbol extraction.
2821/// Implements the `LanguageProvider` trait from `language.rs`.
2822pub struct TreeSitterProvider {
2823    parser: RefCell<FileParser>,
2824}
2825
2826#[derive(Debug, Clone)]
2827struct ReExportTarget {
2828    file: PathBuf,
2829    symbol_name: String,
2830}
2831
2832impl TreeSitterProvider {
2833    /// Create a new `TreeSitterProvider` backed by a fresh `FileParser`.
2834    pub fn new() -> Self {
2835        Self {
2836            parser: RefCell::new(FileParser::new()),
2837        }
2838    }
2839
2840    /// Merge a pre-warmed symbol cache into the parser.
2841    /// Called from the main loop when the background indexer completes.
2842    pub fn merge_warm_cache(&self, cache: SymbolCache) {
2843        let mut parser = self.parser.borrow_mut();
2844        parser.set_warm_cache(cache);
2845    }
2846
2847    /// Return (local_cache_entries, warm_cache_entries) for status reporting.
2848    pub fn symbol_cache_stats(&self) -> (usize, usize) {
2849        let parser = self.parser.borrow();
2850        let local = parser.symbol_cache_len();
2851        let warm = parser.warm_cache_len();
2852        (local, warm)
2853    }
2854
2855    fn resolve_symbol_inner(
2856        &self,
2857        file: &Path,
2858        name: &str,
2859        depth: usize,
2860        visited: &mut HashSet<(PathBuf, String)>,
2861    ) -> Result<Vec<SymbolMatch>, AftError> {
2862        if depth > MAX_REEXPORT_DEPTH {
2863            return Ok(Vec::new());
2864        }
2865
2866        let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
2867        if !visited.insert((canonical_file, name.to_string())) {
2868            return Ok(Vec::new());
2869        }
2870
2871        let symbols = self.parser.borrow_mut().extract_symbols(file)?;
2872        let local_matches = symbol_matches_in_file(file, &symbols, name);
2873        if !local_matches.is_empty() {
2874            return Ok(local_matches);
2875        }
2876
2877        if name == "default" {
2878            let default_matches = self.resolve_local_default_export(file, &symbols)?;
2879            if !default_matches.is_empty() {
2880                return Ok(default_matches);
2881            }
2882        }
2883
2884        let reexport_targets = self.collect_reexport_targets(file, name)?;
2885        let mut matches = Vec::new();
2886        let mut seen = HashSet::new();
2887        for target in reexport_targets {
2888            for resolved in
2889                self.resolve_symbol_inner(&target.file, &target.symbol_name, depth + 1, visited)?
2890            {
2891                let key = format!(
2892                    "{}:{}:{}:{}:{}:{}",
2893                    resolved.file,
2894                    resolved.symbol.name,
2895                    resolved.symbol.range.start_line,
2896                    resolved.symbol.range.start_col,
2897                    resolved.symbol.range.end_line,
2898                    resolved.symbol.range.end_col
2899                );
2900                if seen.insert(key) {
2901                    matches.push(resolved);
2902                }
2903            }
2904        }
2905
2906        Ok(matches)
2907    }
2908
2909    fn collect_reexport_targets(
2910        &self,
2911        file: &Path,
2912        requested_name: &str,
2913    ) -> Result<Vec<ReExportTarget>, AftError> {
2914        let (source, tree, lang) = self.read_parsed_file(file)?;
2915        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
2916            return Ok(Vec::new());
2917        }
2918
2919        let mut targets = Vec::new();
2920        let root = tree.root_node();
2921        let from_dir = file.parent().unwrap_or_else(|| Path::new("."));
2922
2923        let mut cursor = root.walk();
2924        if !cursor.goto_first_child() {
2925            return Ok(targets);
2926        }
2927
2928        loop {
2929            let node = cursor.node();
2930            if node.kind() == "export_statement" {
2931                let Some(source_node) = node.child_by_field_name("source") else {
2932                    if !cursor.goto_next_sibling() {
2933                        break;
2934                    }
2935                    continue;
2936                };
2937
2938                let Some(module_path) = string_content(&source, &source_node) else {
2939                    if !cursor.goto_next_sibling() {
2940                        break;
2941                    }
2942                    continue;
2943                };
2944
2945                let Some(target_file) = resolve_module_path(from_dir, &module_path) else {
2946                    if !cursor.goto_next_sibling() {
2947                        break;
2948                    }
2949                    continue;
2950                };
2951
2952                if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
2953                    if let Some(symbol_name) =
2954                        resolve_export_clause_name(&source, &export_clause, requested_name)
2955                    {
2956                        targets.push(ReExportTarget {
2957                            file: target_file,
2958                            symbol_name,
2959                        });
2960                    }
2961                } else if export_statement_has_wildcard(&source, &node) {
2962                    targets.push(ReExportTarget {
2963                        file: target_file,
2964                        symbol_name: requested_name.to_string(),
2965                    });
2966                }
2967            }
2968
2969            if !cursor.goto_next_sibling() {
2970                break;
2971            }
2972        }
2973
2974        Ok(targets)
2975    }
2976
2977    fn resolve_local_default_export(
2978        &self,
2979        file: &Path,
2980        symbols: &[Symbol],
2981    ) -> Result<Vec<SymbolMatch>, AftError> {
2982        let (source, tree, lang) = self.read_parsed_file(file)?;
2983        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
2984            return Ok(Vec::new());
2985        }
2986
2987        let root = tree.root_node();
2988        let mut matches = Vec::new();
2989        let mut seen = HashSet::new();
2990
2991        let mut cursor = root.walk();
2992        if !cursor.goto_first_child() {
2993            return Ok(matches);
2994        }
2995
2996        loop {
2997            let node = cursor.node();
2998            if node.kind() == "export_statement"
2999                && node.child_by_field_name("source").is_none()
3000                && node_contains_token(&source, &node, "default")
3001            {
3002                if let Some(target_name) = default_export_target_name(&source, &node) {
3003                    for symbol_match in symbol_matches_in_file(file, symbols, &target_name) {
3004                        let key = format!(
3005                            "{}:{}:{}:{}:{}:{}",
3006                            symbol_match.file,
3007                            symbol_match.symbol.name,
3008                            symbol_match.symbol.range.start_line,
3009                            symbol_match.symbol.range.start_col,
3010                            symbol_match.symbol.range.end_line,
3011                            symbol_match.symbol.range.end_col
3012                        );
3013                        if seen.insert(key) {
3014                            matches.push(symbol_match);
3015                        }
3016                    }
3017                }
3018            }
3019
3020            if !cursor.goto_next_sibling() {
3021                break;
3022            }
3023        }
3024
3025        Ok(matches)
3026    }
3027
3028    fn read_parsed_file(&self, file: &Path) -> Result<(String, Tree, LangId), AftError> {
3029        let source = std::fs::read_to_string(file).map_err(|e| AftError::FileNotFound {
3030            path: format!("{}: {}", file.display(), e),
3031        })?;
3032        let (tree, lang) = {
3033            let mut parser = self.parser.borrow_mut();
3034            parser.parse_cloned(file)?
3035        };
3036        Ok((source, tree, lang))
3037    }
3038}
3039
3040fn symbol_matches_in_file(file: &Path, symbols: &[Symbol], name: &str) -> Vec<SymbolMatch> {
3041    symbols
3042        .iter()
3043        .filter(|symbol| symbol.name == name)
3044        .cloned()
3045        .map(|symbol| SymbolMatch {
3046            file: file.display().to_string(),
3047            symbol,
3048        })
3049        .collect()
3050}
3051
3052fn string_content(source: &str, node: &Node) -> Option<String> {
3053    let text = node_text(source, node);
3054    if text.len() < 2 {
3055        return None;
3056    }
3057
3058    Some(
3059        text.trim_start_matches(|c| c == '\'' || c == '"')
3060            .trim_end_matches(|c| c == '\'' || c == '"')
3061            .to_string(),
3062    )
3063}
3064
3065fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
3066    let mut cursor = node.walk();
3067    if !cursor.goto_first_child() {
3068        return None;
3069    }
3070
3071    loop {
3072        let child = cursor.node();
3073        if child.kind() == kind {
3074            return Some(child);
3075        }
3076        if !cursor.goto_next_sibling() {
3077            break;
3078        }
3079    }
3080
3081    None
3082}
3083
3084fn resolve_export_clause_name(
3085    source: &str,
3086    export_clause: &Node,
3087    requested_name: &str,
3088) -> Option<String> {
3089    let mut cursor = export_clause.walk();
3090    if !cursor.goto_first_child() {
3091        return None;
3092    }
3093
3094    loop {
3095        let child = cursor.node();
3096        if child.kind() == "export_specifier" {
3097            let (source_name, exported_name) = export_specifier_names(source, &child)?;
3098            if exported_name == requested_name {
3099                return Some(source_name);
3100            }
3101        }
3102
3103        if !cursor.goto_next_sibling() {
3104            break;
3105        }
3106    }
3107
3108    None
3109}
3110
3111fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
3112    let source_name = specifier
3113        .child_by_field_name("name")
3114        .map(|node| node_text(source, &node).to_string());
3115    let alias_name = specifier
3116        .child_by_field_name("alias")
3117        .map(|node| node_text(source, &node).to_string());
3118
3119    if let Some(source_name) = source_name {
3120        let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
3121        return Some((source_name, exported_name));
3122    }
3123
3124    let mut names = Vec::new();
3125    let mut cursor = specifier.walk();
3126    if cursor.goto_first_child() {
3127        loop {
3128            let child = cursor.node();
3129            let child_text = node_text(source, &child).trim();
3130            if matches!(
3131                child.kind(),
3132                "identifier" | "type_identifier" | "property_identifier"
3133            ) || child_text == "default"
3134            {
3135                names.push(child_text.to_string());
3136            }
3137            if !cursor.goto_next_sibling() {
3138                break;
3139            }
3140        }
3141    }
3142
3143    match names.as_slice() {
3144        [name] => Some((name.clone(), name.clone())),
3145        [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
3146        _ => None,
3147    }
3148}
3149
3150fn export_statement_has_wildcard(source: &str, node: &Node) -> 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() == "*" {
3158            return true;
3159        }
3160        if !cursor.goto_next_sibling() {
3161            break;
3162        }
3163    }
3164
3165    false
3166}
3167
3168fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
3169    let mut cursor = node.walk();
3170    if !cursor.goto_first_child() {
3171        return false;
3172    }
3173
3174    loop {
3175        if node_text(source, &cursor.node()).trim() == token {
3176            return true;
3177        }
3178        if !cursor.goto_next_sibling() {
3179            break;
3180        }
3181    }
3182
3183    false
3184}
3185
3186fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
3187    let mut cursor = export_stmt.walk();
3188    if !cursor.goto_first_child() {
3189        return None;
3190    }
3191
3192    loop {
3193        let child = cursor.node();
3194        match child.kind() {
3195            "function_declaration"
3196            | "class_declaration"
3197            | "interface_declaration"
3198            | "enum_declaration"
3199            | "type_alias_declaration"
3200            | "lexical_declaration" => {
3201                if let Some(name_node) = child.child_by_field_name("name") {
3202                    return Some(node_text(source, &name_node).to_string());
3203                }
3204
3205                if child.kind() == "lexical_declaration" {
3206                    let mut child_cursor = child.walk();
3207                    if child_cursor.goto_first_child() {
3208                        loop {
3209                            let nested = child_cursor.node();
3210                            if nested.kind() == "variable_declarator" {
3211                                if let Some(name_node) = nested.child_by_field_name("name") {
3212                                    return Some(node_text(source, &name_node).to_string());
3213                                }
3214                            }
3215                            if !child_cursor.goto_next_sibling() {
3216                                break;
3217                            }
3218                        }
3219                    }
3220                }
3221            }
3222            "identifier" | "type_identifier" => {
3223                let text = node_text(source, &child);
3224                if text != "export" && text != "default" {
3225                    return Some(text.to_string());
3226                }
3227            }
3228            _ => {}
3229        }
3230
3231        if !cursor.goto_next_sibling() {
3232            break;
3233        }
3234    }
3235
3236    None
3237}
3238
3239impl crate::language::LanguageProvider for TreeSitterProvider {
3240    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
3241        let matches = self.resolve_symbol_inner(file, name, 0, &mut HashSet::new())?;
3242
3243        if matches.is_empty() {
3244            Err(AftError::SymbolNotFound {
3245                name: name.to_string(),
3246                file: file.display().to_string(),
3247            })
3248        } else {
3249            Ok(matches)
3250        }
3251    }
3252
3253    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
3254        self.parser.borrow_mut().extract_symbols(file)
3255    }
3256
3257    fn as_any(&self) -> &dyn std::any::Any {
3258        self
3259    }
3260}
3261
3262#[cfg(test)]
3263mod tests {
3264    use super::*;
3265    use crate::language::LanguageProvider;
3266    use std::path::PathBuf;
3267
3268    fn fixture_path(name: &str) -> PathBuf {
3269        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3270            .join("tests")
3271            .join("fixtures")
3272            .join(name)
3273    }
3274
3275    // --- Language detection ---
3276
3277    #[test]
3278    fn detect_ts() {
3279        assert_eq!(
3280            detect_language(Path::new("foo.ts")),
3281            Some(LangId::TypeScript)
3282        );
3283    }
3284
3285    #[test]
3286    fn detect_tsx() {
3287        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
3288    }
3289
3290    #[test]
3291    fn detect_js() {
3292        assert_eq!(
3293            detect_language(Path::new("foo.js")),
3294            Some(LangId::JavaScript)
3295        );
3296    }
3297
3298    #[test]
3299    fn detect_jsx() {
3300        assert_eq!(
3301            detect_language(Path::new("foo.jsx")),
3302            Some(LangId::JavaScript)
3303        );
3304    }
3305
3306    #[test]
3307    fn detect_py() {
3308        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
3309    }
3310
3311    #[test]
3312    fn detect_rs() {
3313        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
3314    }
3315
3316    #[test]
3317    fn detect_go() {
3318        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
3319    }
3320
3321    #[test]
3322    fn detect_c() {
3323        assert_eq!(detect_language(Path::new("foo.c")), Some(LangId::C));
3324    }
3325
3326    #[test]
3327    fn detect_h() {
3328        assert_eq!(detect_language(Path::new("foo.h")), Some(LangId::C));
3329    }
3330
3331    #[test]
3332    fn detect_cc() {
3333        assert_eq!(detect_language(Path::new("foo.cc")), Some(LangId::Cpp));
3334    }
3335
3336    #[test]
3337    fn detect_cpp() {
3338        assert_eq!(detect_language(Path::new("foo.cpp")), Some(LangId::Cpp));
3339    }
3340
3341    #[test]
3342    fn detect_cxx() {
3343        assert_eq!(detect_language(Path::new("foo.cxx")), Some(LangId::Cpp));
3344    }
3345
3346    #[test]
3347    fn detect_hpp() {
3348        assert_eq!(detect_language(Path::new("foo.hpp")), Some(LangId::Cpp));
3349    }
3350
3351    #[test]
3352    fn detect_hh() {
3353        assert_eq!(detect_language(Path::new("foo.hh")), Some(LangId::Cpp));
3354    }
3355
3356    #[test]
3357    fn detect_zig() {
3358        assert_eq!(detect_language(Path::new("foo.zig")), Some(LangId::Zig));
3359    }
3360
3361    #[test]
3362    fn detect_cs() {
3363        assert_eq!(detect_language(Path::new("foo.cs")), Some(LangId::CSharp));
3364    }
3365
3366    #[test]
3367    fn detect_unknown_returns_none() {
3368        assert_eq!(detect_language(Path::new("foo.txt")), None);
3369    }
3370
3371    // --- Unsupported extension error ---
3372
3373    #[test]
3374    fn unsupported_extension_returns_invalid_request() {
3375        // Use a file that exists but has an unsupported extension
3376        let path = fixture_path("sample.ts");
3377        let bad_path = path.with_extension("txt");
3378        // Create a dummy file so the error comes from language detection, not I/O
3379        std::fs::write(&bad_path, "hello").unwrap();
3380        let provider = TreeSitterProvider::new();
3381        let result = provider.list_symbols(&bad_path);
3382        std::fs::remove_file(&bad_path).ok();
3383        match result {
3384            Err(AftError::InvalidRequest { message }) => {
3385                assert!(
3386                    message.contains("unsupported file extension"),
3387                    "msg: {}",
3388                    message
3389                );
3390                assert!(message.contains("txt"), "msg: {}", message);
3391            }
3392            other => panic!("expected InvalidRequest, got {:?}", other),
3393        }
3394    }
3395
3396    // --- TypeScript extraction ---
3397
3398    #[test]
3399    fn ts_extracts_all_symbol_kinds() {
3400        let provider = TreeSitterProvider::new();
3401        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3402
3403        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3404        assert!(
3405            names.contains(&"greet"),
3406            "missing function greet: {:?}",
3407            names
3408        );
3409        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
3410        assert!(
3411            names.contains(&"UserService"),
3412            "missing class UserService: {:?}",
3413            names
3414        );
3415        assert!(
3416            names.contains(&"Config"),
3417            "missing interface Config: {:?}",
3418            names
3419        );
3420        assert!(
3421            names.contains(&"Status"),
3422            "missing enum Status: {:?}",
3423            names
3424        );
3425        assert!(
3426            names.contains(&"UserId"),
3427            "missing type alias UserId: {:?}",
3428            names
3429        );
3430        assert!(
3431            names.contains(&"internalHelper"),
3432            "missing non-exported fn: {:?}",
3433            names
3434        );
3435
3436        // At least 6 unique symbols as required
3437        assert!(
3438            symbols.len() >= 6,
3439            "expected ≥6 symbols, got {}: {:?}",
3440            symbols.len(),
3441            names
3442        );
3443    }
3444
3445    #[test]
3446    fn ts_symbol_kinds_correct() {
3447        let provider = TreeSitterProvider::new();
3448        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3449
3450        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3451
3452        assert_eq!(find("greet").kind, SymbolKind::Function);
3453        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
3454        assert_eq!(find("UserService").kind, SymbolKind::Class);
3455        assert_eq!(find("Config").kind, SymbolKind::Interface);
3456        assert_eq!(find("Status").kind, SymbolKind::Enum);
3457        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
3458    }
3459
3460    #[test]
3461    fn ts_export_detection() {
3462        let provider = TreeSitterProvider::new();
3463        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3464
3465        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3466
3467        assert!(find("greet").exported, "greet should be exported");
3468        assert!(find("add").exported, "add should be exported");
3469        assert!(
3470            find("UserService").exported,
3471            "UserService should be exported"
3472        );
3473        assert!(find("Config").exported, "Config should be exported");
3474        assert!(find("Status").exported, "Status should be exported");
3475        assert!(
3476            !find("internalHelper").exported,
3477            "internalHelper should not be exported"
3478        );
3479    }
3480
3481    #[test]
3482    fn ts_method_scope_chain() {
3483        let provider = TreeSitterProvider::new();
3484        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3485
3486        let methods: Vec<&Symbol> = symbols
3487            .iter()
3488            .filter(|s| s.kind == SymbolKind::Method)
3489            .collect();
3490        assert!(!methods.is_empty(), "should have at least one method");
3491
3492        for method in &methods {
3493            assert_eq!(
3494                method.scope_chain,
3495                vec!["UserService"],
3496                "method {} should have UserService in scope chain",
3497                method.name
3498            );
3499            assert_eq!(method.parent.as_deref(), Some("UserService"));
3500        }
3501    }
3502
3503    #[test]
3504    fn ts_signatures_present() {
3505        let provider = TreeSitterProvider::new();
3506        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3507
3508        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3509
3510        let greet_sig = find("greet").signature.as_ref().unwrap();
3511        assert!(
3512            greet_sig.contains("greet"),
3513            "signature should contain function name: {}",
3514            greet_sig
3515        );
3516    }
3517
3518    #[test]
3519    fn ts_ranges_valid() {
3520        let provider = TreeSitterProvider::new();
3521        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
3522
3523        for s in &symbols {
3524            assert!(
3525                s.range.end_line >= s.range.start_line,
3526                "symbol {} has invalid range: {:?}",
3527                s.name,
3528                s.range
3529            );
3530        }
3531    }
3532
3533    // --- JavaScript extraction ---
3534
3535    #[test]
3536    fn js_extracts_core_symbols() {
3537        let provider = TreeSitterProvider::new();
3538        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3539
3540        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3541        assert!(
3542            names.contains(&"multiply"),
3543            "missing function multiply: {:?}",
3544            names
3545        );
3546        assert!(
3547            names.contains(&"divide"),
3548            "missing arrow fn divide: {:?}",
3549            names
3550        );
3551        assert!(
3552            names.contains(&"EventEmitter"),
3553            "missing class EventEmitter: {:?}",
3554            names
3555        );
3556        assert!(
3557            names.contains(&"main"),
3558            "missing default export fn main: {:?}",
3559            names
3560        );
3561
3562        assert!(
3563            symbols.len() >= 4,
3564            "expected ≥4 symbols, got {}: {:?}",
3565            symbols.len(),
3566            names
3567        );
3568    }
3569
3570    #[test]
3571    fn js_arrow_fn_correctly_named() {
3572        let provider = TreeSitterProvider::new();
3573        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3574
3575        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
3576        assert_eq!(divide.kind, SymbolKind::Function);
3577        assert!(divide.exported, "divide should be exported");
3578
3579        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
3580        assert_eq!(internal.kind, SymbolKind::Function);
3581        assert!(!internal.exported, "internalUtil should not be exported");
3582    }
3583
3584    #[test]
3585    fn js_method_scope_chain() {
3586        let provider = TreeSitterProvider::new();
3587        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
3588
3589        let methods: Vec<&Symbol> = symbols
3590            .iter()
3591            .filter(|s| s.kind == SymbolKind::Method)
3592            .collect();
3593
3594        for method in &methods {
3595            assert_eq!(
3596                method.scope_chain,
3597                vec!["EventEmitter"],
3598                "method {} should have EventEmitter in scope chain",
3599                method.name
3600            );
3601        }
3602    }
3603
3604    // --- TSX extraction ---
3605
3606    #[test]
3607    fn tsx_extracts_react_component() {
3608        let provider = TreeSitterProvider::new();
3609        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
3610
3611        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3612        assert!(
3613            names.contains(&"Button"),
3614            "missing React component Button: {:?}",
3615            names
3616        );
3617        assert!(
3618            names.contains(&"Counter"),
3619            "missing class Counter: {:?}",
3620            names
3621        );
3622        assert!(
3623            names.contains(&"formatLabel"),
3624            "missing function formatLabel: {:?}",
3625            names
3626        );
3627
3628        assert!(
3629            symbols.len() >= 2,
3630            "expected ≥2 symbols, got {}: {:?}",
3631            symbols.len(),
3632            names
3633        );
3634    }
3635
3636    #[test]
3637    fn tsx_jsx_doesnt_break_parser() {
3638        // Main assertion: TSX grammar handles JSX without errors
3639        let provider = TreeSitterProvider::new();
3640        let result = provider.list_symbols(&fixture_path("sample.tsx"));
3641        assert!(
3642            result.is_ok(),
3643            "TSX parsing should succeed: {:?}",
3644            result.err()
3645        );
3646    }
3647
3648    // --- resolve_symbol ---
3649
3650    #[test]
3651    fn resolve_symbol_finds_match() {
3652        let provider = TreeSitterProvider::new();
3653        let matches = provider
3654            .resolve_symbol(&fixture_path("sample.ts"), "greet")
3655            .unwrap();
3656        assert_eq!(matches.len(), 1);
3657        assert_eq!(matches[0].symbol.name, "greet");
3658        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
3659    }
3660
3661    #[test]
3662    fn resolve_symbol_not_found() {
3663        let provider = TreeSitterProvider::new();
3664        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
3665        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
3666    }
3667
3668    #[test]
3669    fn resolve_symbol_follows_reexport_chains() {
3670        let dir = tempfile::tempdir().unwrap();
3671        let config = dir.path().join("config.ts");
3672        let barrel1 = dir.path().join("barrel1.ts");
3673        let barrel2 = dir.path().join("barrel2.ts");
3674        let barrel3 = dir.path().join("barrel3.ts");
3675        let index = dir.path().join("index.ts");
3676
3677        std::fs::write(
3678            &config,
3679            "export class Config {}\nexport default class DefaultConfig {}\n",
3680        )
3681        .unwrap();
3682        std::fs::write(
3683            &barrel1,
3684            "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
3685        )
3686        .unwrap();
3687        std::fs::write(
3688            &barrel2,
3689            "export { Config as RenamedConfig } from './barrel1';\n",
3690        )
3691        .unwrap();
3692        std::fs::write(
3693            &barrel3,
3694            "export * from './barrel2';\nexport * from './barrel1';\n",
3695        )
3696        .unwrap();
3697        std::fs::write(
3698            &index,
3699            "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
3700        )
3701        .unwrap();
3702
3703        let provider = TreeSitterProvider::new();
3704        let config_canon = std::fs::canonicalize(&config).unwrap();
3705
3706        let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
3707        assert_eq!(direct.len(), 1);
3708        assert_eq!(direct[0].symbol.name, "Config");
3709        assert_eq!(direct[0].file, config_canon.display().to_string());
3710
3711        let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
3712        assert_eq!(renamed.len(), 1);
3713        assert_eq!(renamed[0].symbol.name, "Config");
3714        assert_eq!(renamed[0].file, config_canon.display().to_string());
3715
3716        let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
3717        assert_eq!(wildcard_chain.len(), 1);
3718        assert_eq!(wildcard_chain[0].symbol.name, "Config");
3719        assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
3720
3721        let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
3722        assert_eq!(wildcard_default.len(), 1);
3723        assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
3724        assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
3725
3726        let local = provider.resolve_symbol(&index, "Config").unwrap();
3727        assert_eq!(local.len(), 1);
3728        assert_eq!(local[0].symbol.name, "Config");
3729        assert_eq!(local[0].file, index.display().to_string());
3730    }
3731
3732    // --- Parse tree caching ---
3733
3734    #[test]
3735    fn symbol_range_includes_rust_attributes() {
3736        let dir = tempfile::tempdir().unwrap();
3737        let path = dir.path().join("test_attrs.rs");
3738        std::fs::write(
3739            &path,
3740            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
3741        )
3742        .unwrap();
3743
3744        let provider = TreeSitterProvider::new();
3745        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
3746        assert_eq!(matches.len(), 1);
3747        assert_eq!(
3748            matches[0].symbol.range.start_line, 0,
3749            "symbol range should include preceding /// doc comment, got start_line={}",
3750            matches[0].symbol.range.start_line
3751        );
3752    }
3753
3754    #[test]
3755    fn symbol_range_includes_go_doc_comment() {
3756        let dir = tempfile::tempdir().unwrap();
3757        let path = dir.path().join("test_doc.go");
3758        std::fs::write(
3759            &path,
3760            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
3761        )
3762        .unwrap();
3763
3764        let provider = TreeSitterProvider::new();
3765        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
3766        assert_eq!(matches.len(), 1);
3767        assert_eq!(
3768            matches[0].symbol.range.start_line, 2,
3769            "symbol range should include preceding doc comments, got start_line={}",
3770            matches[0].symbol.range.start_line
3771        );
3772    }
3773
3774    #[test]
3775    fn symbol_range_skips_unrelated_comments() {
3776        let dir = tempfile::tempdir().unwrap();
3777        let path = dir.path().join("test_gap.go");
3778        std::fs::write(
3779            &path,
3780            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
3781        )
3782        .unwrap();
3783
3784        let provider = TreeSitterProvider::new();
3785        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
3786        assert_eq!(matches.len(), 1);
3787        assert_eq!(
3788            matches[0].symbol.range.start_line, 4,
3789            "symbol range should NOT include comment separated by blank line, got start_line={}",
3790            matches[0].symbol.range.start_line
3791        );
3792    }
3793
3794    #[test]
3795    fn parse_cache_returns_same_tree() {
3796        let mut parser = FileParser::new();
3797        let path = fixture_path("sample.ts");
3798
3799        let (tree1, _) = parser.parse(&path).unwrap();
3800        let tree1_root = tree1.root_node().byte_range();
3801
3802        let (tree2, _) = parser.parse(&path).unwrap();
3803        let tree2_root = tree2.root_node().byte_range();
3804
3805        // Same tree (cache hit) should return identical root node range
3806        assert_eq!(tree1_root, tree2_root);
3807    }
3808
3809    #[test]
3810    fn extract_symbols_from_tree_matches_list_symbols() {
3811        let path = fixture_path("sample.rs");
3812        let source = std::fs::read_to_string(&path).unwrap();
3813
3814        let provider = TreeSitterProvider::new();
3815        let listed = provider.list_symbols(&path).unwrap();
3816
3817        let mut parser = FileParser::new();
3818        let (tree, lang) = parser.parse(&path).unwrap();
3819        let extracted = extract_symbols_from_tree(&source, tree, lang).unwrap();
3820
3821        assert_eq!(symbols_as_debug(&extracted), symbols_as_debug(&listed));
3822    }
3823
3824    fn symbols_as_debug(symbols: &[Symbol]) -> Vec<String> {
3825        symbols
3826            .iter()
3827            .map(|symbol| {
3828                format!(
3829                    "{}|{:?}|{}:{}-{}:{}|{:?}|{:?}|{}|{:?}",
3830                    symbol.name,
3831                    symbol.kind,
3832                    symbol.range.start_line,
3833                    symbol.range.start_col,
3834                    symbol.range.end_line,
3835                    symbol.range.end_col,
3836                    symbol.signature,
3837                    symbol.scope_chain,
3838                    symbol.exported,
3839                    symbol.parent,
3840                )
3841            })
3842            .collect()
3843    }
3844
3845    // --- Python extraction ---
3846
3847    #[test]
3848    fn py_extracts_all_symbols() {
3849        let provider = TreeSitterProvider::new();
3850        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3851
3852        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3853        assert!(
3854            names.contains(&"top_level_function"),
3855            "missing top_level_function: {:?}",
3856            names
3857        );
3858        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
3859        assert!(
3860            names.contains(&"instance_method"),
3861            "missing method instance_method: {:?}",
3862            names
3863        );
3864        assert!(
3865            names.contains(&"decorated_function"),
3866            "missing decorated_function: {:?}",
3867            names
3868        );
3869
3870        // Plan requires ≥4 symbols
3871        assert!(
3872            symbols.len() >= 4,
3873            "expected ≥4 symbols, got {}: {:?}",
3874            symbols.len(),
3875            names
3876        );
3877    }
3878
3879    #[test]
3880    fn py_symbol_kinds_correct() {
3881        let provider = TreeSitterProvider::new();
3882        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3883
3884        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3885
3886        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
3887        assert_eq!(find("MyClass").kind, SymbolKind::Class);
3888        assert_eq!(find("instance_method").kind, SymbolKind::Method);
3889        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
3890        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
3891        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
3892        assert_eq!(find("inner_method").kind, SymbolKind::Method);
3893        assert_eq!(find("outer_method").kind, SymbolKind::Method);
3894    }
3895
3896    #[test]
3897    fn py_method_scope_chain() {
3898        let provider = TreeSitterProvider::new();
3899        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3900
3901        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3902
3903        // Method inside MyClass
3904        assert_eq!(
3905            find("instance_method").scope_chain,
3906            vec!["MyClass"],
3907            "instance_method should have MyClass in scope chain"
3908        );
3909        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
3910
3911        // Method inside OuterClass > InnerClass
3912        assert_eq!(
3913            find("inner_method").scope_chain,
3914            vec!["OuterClass", "InnerClass"],
3915            "inner_method should have nested scope chain"
3916        );
3917
3918        // InnerClass itself should have OuterClass in scope
3919        assert_eq!(
3920            find("InnerClass").scope_chain,
3921            vec!["OuterClass"],
3922            "InnerClass should have OuterClass in scope"
3923        );
3924
3925        // Top-level function has empty scope
3926        assert!(
3927            find("top_level_function").scope_chain.is_empty(),
3928            "top-level function should have empty scope chain"
3929        );
3930    }
3931
3932    #[test]
3933    fn py_decorated_function_signature() {
3934        let provider = TreeSitterProvider::new();
3935        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3936
3937        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
3938
3939        let sig = find("decorated_function").signature.as_ref().unwrap();
3940        assert!(
3941            sig.contains("@staticmethod"),
3942            "decorated function signature should include decorator: {}",
3943            sig
3944        );
3945        assert!(
3946            sig.contains("def decorated_function"),
3947            "signature should include function def: {}",
3948            sig
3949        );
3950    }
3951
3952    #[test]
3953    fn py_ranges_valid() {
3954        let provider = TreeSitterProvider::new();
3955        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
3956
3957        for s in &symbols {
3958            assert!(
3959                s.range.end_line >= s.range.start_line,
3960                "symbol {} has invalid range: {:?}",
3961                s.name,
3962                s.range
3963            );
3964        }
3965    }
3966
3967    // --- Rust extraction ---
3968
3969    #[test]
3970    fn rs_extracts_all_symbols() {
3971        let provider = TreeSitterProvider::new();
3972        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
3973
3974        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3975        assert!(
3976            names.contains(&"public_function"),
3977            "missing public_function: {:?}",
3978            names
3979        );
3980        assert!(
3981            names.contains(&"private_function"),
3982            "missing private_function: {:?}",
3983            names
3984        );
3985        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
3986        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
3987        assert!(
3988            names.contains(&"Drawable"),
3989            "missing trait Drawable: {:?}",
3990            names
3991        );
3992        // impl methods
3993        assert!(
3994            names.contains(&"new"),
3995            "missing impl method new: {:?}",
3996            names
3997        );
3998
3999        // Plan requires ≥6 symbols
4000        assert!(
4001            symbols.len() >= 6,
4002            "expected ≥6 symbols, got {}: {:?}",
4003            symbols.len(),
4004            names
4005        );
4006    }
4007
4008    #[test]
4009    fn rs_symbol_kinds_correct() {
4010        let provider = TreeSitterProvider::new();
4011        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
4012
4013        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4014
4015        assert_eq!(find("public_function").kind, SymbolKind::Function);
4016        assert_eq!(find("private_function").kind, SymbolKind::Function);
4017        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
4018        assert_eq!(find("Color").kind, SymbolKind::Enum);
4019        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
4020        assert_eq!(find("new").kind, SymbolKind::Method);
4021    }
4022
4023    #[test]
4024    fn rs_pub_export_detection() {
4025        let provider = TreeSitterProvider::new();
4026        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
4027
4028        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4029
4030        assert!(
4031            find("public_function").exported,
4032            "pub fn should be exported"
4033        );
4034        assert!(
4035            !find("private_function").exported,
4036            "non-pub fn should not be exported"
4037        );
4038        assert!(find("MyStruct").exported, "pub struct should be exported");
4039        assert!(find("Color").exported, "pub enum should be exported");
4040        assert!(find("Drawable").exported, "pub trait should be exported");
4041        assert!(
4042            find("new").exported,
4043            "pub fn inside impl should be exported"
4044        );
4045        assert!(
4046            !find("helper").exported,
4047            "non-pub fn inside impl should not be exported"
4048        );
4049    }
4050
4051    #[test]
4052    fn rs_impl_method_scope_chain() {
4053        let provider = TreeSitterProvider::new();
4054        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
4055
4056        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4057
4058        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
4059        assert_eq!(
4060            find("new").scope_chain,
4061            vec!["MyStruct"],
4062            "impl method should have type in scope chain"
4063        );
4064        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
4065
4066        // Free function has empty scope chain
4067        assert!(
4068            find("public_function").scope_chain.is_empty(),
4069            "free function should have empty scope chain"
4070        );
4071    }
4072
4073    #[test]
4074    fn rs_trait_impl_scope_chain() {
4075        let provider = TreeSitterProvider::new();
4076        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
4077
4078        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
4079        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
4080        assert_eq!(
4081            draw.scope_chain,
4082            vec!["Drawable for MyStruct"],
4083            "trait impl method should have 'Trait for Type' scope"
4084        );
4085        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
4086    }
4087
4088    #[test]
4089    fn rs_ranges_valid() {
4090        let provider = TreeSitterProvider::new();
4091        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
4092
4093        for s in &symbols {
4094            assert!(
4095                s.range.end_line >= s.range.start_line,
4096                "symbol {} has invalid range: {:?}",
4097                s.name,
4098                s.range
4099            );
4100        }
4101    }
4102
4103    // --- Go extraction ---
4104
4105    #[test]
4106    fn go_extracts_all_symbols() {
4107        let provider = TreeSitterProvider::new();
4108        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4109
4110        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
4111        assert!(
4112            names.contains(&"ExportedFunction"),
4113            "missing ExportedFunction: {:?}",
4114            names
4115        );
4116        assert!(
4117            names.contains(&"unexportedFunction"),
4118            "missing unexportedFunction: {:?}",
4119            names
4120        );
4121        assert!(
4122            names.contains(&"MyStruct"),
4123            "missing struct MyStruct: {:?}",
4124            names
4125        );
4126        assert!(
4127            names.contains(&"Reader"),
4128            "missing interface Reader: {:?}",
4129            names
4130        );
4131        // receiver method
4132        assert!(
4133            names.contains(&"String"),
4134            "missing receiver method String: {:?}",
4135            names
4136        );
4137
4138        // Plan requires ≥4 symbols
4139        assert!(
4140            symbols.len() >= 4,
4141            "expected ≥4 symbols, got {}: {:?}",
4142            symbols.len(),
4143            names
4144        );
4145    }
4146
4147    #[test]
4148    fn go_symbol_kinds_correct() {
4149        let provider = TreeSitterProvider::new();
4150        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4151
4152        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4153
4154        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
4155        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
4156        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
4157        assert_eq!(find("Reader").kind, SymbolKind::Interface);
4158        assert_eq!(find("String").kind, SymbolKind::Method);
4159        assert_eq!(find("helper").kind, SymbolKind::Method);
4160    }
4161
4162    #[test]
4163    fn go_uppercase_export_detection() {
4164        let provider = TreeSitterProvider::new();
4165        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4166
4167        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4168
4169        assert!(
4170            find("ExportedFunction").exported,
4171            "ExportedFunction (uppercase) should be exported"
4172        );
4173        assert!(
4174            !find("unexportedFunction").exported,
4175            "unexportedFunction (lowercase) should not be exported"
4176        );
4177        assert!(
4178            find("MyStruct").exported,
4179            "MyStruct (uppercase) should be exported"
4180        );
4181        assert!(
4182            find("Reader").exported,
4183            "Reader (uppercase) should be exported"
4184        );
4185        assert!(
4186            find("String").exported,
4187            "String method (uppercase) should be exported"
4188        );
4189        assert!(
4190            !find("helper").exported,
4191            "helper method (lowercase) should not be exported"
4192        );
4193    }
4194
4195    #[test]
4196    fn go_receiver_method_scope_chain() {
4197        let provider = TreeSitterProvider::new();
4198        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4199
4200        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
4201
4202        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
4203        assert_eq!(
4204            find("String").scope_chain,
4205            vec!["MyStruct"],
4206            "receiver method should have type in scope chain"
4207        );
4208        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
4209
4210        // Regular function has empty scope chain
4211        assert!(
4212            find("ExportedFunction").scope_chain.is_empty(),
4213            "regular function should have empty scope chain"
4214        );
4215    }
4216
4217    #[test]
4218    fn go_ranges_valid() {
4219        let provider = TreeSitterProvider::new();
4220        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
4221
4222        for s in &symbols {
4223            assert!(
4224                s.range.end_line >= s.range.start_line,
4225                "symbol {} has invalid range: {:?}",
4226                s.name,
4227                s.range
4228            );
4229        }
4230    }
4231
4232    // --- Cross-language ---
4233
4234    #[test]
4235    fn cross_language_all_six_produce_symbols() {
4236        let provider = TreeSitterProvider::new();
4237
4238        let fixtures = [
4239            ("sample.ts", "TypeScript"),
4240            ("sample.tsx", "TSX"),
4241            ("sample.js", "JavaScript"),
4242            ("sample.py", "Python"),
4243            ("sample.rs", "Rust"),
4244            ("sample.go", "Go"),
4245        ];
4246
4247        for (fixture, lang) in &fixtures {
4248            let symbols = provider
4249                .list_symbols(&fixture_path(fixture))
4250                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
4251            assert!(
4252                symbols.len() >= 2,
4253                "{} should produce ≥2 symbols, got {}: {:?}",
4254                lang,
4255                symbols.len(),
4256                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
4257            );
4258        }
4259    }
4260
4261    // --- Symbol cache tests ---
4262
4263    #[test]
4264    fn symbol_cache_returns_cached_results_on_second_call() {
4265        let dir = tempfile::tempdir().unwrap();
4266        let file = dir.path().join("test.rs");
4267        std::fs::write(&file, "pub fn hello() {}\npub fn world() {}").unwrap();
4268
4269        let mut parser = FileParser::new();
4270
4271        let symbols1 = parser.extract_symbols(&file).unwrap();
4272        assert_eq!(symbols1.len(), 2);
4273
4274        // Second call should return cached result
4275        let symbols2 = parser.extract_symbols(&file).unwrap();
4276        assert_eq!(symbols2.len(), 2);
4277        assert_eq!(symbols1[0].name, symbols2[0].name);
4278
4279        // Verify cache is populated
4280        assert!(parser.symbol_cache.contains_key(&file));
4281    }
4282
4283    #[test]
4284    fn symbol_cache_invalidates_on_file_change() {
4285        let dir = tempfile::tempdir().unwrap();
4286        let file = dir.path().join("test.rs");
4287        std::fs::write(&file, "pub fn hello() {}").unwrap();
4288
4289        let mut parser = FileParser::new();
4290
4291        let symbols1 = parser.extract_symbols(&file).unwrap();
4292        assert_eq!(symbols1.len(), 1);
4293        assert_eq!(symbols1[0].name, "hello");
4294
4295        // Wait to ensure mtime changes (filesystem resolution can be 1s on some OS)
4296        std::thread::sleep(std::time::Duration::from_millis(50));
4297
4298        // Modify file — add a second function
4299        std::fs::write(&file, "pub fn hello() {}\npub fn goodbye() {}").unwrap();
4300
4301        // Should detect mtime change and re-extract
4302        let symbols2 = parser.extract_symbols(&file).unwrap();
4303        assert_eq!(symbols2.len(), 2);
4304        assert!(symbols2.iter().any(|s| s.name == "goodbye"));
4305    }
4306
4307    #[test]
4308    fn symbol_cache_invalidate_method_clears_entry() {
4309        let dir = tempfile::tempdir().unwrap();
4310        let file = dir.path().join("test.rs");
4311        std::fs::write(&file, "pub fn hello() {}").unwrap();
4312
4313        let mut parser = FileParser::new();
4314        parser.extract_symbols(&file).unwrap();
4315        assert!(parser.symbol_cache.contains_key(&file));
4316
4317        parser.invalidate_symbols(&file);
4318        assert!(!parser.symbol_cache.contains_key(&file));
4319        // Parse tree cache should also be cleared
4320        assert!(!parser.cache.contains_key(&file));
4321    }
4322
4323    #[test]
4324    fn symbol_cache_works_for_multiple_languages() {
4325        let dir = tempfile::tempdir().unwrap();
4326        let rs_file = dir.path().join("lib.rs");
4327        let ts_file = dir.path().join("app.ts");
4328        let py_file = dir.path().join("main.py");
4329
4330        std::fs::write(&rs_file, "pub fn rust_fn() {}").unwrap();
4331        std::fs::write(&ts_file, "export function tsFn() {}").unwrap();
4332        std::fs::write(&py_file, "def py_fn():\n    pass").unwrap();
4333
4334        let mut parser = FileParser::new();
4335
4336        let rs_syms = parser.extract_symbols(&rs_file).unwrap();
4337        let ts_syms = parser.extract_symbols(&ts_file).unwrap();
4338        let py_syms = parser.extract_symbols(&py_file).unwrap();
4339
4340        assert!(rs_syms.iter().any(|s| s.name == "rust_fn"));
4341        assert!(ts_syms.iter().any(|s| s.name == "tsFn"));
4342        assert!(py_syms.iter().any(|s| s.name == "py_fn"));
4343
4344        // All should be cached now
4345        assert_eq!(parser.symbol_cache.len(), 3);
4346
4347        // Re-extract should return same results from cache
4348        let rs_syms2 = parser.extract_symbols(&rs_file).unwrap();
4349        assert_eq!(rs_syms.len(), rs_syms2.len());
4350    }
4351}