Skip to main content

aft/
parser.rs

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