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