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