Skip to main content

aft/
parser.rs

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