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;; function-like values assigned to const/let/var
26(lexical_declaration
27  (variable_declarator
28    name: (identifier) @arrow.name
29    value: (arrow_function) @arrow.body) @arrow.decl) @arrow.def
30(lexical_declaration
31  (variable_declarator
32    name: (identifier) @arrow.name
33    value: (function_expression) @arrow.body) @arrow.decl) @arrow.def
34(lexical_declaration
35  (variable_declarator
36    name: (identifier) @arrow.name
37    value: (generator_function) @arrow.body) @arrow.decl) @arrow.def
38
39;; anonymous default exports
40(export_statement
41  value: (function_expression) @default.body) @default.def
42(export_statement
43  value: (generator_function) @default.body) @default.def
44(export_statement
45  value: (class) @default.body) @default.def
46
47;; class declarations
48(class_declaration
49  name: (type_identifier) @class.name) @class.def
50
51;; method definitions inside classes
52(class_declaration
53  name: (type_identifier) @method.class_name
54  body: (class_body
55    (method_definition
56      name: (property_identifier) @method.name) @method.def))
57
58;; interface declarations
59(interface_declaration
60  name: (type_identifier) @interface.name) @interface.def
61
62;; enum declarations
63(enum_declaration
64  name: (identifier) @enum.name) @enum.def
65
66;; type alias declarations
67(type_alias_declaration
68  name: (type_identifier) @type_alias.name) @type_alias.def
69
70;; top-level const/let variable declarations
71(lexical_declaration
72  (variable_declarator
73    name: (identifier) @var.name) @var.decl) @var.def
74
75;; export statement wrappers (top-level only)
76(export_statement) @export.stmt
77"#;
78
79const JS_QUERY: &str = r#"
80;; function declarations
81(function_declaration
82  name: (identifier) @fn.name) @fn.def
83
84;; function-like values assigned to const/let/var
85(lexical_declaration
86  (variable_declarator
87    name: (identifier) @arrow.name
88    value: (arrow_function) @arrow.body) @arrow.decl) @arrow.def
89(lexical_declaration
90  (variable_declarator
91    name: (identifier) @arrow.name
92    value: (function_expression) @arrow.body) @arrow.decl) @arrow.def
93(lexical_declaration
94  (variable_declarator
95    name: (identifier) @arrow.name
96    value: (generator_function) @arrow.body) @arrow.decl) @arrow.def
97
98;; anonymous default exports
99(export_statement
100  value: (function_expression) @default.body) @default.def
101(export_statement
102  value: (generator_function) @default.body) @default.def
103(export_statement
104  value: (class) @default.body) @default.def
105
106;; class declarations
107(class_declaration
108  name: (identifier) @class.name) @class.def
109
110;; method definitions inside classes
111(class_declaration
112  name: (identifier) @method.class_name
113  body: (class_body
114    (method_definition
115      name: (property_identifier) @method.name) @method.def))
116
117;; top-level const/let variable declarations
118(lexical_declaration
119  (variable_declarator
120    name: (identifier) @var.name) @var.decl) @var.def
121
122;; export statement wrappers (top-level only)
123(export_statement) @export.stmt
124"#;
125
126const PY_QUERY: &str = r#"
127;; function definitions (top-level and nested)
128(function_definition
129  name: (identifier) @fn.name) @fn.def
130
131;; class definitions
132(class_definition
133  name: (identifier) @class.name) @class.def
134
135;; decorated definitions (wraps function_definition or class_definition)
136(decorated_definition
137  (decorator) @dec.decorator) @dec.def
138"#;
139
140const RS_QUERY: &str = r#"
141;; free functions (with optional visibility)
142(function_item
143  name: (identifier) @fn.name) @fn.def
144
145;; struct items
146(struct_item
147  name: (type_identifier) @struct.name) @struct.def
148
149;; enum items
150(enum_item
151  name: (type_identifier) @enum.name) @enum.def
152
153;; trait items
154(trait_item
155  name: (type_identifier) @trait.name) @trait.def
156
157;; impl blocks — capture the whole block to find methods
158(impl_item) @impl.def
159
160;; visibility modifiers on any item
161(visibility_modifier) @vis.mod
162"#;
163
164const GO_QUERY: &str = r#"
165;; function declarations
166(function_declaration
167  name: (identifier) @fn.name) @fn.def
168
169;; method declarations (with receiver)
170(method_declaration
171  name: (field_identifier) @method.name) @method.def
172
173;; type declarations (struct and interface)
174(type_declaration
175  (type_spec
176    name: (type_identifier) @type.name
177    type: (_) @type.body)) @type.def
178"#;
179
180const C_QUERY: &str = r#"
181;; function definitions
182(function_definition
183  declarator: (function_declarator
184    declarator: (identifier) @fn.name)) @fn.def
185
186;; function declarations / prototypes
187(declaration
188  declarator: (function_declarator
189    declarator: (identifier) @fn.name)) @fn.def
190
191;; struct declarations
192(struct_specifier
193  name: (type_identifier) @struct.name
194  body: (field_declaration_list)) @struct.def
195
196;; enum declarations
197(enum_specifier
198  name: (type_identifier) @enum.name
199  body: (enumerator_list)) @enum.def
200
201;; typedef aliases
202(type_definition
203  declarator: (type_identifier) @type.name) @type.def
204
205;; macros
206(preproc_def
207  name: (identifier) @macro.name) @macro.def
208
209(preproc_function_def
210  name: (identifier) @macro.name) @macro.def
211"#;
212
213const CPP_QUERY: &str = r#"
214;; free function definitions
215(function_definition
216  declarator: (function_declarator
217    declarator: (identifier) @fn.name)) @fn.def
218
219;; free function declarations
220(declaration
221  declarator: (function_declarator
222    declarator: (identifier) @fn.name)) @fn.def
223
224;; inline method definitions / declarations inside class bodies
225(function_definition
226  declarator: (function_declarator
227    declarator: (field_identifier) @method.name)) @method.def
228
229(field_declaration
230  declarator: (function_declarator
231    declarator: (field_identifier) @method.name)) @method.def
232
233;; qualified functions / methods
234(function_definition
235  declarator: (function_declarator
236    declarator: (qualified_identifier
237      scope: (_) @qual.scope
238      name: (identifier) @qual.name))) @qual.def
239
240(declaration
241  declarator: (function_declarator
242    declarator: (qualified_identifier
243      scope: (_) @qual.scope
244      name: (identifier) @qual.name))) @qual.def
245
246;; class / struct / enum / namespace declarations
247(class_specifier
248  name: (_) @class.name) @class.def
249
250(struct_specifier
251  name: (_) @struct.name) @struct.def
252
253(enum_specifier
254  name: (_) @enum.name) @enum.def
255
256(namespace_definition
257  name: (_) @namespace.name) @namespace.def
258
259;; template declarations
260(template_declaration
261  (class_specifier
262    name: (_) @template.class.name) @template.class.item) @template.class.def
263
264(template_declaration
265  (struct_specifier
266    name: (_) @template.struct.name) @template.struct.item) @template.struct.def
267
268(template_declaration
269  (function_definition
270    declarator: (function_declarator
271      declarator: (identifier) @template.fn.name)) @template.fn.item) @template.fn.def
272
273(template_declaration
274  (function_definition
275    declarator: (function_declarator
276      declarator: (qualified_identifier
277        scope: (_) @template.qual.scope
278        name: (identifier) @template.qual.name))) @template.qual.item) @template.qual.def
279"#;
280
281const ZIG_QUERY: &str = r#"
282;; functions
283(function_declaration
284  name: (identifier) @fn.name) @fn.def
285
286;; container declarations bound to const names
287(variable_declaration
288  (identifier) @struct.name
289  "="
290  (struct_declaration) @struct.body) @struct.def
291
292(variable_declaration
293  (identifier) @enum.name
294  "="
295  (enum_declaration) @enum.body) @enum.def
296
297(variable_declaration
298  (identifier) @union.name
299  "="
300  (union_declaration) @union.body) @union.def
301
302;; const declarations
303(variable_declaration
304  (identifier) @const.name) @const.def
305
306;; tests
307(test_declaration
308  (string) @test.name) @test.def
309
310(test_declaration
311  (identifier) @test.name) @test.def
312"#;
313
314const CSHARP_QUERY: &str = r#"
315;; types
316(class_declaration
317  name: (identifier) @class.name) @class.def
318
319(interface_declaration
320  name: (identifier) @interface.name) @interface.def
321
322(struct_declaration
323  name: (identifier) @struct.name) @struct.def
324
325(enum_declaration
326  name: (identifier) @enum.name) @enum.def
327
328;; members
329(method_declaration
330  name: (identifier) @method.name) @method.def
331
332(property_declaration
333  name: (identifier) @property.name) @property.def
334
335;; namespaces
336(namespace_declaration
337  name: (_) @namespace.name) @namespace.def
338
339(file_scoped_namespace_declaration
340  name: (_) @namespace.name) @namespace.def
341"#;
342
343// --- Bash query ---
344
345const BASH_QUERY: &str = r#"
346;; function definitions (both `function foo()` and `foo()` styles)
347(function_definition
348  name: (word) @fn.name) @fn.def
349"#;
350
351// --- Solidity query ---
352
353const SOL_QUERY: &str = r#"
354;; contracts / libraries / interfaces
355(contract_declaration
356  name: (identifier) @contract.name) @contract.def
357
358(library_declaration
359  name: (identifier) @library.name) @library.def
360
361(interface_declaration
362  name: (identifier) @interface.name) @interface.def
363
364;; functions, modifiers, constructors
365(function_definition
366  name: (identifier) @fn.name) @fn.def
367
368(modifier_definition
369  name: (identifier) @modifier.name) @modifier.def
370
371(constructor_definition) @constructor.def
372
373(fallback_receive_definition) @fallback_receive.def
374
375;; events / errors
376(event_definition
377  name: (identifier) @event.name) @event.def
378
379(error_declaration
380  name: (identifier) @error.name) @error.def
381
382;; data types
383(struct_declaration
384  name: (identifier) @struct.name) @struct.def
385
386(enum_declaration
387  name: (identifier) @enum.name) @enum.def
388
389;; state variables (top-level inside a contract)
390(state_variable_declaration
391  name: (identifier) @var.name) @var.def
392"#;
393
394const SCSS_QUERY: &str = r#"
395;; SCSS definitions
396(mixin_statement
397  name: (identifier) @mixin.name) @mixin.def
398
399(function_statement
400  name: (identifier) @fn.name) @fn.def
401
402(declaration
403  (property_name) @var.name) @var.def
404
405(rule_set
406  (selectors) @selector.name) @selector.def
407"#;
408
409const SCALA_QUERY: &str = r#"
410;; classes / objects / traits
411(class_definition
412  name: (identifier) @class.name) @class.def
413(object_definition
414  name: (identifier) @object.name) @object.def
415(enum_definition
416  name: (_) @enum.name) @enum.def
417(trait_definition
418  name: (identifier) @trait.name) @trait.def
419;; methods (def)
420(function_definition
421  name: (identifier) @fn.name) @fn.def
422(function_declaration
423  name: (identifier) @fn.name) @fn.def
424;; vals / vars / type aliases
425(val_definition
426  pattern: (identifier) @val.name) @val.def
427(var_definition
428  pattern: (identifier) @var.name) @var.def
429(given_definition
430  name: (_) @given.name) @given.def
431(type_definition
432  name: (type_identifier) @type.name) @type.def
433"#;
434
435const JAVA_QUERY: &str = r#"
436;; types
437(class_declaration
438  name: (identifier) @class.name) @class.def
439(interface_declaration
440  name: (identifier) @interface.name) @interface.def
441(annotation_type_declaration
442  name: (identifier) @interface.name) @interface.def
443(enum_declaration
444  name: (identifier) @enum.name) @enum.def
445(record_declaration
446  name: (identifier) @struct.name) @struct.def
447
448;; members
449(method_declaration
450  name: (identifier) @fn.name) @fn.def
451(constructor_declaration
452  name: (identifier) @fn.name) @fn.def
453(field_declaration
454  declarator: (variable_declarator
455    name: (identifier) @var.name)) @var.def
456"#;
457
458const RUBY_QUERY: &str = r#"
459;; modules / classes
460(module
461  name: (constant) @module.name) @module.def
462(class
463  name: (constant) @class.name) @class.def
464
465;; methods
466(method
467  name: (_) @fn.name) @fn.def
468(singleton_method
469  name: (_) @fn.name) @fn.def
470
471;; constants
472(assignment
473  left: (constant) @var.name) @var.def
474"#;
475
476const KOTLIN_QUERY: &str = r#"
477;; declarations
478(class_declaration
479  (type_identifier) @class.name) @class.def
480(object_declaration
481  (type_identifier) @object.name) @object.def
482(function_declaration
483  (simple_identifier) @fn.name) @fn.def
484(property_declaration
485  (variable_declaration
486    (simple_identifier) @var.name)) @var.def
487(type_alias
488  (type_identifier) @type.name) @type.def
489"#;
490
491const SWIFT_QUERY: &str = r#"
492;; types
493(class_declaration
494  name: (type_identifier) @class.name) @class.def
495(protocol_declaration
496  name: (type_identifier) @interface.name) @interface.def
497
498;; functions and members
499(function_declaration
500  name: (simple_identifier) @fn.name) @fn.def
501(protocol_function_declaration
502  name: (simple_identifier) @fn.name) @fn.def
503(property_declaration
504  name: (pattern
505    bound_identifier: (simple_identifier) @var.name)) @var.def
506(typealias_declaration
507  name: (type_identifier) @type.name) @type.def
508"#;
509
510const PHP_QUERY: &str = r#"
511;; namespaces and types
512(namespace_definition
513  name: (namespace_name) @namespace.name) @namespace.def
514(class_declaration
515  name: (name) @class.name) @class.def
516(interface_declaration
517  name: (name) @interface.name) @interface.def
518(trait_declaration
519  name: (name) @trait.name) @trait.def
520(enum_declaration
521  name: (name) @enum.name) @enum.def
522
523;; functions and members
524(function_definition
525  name: (name) @fn.name) @fn.def
526(method_declaration
527  name: (name) @fn.name) @fn.def
528(property_declaration
529  (property_element
530    name: (variable_name (name) @var.name))) @var.def
531"#;
532
533const LUA_QUERY: &str = r#"
534;; functions
535(function_declaration
536  name: (identifier) @fn.name) @fn.def
537(function_declaration
538  name: (dot_index_expression
539    field: (identifier) @fn.name)) @fn.def
540(function_declaration
541  name: (method_index_expression
542    method: (identifier) @fn.name)) @fn.def
543
544;; locals / module tables
545(variable_declaration
546  (assignment_statement
547    (variable_list
548      name: (identifier) @var.name))) @var.def
549(variable_declaration
550  (assignment_statement
551    (variable_list
552      name: (variable) @var.name))) @var.def
553(variable_declaration
554  (variable_list
555    name: (identifier) @var.name)) @var.def
556(variable_declaration
557  (variable_list
558    name: (variable) @var.name)) @var.def
559"#;
560
561const PERL_QUERY: &str = r#"
562;; packages and subroutines
563(package_statement
564  (package_name) @package.name) @package.def
565(function_definition
566  name: (identifier) @fn.name) @fn.def
567(function_definition_without_sub
568  name: (identifier) @fn.name) @fn.def
569
570;; constants / lexical variables
571(use_constant_statement
572  constant: (identifier) @var.name) @var.def
573(variable_declaration
574  variable_name: (_) @var.name) @var.def
575"#;
576
577/// Supported language identifier.
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
579pub enum LangId {
580    TypeScript,
581    Tsx,
582    JavaScript,
583    Python,
584    Rust,
585    Go,
586    C,
587    Cpp,
588    Zig,
589    CSharp,
590    Bash,
591    Html,
592    Markdown,
593    Solidity,
594    Scss,
595    Vue,
596    Json,
597    Scala,
598    Java,
599    Ruby,
600    Kotlin,
601    Swift,
602    Php,
603    Lua,
604    Perl,
605    Yaml,
606}
607
608/// Maps file extension to language identifier.
609pub fn detect_language(path: &Path) -> Option<LangId> {
610    let ext = path.extension()?.to_str()?;
611    match ext {
612        "ts" | "mts" | "cts" => Some(LangId::TypeScript),
613        "tsx" => Some(LangId::Tsx),
614        "js" | "jsx" | "mjs" | "cjs" => Some(LangId::JavaScript),
615        "py" | "pyi" => Some(LangId::Python),
616        "rs" => Some(LangId::Rust),
617        "go" => Some(LangId::Go),
618        "c" | "h" => Some(LangId::C),
619        "cc" | "cpp" | "cxx" | "hpp" | "hh" => Some(LangId::Cpp),
620        "zig" => Some(LangId::Zig),
621        "cs" => Some(LangId::CSharp),
622        "sh" | "bash" | "zsh" => Some(LangId::Bash),
623        "html" | "htm" => Some(LangId::Html),
624        "md" | "markdown" | "mdx" => Some(LangId::Markdown),
625        "sol" => Some(LangId::Solidity),
626        "scss" => Some(LangId::Scss),
627        "vue" => Some(LangId::Vue),
628        "json" | "jsonc" => Some(LangId::Json),
629        "scala" | "sc" => Some(LangId::Scala),
630        "java" => Some(LangId::Java),
631        "rb" => Some(LangId::Ruby),
632        "kt" | "kts" => Some(LangId::Kotlin),
633        "swift" => Some(LangId::Swift),
634        "inc" | "php" => Some(LangId::Php),
635        "lua" => Some(LangId::Lua),
636        "pl" | "pm" | "t" => Some(LangId::Perl),
637        "yaml" | "yml" => Some(LangId::Yaml),
638        _ => None,
639    }
640}
641
642/// Returns the tree-sitter Language grammar for a given LangId.
643pub fn grammar_for(lang: LangId) -> Language {
644    match lang {
645        LangId::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
646        LangId::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
647        LangId::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
648        LangId::Python => tree_sitter_python::LANGUAGE.into(),
649        LangId::Rust => tree_sitter_rust::LANGUAGE.into(),
650        LangId::Go => tree_sitter_go::LANGUAGE.into(),
651        LangId::C => tree_sitter_c::LANGUAGE.into(),
652        LangId::Cpp => tree_sitter_cpp::LANGUAGE.into(),
653        LangId::Zig => tree_sitter_zig::LANGUAGE.into(),
654        LangId::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
655        LangId::Bash => tree_sitter_bash::LANGUAGE.into(),
656        LangId::Html => tree_sitter_html::LANGUAGE.into(),
657        LangId::Markdown => tree_sitter_md::LANGUAGE.into(),
658        LangId::Solidity => tree_sitter_solidity::LANGUAGE.into(),
659        LangId::Scss => tree_sitter_scss::language(),
660        LangId::Vue => tree_sitter_vue::LANGUAGE.into(),
661        LangId::Json => tree_sitter_json::LANGUAGE.into(),
662        LangId::Scala => tree_sitter_scala::LANGUAGE.into(),
663        LangId::Java => tree_sitter_java::LANGUAGE.into(),
664        LangId::Ruby => tree_sitter_ruby::LANGUAGE.into(),
665        LangId::Kotlin => tree_sitter_kotlin_sg::LANGUAGE.into(),
666        LangId::Swift => tree_sitter_swift::LANGUAGE.into(),
667        LangId::Php => tree_sitter_php::LANGUAGE_PHP.into(),
668        LangId::Lua => tree_sitter_lua::LANGUAGE.into(),
669        LangId::Perl => tree_sitter_perl::LANGUAGE.into(),
670        LangId::Yaml => tree_sitter_yaml::LANGUAGE.into(),
671    }
672}
673
674/// Returns the query pattern string for a given LangId, if implemented.
675fn query_for(lang: LangId) -> Option<&'static str> {
676    match lang {
677        LangId::TypeScript | LangId::Tsx => Some(TS_QUERY),
678        LangId::JavaScript => Some(JS_QUERY),
679        LangId::Python => Some(PY_QUERY),
680        LangId::Rust => Some(RS_QUERY),
681        LangId::Go => Some(GO_QUERY),
682        LangId::C => Some(C_QUERY),
683        LangId::Cpp => Some(CPP_QUERY),
684        LangId::Zig => Some(ZIG_QUERY),
685        LangId::CSharp => Some(CSHARP_QUERY),
686        LangId::Bash => Some(BASH_QUERY),
687        LangId::Html => None, // HTML uses direct tree walking like Markdown
688        LangId::Markdown => None,
689        LangId::Solidity => Some(SOL_QUERY),
690        LangId::Scss => Some(SCSS_QUERY),
691        LangId::Vue => None,
692        LangId::Json => None,
693        LangId::Scala => Some(SCALA_QUERY),
694        LangId::Java => Some(JAVA_QUERY),
695        LangId::Ruby => Some(RUBY_QUERY),
696        LangId::Kotlin => Some(KOTLIN_QUERY),
697        LangId::Swift => Some(SWIFT_QUERY),
698        LangId::Php => Some(PHP_QUERY),
699        LangId::Lua => Some(LUA_QUERY),
700        LangId::Perl => Some(PERL_QUERY),
701        LangId::Yaml => None, // YAML uses direct tree walking like JSON
702    }
703}
704
705static TS_QUERY_CACHE: LazyLock<Result<Query, String>> =
706    LazyLock::new(|| compile_query(LangId::TypeScript));
707static TSX_QUERY_CACHE: LazyLock<Result<Query, String>> =
708    LazyLock::new(|| compile_query(LangId::Tsx));
709static JS_QUERY_CACHE: LazyLock<Result<Query, String>> =
710    LazyLock::new(|| compile_query(LangId::JavaScript));
711static PY_QUERY_CACHE: LazyLock<Result<Query, String>> =
712    LazyLock::new(|| compile_query(LangId::Python));
713static RS_QUERY_CACHE: LazyLock<Result<Query, String>> =
714    LazyLock::new(|| compile_query(LangId::Rust));
715static GO_QUERY_CACHE: LazyLock<Result<Query, String>> =
716    LazyLock::new(|| compile_query(LangId::Go));
717static C_QUERY_CACHE: LazyLock<Result<Query, String>> = LazyLock::new(|| compile_query(LangId::C));
718static CPP_QUERY_CACHE: LazyLock<Result<Query, String>> =
719    LazyLock::new(|| compile_query(LangId::Cpp));
720static ZIG_QUERY_CACHE: LazyLock<Result<Query, String>> =
721    LazyLock::new(|| compile_query(LangId::Zig));
722static CSHARP_QUERY_CACHE: LazyLock<Result<Query, String>> =
723    LazyLock::new(|| compile_query(LangId::CSharp));
724static BASH_QUERY_CACHE: LazyLock<Result<Query, String>> =
725    LazyLock::new(|| compile_query(LangId::Bash));
726static SOL_QUERY_CACHE: LazyLock<Result<Query, String>> =
727    LazyLock::new(|| compile_query(LangId::Solidity));
728static SCSS_QUERY_CACHE: LazyLock<Result<Query, String>> =
729    LazyLock::new(|| compile_query(LangId::Scss));
730static SCALA_QUERY_CACHE: LazyLock<Result<Query, String>> =
731    LazyLock::new(|| compile_query(LangId::Scala));
732static JAVA_QUERY_CACHE: LazyLock<Result<Query, String>> =
733    LazyLock::new(|| compile_query(LangId::Java));
734static RUBY_QUERY_CACHE: LazyLock<Result<Query, String>> =
735    LazyLock::new(|| compile_query(LangId::Ruby));
736static KOTLIN_QUERY_CACHE: LazyLock<Result<Query, String>> =
737    LazyLock::new(|| compile_query(LangId::Kotlin));
738static SWIFT_QUERY_CACHE: LazyLock<Result<Query, String>> =
739    LazyLock::new(|| compile_query(LangId::Swift));
740static PHP_QUERY_CACHE: LazyLock<Result<Query, String>> =
741    LazyLock::new(|| compile_query(LangId::Php));
742static LUA_QUERY_CACHE: LazyLock<Result<Query, String>> =
743    LazyLock::new(|| compile_query(LangId::Lua));
744static PERL_QUERY_CACHE: LazyLock<Result<Query, String>> =
745    LazyLock::new(|| compile_query(LangId::Perl));
746
747fn compile_query(lang: LangId) -> Result<Query, String> {
748    let query_src = query_for(lang).ok_or_else(|| format!("missing query for {lang:?}"))?;
749    let grammar = grammar_for(lang);
750    Query::new(&grammar, query_src)
751        .map_err(|error| format!("query compile error for {lang:?}: {error}"))
752}
753
754fn cached_query_for(lang: LangId) -> Result<Option<&'static Query>, AftError> {
755    let query = match lang {
756        LangId::TypeScript => Some(&*TS_QUERY_CACHE),
757        LangId::Tsx => Some(&*TSX_QUERY_CACHE),
758        LangId::JavaScript => Some(&*JS_QUERY_CACHE),
759        LangId::Python => Some(&*PY_QUERY_CACHE),
760        LangId::Rust => Some(&*RS_QUERY_CACHE),
761        LangId::Go => Some(&*GO_QUERY_CACHE),
762        LangId::C => Some(&*C_QUERY_CACHE),
763        LangId::Cpp => Some(&*CPP_QUERY_CACHE),
764        LangId::Zig => Some(&*ZIG_QUERY_CACHE),
765        LangId::CSharp => Some(&*CSHARP_QUERY_CACHE),
766        LangId::Bash => Some(&*BASH_QUERY_CACHE),
767        LangId::Solidity => Some(&*SOL_QUERY_CACHE),
768        LangId::Scss => Some(&*SCSS_QUERY_CACHE),
769        LangId::Scala => Some(&*SCALA_QUERY_CACHE),
770        LangId::Java => Some(&*JAVA_QUERY_CACHE),
771        LangId::Ruby => Some(&*RUBY_QUERY_CACHE),
772        LangId::Kotlin => Some(&*KOTLIN_QUERY_CACHE),
773        LangId::Swift => Some(&*SWIFT_QUERY_CACHE),
774        LangId::Php => Some(&*PHP_QUERY_CACHE),
775        LangId::Lua => Some(&*LUA_QUERY_CACHE),
776        LangId::Perl => Some(&*PERL_QUERY_CACHE),
777        LangId::Html | LangId::Markdown | LangId::Vue | LangId::Json | LangId::Yaml => None,
778    };
779
780    query
781        .map(|result| {
782            result.as_ref().map_err(|message| AftError::ParseError {
783                message: message.clone(),
784            })
785        })
786        .transpose()
787}
788
789/// Cached parse result: mtime at parse time + the tree.
790struct CachedTree {
791    mtime: SystemTime,
792    size: u64,
793    content_hash: blake3::Hash,
794    tree: Tree,
795}
796
797/// Cached symbol extraction result: mtime at extraction time + symbols.
798#[derive(Clone)]
799struct CachedSymbols {
800    mtime: SystemTime,
801    size: u64,
802    content_hash: blake3::Hash,
803    symbols: Vec<Symbol>,
804}
805
806fn content_hash_for_source(source: &str) -> blake3::Hash {
807    if source.len() as u64 > cache_freshness::CONTENT_HASH_SIZE_CAP {
808        cache_freshness::zero_hash()
809    } else {
810        cache_freshness::hash_bytes(source.as_bytes())
811    }
812}
813
814fn cached_file_is_fresh(
815    path: &Path,
816    cached_mtime: SystemTime,
817    cached_size: u64,
818    cached_content_hash: blake3::Hash,
819    fallback_mtime: SystemTime,
820) -> bool {
821    let Ok(metadata) = std::fs::metadata(path) else {
822        return false;
823    };
824    let current_size = metadata.len();
825    if current_size != cached_size {
826        return false;
827    }
828
829    let current_mtime = metadata.modified().unwrap_or(fallback_mtime);
830    if current_size > cache_freshness::CONTENT_HASH_SIZE_CAP {
831        return current_mtime == cached_mtime;
832    }
833
834    matches!(
835        cache_freshness::hash_file_if_small(path, current_size),
836        Ok(Some(hash)) if hash == cached_content_hash
837    )
838}
839
840/// Shared symbol cache that can be pre-warmed in a background thread
841/// and read by all parser instances.
842#[derive(Clone, Default)]
843pub struct SymbolCache {
844    entries: HashMap<PathBuf, CachedSymbols>,
845    generation: u64,
846    project_root: Option<PathBuf>,
847}
848
849pub type SharedSymbolCache = Arc<RwLock<SymbolCache>>;
850
851impl SymbolCache {
852    pub fn new() -> Self {
853        Self {
854            entries: HashMap::new(),
855            generation: 0,
856            project_root: None,
857        }
858    }
859
860    /// Set the project root used for disk persistence.
861    pub fn set_project_root(&mut self, project_root: PathBuf) {
862        debug_assert!(project_root.is_absolute());
863        self.project_root = Some(project_root);
864    }
865
866    /// Set the project root only when the caller still belongs to the active
867    /// cache generation.
868    pub fn set_project_root_for_generation(
869        &mut self,
870        generation: u64,
871        project_root: PathBuf,
872    ) -> bool {
873        if self.generation != generation {
874            return false;
875        }
876        self.set_project_root(project_root);
877        true
878    }
879
880    /// Insert pre-warmed symbols for a file.
881    pub fn insert(
882        &mut self,
883        path: PathBuf,
884        mtime: SystemTime,
885        size: u64,
886        content_hash: blake3::Hash,
887        symbols: Vec<Symbol>,
888    ) {
889        self.entries.insert(
890            path,
891            CachedSymbols {
892                mtime,
893                size,
894                content_hash,
895                symbols,
896            },
897        );
898    }
899
900    /// Insert symbols only when the caller still belongs to the active cache generation.
901    pub fn insert_for_generation(
902        &mut self,
903        generation: u64,
904        path: PathBuf,
905        mtime: SystemTime,
906        size: u64,
907        content_hash: blake3::Hash,
908        symbols: Vec<Symbol>,
909    ) -> bool {
910        if self.generation != generation {
911            return false;
912        }
913        self.insert(path, mtime, size, content_hash, symbols);
914        true
915    }
916
917    /// Return cached symbols when the source file is still fresh.
918    pub fn get(&self, path: &Path, mtime: SystemTime) -> Option<Vec<Symbol>> {
919        self.entries.get(path).and_then(|cached| {
920            cached_file_is_fresh(path, cached.mtime, cached.size, cached.content_hash, mtime)
921                .then(|| cached.symbols.clone())
922        })
923    }
924
925    /// Return a cached symbol count when file metadata exactly matches the cache entry.
926    ///
927    /// This is the fast path for directory file-tree summaries: when mtime and
928    /// size are unchanged, callers can use the count without cloning symbols or
929    /// re-reading the file to verify a content hash.
930    pub fn symbol_count_if_metadata_matches(
931        &self,
932        path: &Path,
933        mtime: SystemTime,
934        size: u64,
935    ) -> Option<usize> {
936        self.entries.get(path).and_then(|cached| {
937            (cached.mtime == mtime && cached.size == size).then_some(cached.symbols.len())
938        })
939    }
940
941    /// Whether the cache has a still-valid entry for the given file mtime.
942    pub fn contains_path_with_mtime(&self, path: &Path, mtime: SystemTime) -> bool {
943        self.entries
944            .get(path)
945            .is_some_and(|cached| cached.mtime == mtime)
946    }
947
948    /// Load valid symbol entries from disk, dropping only entries whose source file changed.
949    pub fn load_from_disk(
950        &mut self,
951        storage_dir: &Path,
952        project_key: &str,
953        current_root: &Path,
954    ) -> usize {
955        debug_assert!(current_root.is_absolute());
956        let Some(cache) = symbol_cache_disk::read_from_disk(storage_dir, project_key) else {
957            return 0;
958        };
959
960        self.project_root = Some(current_root.to_path_buf());
961        self.entries.clear();
962        let mut loaded = 0usize;
963
964        for entry in cache.entries {
965            let Some(path) =
966                crate::search_index::cached_path_under_root(current_root, &entry.relative_path)
967            else {
968                continue;
969            };
970            let cached_freshness = FileFreshness {
971                mtime: entry.mtime,
972                size: entry.size,
973                content_hash: entry.content_hash,
974            };
975            let mtime = match cache_freshness::verify_file(&path, &cached_freshness) {
976                FreshnessVerdict::HotFresh => entry.mtime,
977                FreshnessVerdict::ContentFresh { new_mtime, .. } => new_mtime,
978                FreshnessVerdict::Stale | FreshnessVerdict::Deleted => continue,
979            };
980
981            self.entries.insert(
982                path,
983                CachedSymbols {
984                    mtime,
985                    size: entry.size,
986                    content_hash: entry.content_hash,
987                    symbols: entry.symbols,
988                },
989            );
990            loaded += 1;
991        }
992
993        loaded
994    }
995
996    /// Load valid symbol entries from disk only when the caller still belongs
997    /// to the active cache generation.
998    pub fn load_from_disk_for_generation(
999        &mut self,
1000        generation: u64,
1001        storage_dir: &Path,
1002        project_key: &str,
1003        current_root: &Path,
1004    ) -> usize {
1005        if self.generation != generation {
1006            return 0;
1007        }
1008        self.load_from_disk(storage_dir, project_key, current_root)
1009    }
1010
1011    /// Invalidate cached symbols for a specific file.
1012    pub fn invalidate(&mut self, path: &Path) {
1013        self.entries.remove(path);
1014    }
1015
1016    /// Clear all entries and advance the generation to ignore stale background writers.
1017    pub fn reset(&mut self) -> u64 {
1018        self.entries.clear();
1019        self.project_root = None;
1020        self.generation = self.generation.wrapping_add(1);
1021        self.generation
1022    }
1023
1024    /// Current generation token.
1025    pub fn generation(&self) -> u64 {
1026        self.generation
1027    }
1028
1029    /// Whether the cache has an entry for a file.
1030    pub fn contains_key(&self, path: &Path) -> bool {
1031        self.entries.contains_key(path)
1032    }
1033
1034    /// Number of cached entries.
1035    pub fn len(&self) -> usize {
1036        self.entries.len()
1037    }
1038
1039    pub(crate) fn project_root(&self) -> Option<PathBuf> {
1040        self.project_root.clone()
1041    }
1042
1043    pub(crate) fn disk_entries(
1044        &self,
1045    ) -> Vec<(&PathBuf, SystemTime, u64, blake3::Hash, &Vec<Symbol>)> {
1046        // Persist EVERY parsed entry, including files that legitimately parse to
1047        // zero symbols (e.g. a TS file with only imports, a config-shaped file).
1048        // Previously these were filtered out, so they were never written to disk;
1049        // on the next spawn `load_from_disk` couldn't reload them, the prewarm
1050        // skip-check (`contains_path_with_mtime`) missed, and they were re-parsed
1051        // on every startup forever (the "N new" churn in issue #86). An empty
1052        // entry serializes to `symbols: []` — a few bytes — and makes the prewarm
1053        // skip it. It also makes the "persisted symbol cache: N files" count
1054        // accurate (was overstated by the filtered-out empties).
1055        self.entries
1056            .iter()
1057            .map(|(path, cached)| {
1058                (
1059                    path,
1060                    cached.mtime,
1061                    cached.size,
1062                    cached.content_hash,
1063                    &cached.symbols,
1064                )
1065            })
1066            .collect()
1067    }
1068}
1069
1070/// Core parsing engine. Handles language detection, parse tree caching,
1071/// symbol table caching, and query pattern execution via tree-sitter.
1072pub struct FileParser {
1073    cache: HashMap<PathBuf, CachedTree>,
1074    parsers: HashMap<LangId, Parser>,
1075    symbol_cache: SharedSymbolCache,
1076    symbol_cache_generation: Option<u64>,
1077}
1078
1079impl FileParser {
1080    /// Create a new `FileParser` with an empty parse cache.
1081    pub fn new() -> Self {
1082        Self::with_symbol_cache(Arc::new(RwLock::new(SymbolCache::new())))
1083    }
1084
1085    /// Create a new `FileParser` backed by a shared symbol cache.
1086    pub fn with_symbol_cache(symbol_cache: SharedSymbolCache) -> Self {
1087        Self::with_symbol_cache_generation(symbol_cache, None)
1088    }
1089
1090    /// Create a new `FileParser` backed by a shared symbol cache generation.
1091    pub fn with_symbol_cache_generation(
1092        symbol_cache: SharedSymbolCache,
1093        symbol_cache_generation: Option<u64>,
1094    ) -> Self {
1095        Self {
1096            cache: HashMap::new(),
1097            parsers: HashMap::new(),
1098            symbol_cache,
1099            symbol_cache_generation,
1100        }
1101    }
1102
1103    fn parser_for(&mut self, lang: LangId) -> Result<&mut Parser, AftError> {
1104        use std::collections::hash_map::Entry;
1105
1106        match self.parsers.entry(lang) {
1107            Entry::Occupied(entry) => Ok(entry.into_mut()),
1108            Entry::Vacant(entry) => {
1109                let grammar = grammar_for(lang);
1110                let mut parser = Parser::new();
1111                parser.set_language(&grammar).map_err(|e| {
1112                    crate::slog_error!("grammar init failed for {:?}: {}", lang, e);
1113                    AftError::ParseError {
1114                        message: format!("grammar init failed for {:?}: {}", lang, e),
1115                    }
1116                })?;
1117                Ok(entry.insert(parser))
1118            }
1119        }
1120    }
1121
1122    /// Number of entries in the shared symbol cache.
1123    pub fn symbol_cache_len(&self) -> usize {
1124        self.symbol_cache
1125            .read()
1126            .map(|cache| cache.len())
1127            .unwrap_or(0)
1128    }
1129
1130    /// Shared symbol cache backing this parser.
1131    pub fn symbol_cache(&self) -> SharedSymbolCache {
1132        Arc::clone(&self.symbol_cache)
1133    }
1134
1135    /// Parse a file, returning the tree and detected language. Uses cache if
1136    /// the file hasn't been modified since last parse.
1137    pub fn parse(&mut self, path: &Path) -> Result<(&Tree, LangId), AftError> {
1138        let lang = detect_language(path).ok_or_else(|| AftError::InvalidRequest {
1139            message: format!(
1140                "unsupported file extension: {}",
1141                path.extension()
1142                    .and_then(|e| e.to_str())
1143                    .unwrap_or("<none>")
1144            ),
1145        })?;
1146
1147        let canon = path.to_path_buf();
1148        let current_mtime = std::fs::metadata(path)
1149            .and_then(|m| m.modified())
1150            .map_err(|e| AftError::FileNotFound {
1151                path: format!("{}: {}", path.display(), e),
1152            })?;
1153
1154        // Check cache validity. Mtime alone is not enough: editors and tests can
1155        // restore timestamps after changing file contents, so small files fall
1156        // back to a Blake3 content hash when size does not prove staleness.
1157        let needs_reparse = match self.cache.get(&canon) {
1158            Some(cached) => !cached_file_is_fresh(
1159                path,
1160                cached.mtime,
1161                cached.size,
1162                cached.content_hash,
1163                current_mtime,
1164            ),
1165            None => true,
1166        };
1167
1168        if needs_reparse {
1169            let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
1170                path: format!("{}: {}", path.display(), e),
1171            })?;
1172
1173            let tree = self.parser_for(lang)?.parse(&source, None).ok_or_else(|| {
1174                crate::slog_error!("parse failed for {}", path.display());
1175                AftError::ParseError {
1176                    message: format!("tree-sitter parse returned None for {}", path.display()),
1177                }
1178            })?;
1179
1180            self.cache.insert(
1181                canon.clone(),
1182                CachedTree {
1183                    mtime: current_mtime,
1184                    size: source.len() as u64,
1185                    content_hash: content_hash_for_source(&source),
1186                    tree,
1187                },
1188            );
1189        }
1190
1191        let cached = self.cache.get(&canon).ok_or_else(|| AftError::ParseError {
1192            message: format!("parser cache missing entry for {}", path.display()),
1193        })?;
1194        Ok((&cached.tree, lang))
1195    }
1196
1197    /// Like [`FileParser::parse`] but returns an owned `Tree` clone.
1198    ///
1199    /// Useful when the caller needs to hold the tree while also calling
1200    /// other mutable methods on this parser.
1201    pub fn parse_cloned(&mut self, path: &Path) -> Result<(Tree, LangId), AftError> {
1202        let (tree, lang) = self.parse(path)?;
1203        Ok((tree.clone(), lang))
1204    }
1205
1206    /// Like [`FileParser::parse`] but reuses caller-provided file contents and
1207    /// metadata instead of reading + stat'ing the file again.
1208    ///
1209    /// `parse()` reads the file from disk to build the tree. A caller that has
1210    /// ALREADY read the source (e.g. [`FileParser::extract_symbols`], which
1211    /// reads it to hash + extract symbols) would otherwise pay a second
1212    /// `read_to_string` for the exact same bytes on every cold-cache file — a
1213    /// real cost when warming the symbol cache / call graph over a large repo.
1214    /// This variant takes the already-read `source` and the already-computed
1215    /// `current_mtime`/`size`/`content_hash` so the cold path does one read and
1216    /// one hash. Tree-cache freshness semantics are identical to `parse()`.
1217    fn parse_with_source(
1218        &mut self,
1219        path: &Path,
1220        source: &str,
1221        current_mtime: std::time::SystemTime,
1222        size: u64,
1223        content_hash: blake3::Hash,
1224    ) -> Result<(&Tree, LangId), AftError> {
1225        let lang = detect_language(path).ok_or_else(|| AftError::InvalidRequest {
1226            message: format!(
1227                "unsupported file extension: {}",
1228                path.extension()
1229                    .and_then(|e| e.to_str())
1230                    .unwrap_or("<none>")
1231            ),
1232        })?;
1233
1234        let canon = path.to_path_buf();
1235        let needs_reparse = match self.cache.get(&canon) {
1236            Some(cached) => !cached_file_is_fresh(
1237                path,
1238                cached.mtime,
1239                cached.size,
1240                cached.content_hash,
1241                current_mtime,
1242            ),
1243            None => true,
1244        };
1245
1246        if needs_reparse {
1247            let tree = self.parser_for(lang)?.parse(source, None).ok_or_else(|| {
1248                crate::slog_error!("parse failed for {}", path.display());
1249                AftError::ParseError {
1250                    message: format!("tree-sitter parse returned None for {}", path.display()),
1251                }
1252            })?;
1253
1254            self.cache.insert(
1255                canon.clone(),
1256                CachedTree {
1257                    mtime: current_mtime,
1258                    size,
1259                    content_hash,
1260                    tree,
1261                },
1262            );
1263        }
1264
1265        let cached = self.cache.get(&canon).ok_or_else(|| AftError::ParseError {
1266            message: format!("parser cache missing entry for {}", path.display()),
1267        })?;
1268        Ok((&cached.tree, lang))
1269    }
1270
1271    /// Extract symbols from a file using language-specific query patterns.
1272    /// Results are cached by `(path, mtime)` — subsequent calls for unchanged
1273    /// files return the cached symbol table without re-parsing.
1274    pub fn extract_symbols(&mut self, path: &Path) -> Result<Vec<Symbol>, AftError> {
1275        let canon = path.to_path_buf();
1276        let current_mtime = std::fs::metadata(path)
1277            .and_then(|m| m.modified())
1278            .map_err(|e| AftError::FileNotFound {
1279                path: format!("{}: {}", path.display(), e),
1280            })?;
1281
1282        // Return cached symbols if file hasn't changed.
1283        if let Some(symbols) = self
1284            .symbol_cache
1285            .read()
1286            .map_err(|_| AftError::ParseError {
1287                message: "symbol cache lock poisoned".to_string(),
1288            })?
1289            .get(&canon, current_mtime)
1290        {
1291            return Ok(symbols);
1292        }
1293
1294        let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
1295            path: format!("{}: {}", path.display(), e),
1296        })?;
1297        let size = source.len() as u64;
1298        let content_hash = content_hash_for_source(&source);
1299
1300        let symbols = {
1301            // Reuse the source we just read instead of letting parse() read the
1302            // same file a second time (cold-path double-read, council perf #5).
1303            let (tree, lang) =
1304                self.parse_with_source(path, &source, current_mtime, size, content_hash)?;
1305            extract_symbols_from_tree(&source, tree, lang)?
1306        };
1307
1308        // Cache the result
1309        let mut symbol_cache = self
1310            .symbol_cache
1311            .write()
1312            .map_err(|_| AftError::ParseError {
1313                message: "symbol cache lock poisoned".to_string(),
1314            })?;
1315        if let Some(generation) = self.symbol_cache_generation {
1316            symbol_cache.insert_for_generation(
1317                generation,
1318                canon,
1319                current_mtime,
1320                size,
1321                content_hash,
1322                symbols.clone(),
1323            );
1324        } else {
1325            symbol_cache.insert(canon, current_mtime, size, content_hash, symbols.clone());
1326        }
1327
1328        Ok(symbols)
1329    }
1330
1331    /// Invalidate cached symbols for a specific file (e.g., after an edit).
1332    pub fn invalidate_symbols(&mut self, path: &Path) {
1333        if let Ok(mut symbol_cache) = self.symbol_cache.write() {
1334            symbol_cache.invalidate(path);
1335        }
1336        self.cache.remove(path);
1337    }
1338}
1339
1340/// Extract symbols from an already-parsed tree without reparsing.
1341///
1342/// Callers that already have a `tree_sitter::Tree` (e.g. callgraph::build_file_data)
1343/// should use this instead of `list_symbols(path)` to avoid the redundant parse.
1344pub fn extract_symbols_from_tree(
1345    source: &str,
1346    tree: &Tree,
1347    lang: LangId,
1348) -> Result<Vec<Symbol>, AftError> {
1349    let root = tree.root_node();
1350
1351    if lang == LangId::Html {
1352        return extract_html_symbols(source, &root);
1353    }
1354    if lang == LangId::Markdown {
1355        return extract_md_symbols(source, &root);
1356    }
1357    if lang == LangId::Vue {
1358        return extract_vue_symbols(source, &root);
1359    }
1360    if lang == LangId::Json {
1361        return extract_json_symbols(source, &root);
1362    }
1363    if lang == LangId::Yaml {
1364        return extract_yaml_symbols(source, &root);
1365    }
1366
1367    let query = cached_query_for(lang)?.ok_or_else(|| AftError::InvalidRequest {
1368        message: format!("no query patterns implemented for {:?} yet", lang),
1369    })?;
1370
1371    match lang {
1372        LangId::TypeScript | LangId::Tsx => extract_ts_symbols(source, &root, query),
1373        LangId::JavaScript => extract_js_symbols(source, &root, query),
1374        LangId::Python => extract_py_symbols(source, &root, query),
1375        LangId::Rust => extract_rs_symbols(source, &root, query),
1376        LangId::Go => extract_go_symbols(source, &root, query),
1377        LangId::C => extract_c_symbols(source, &root, query),
1378        LangId::Cpp => extract_cpp_symbols(source, &root, query),
1379        LangId::Zig => extract_zig_symbols(source, &root, query),
1380        LangId::CSharp => extract_csharp_symbols(source, &root, query),
1381        LangId::Bash => extract_bash_symbols(source, &root, query),
1382        LangId::Solidity => extract_solidity_symbols(source, &root, query),
1383        LangId::Scss => extract_scss_symbols(source, &root, query),
1384        LangId::Scala => extract_scala_symbols(source, &root, query),
1385        LangId::Java => extract_java_symbols(source, &root, query),
1386        LangId::Ruby => extract_ruby_symbols(source, &root, query),
1387        LangId::Kotlin => extract_kotlin_symbols(source, &root, query),
1388        LangId::Swift => extract_swift_symbols(source, &root, query),
1389        LangId::Php => extract_php_symbols(source, &root, query),
1390        LangId::Lua => extract_lua_symbols(source, &root, query),
1391        LangId::Perl => extract_perl_symbols(source, &root, query),
1392        LangId::Html | LangId::Markdown | LangId::Vue | LangId::Json | LangId::Yaml => {
1393            unreachable!("handled before query lookup")
1394        }
1395    }
1396}
1397
1398/// Build a Range from a tree-sitter Node.
1399pub(crate) fn node_range(node: &Node) -> Range {
1400    let start = node.start_position();
1401    let end = node.end_position();
1402    Range {
1403        start_line: start.row as u32,
1404        start_col: start.column as u32,
1405        end_line: end.row as u32,
1406        end_col: end.column as u32,
1407    }
1408}
1409
1410/// Build a Range from a tree-sitter Node, expanding upward to include
1411/// preceding attributes, decorators, and doc comments that belong to the symbol.
1412///
1413/// This ensures that when agents edit/replace a symbol, they get the full
1414/// declaration including `#[test]`, `#[derive(...)]`, `/// doc`, `@decorator`, etc.
1415pub(crate) fn node_range_with_decorators(node: &Node, source: &str, lang: LangId) -> Range {
1416    if matches!(lang, LangId::Python) {
1417        if let Some(parent) = node.parent() {
1418            if parent.kind() == "decorated_definition" {
1419                return node_range(&parent);
1420            }
1421        }
1422    }
1423
1424    // TypeScript / JavaScript / TSX: `export function foo() {}` parses as
1425    // `export_statement > function_declaration`. The function/class/etc.
1426    // node alone starts AFTER the `export ` keyword, so symbol replace would
1427    // produce a syntactically-broken `export export function foo() {}` if the
1428    // agent's replacement content includes its own `export` (it almost always
1429    // does, because they're replacing the *declaration*). Walk up to the
1430    // export_statement so the range covers `export ...`/`export default ...`.
1431    if matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
1432        if let Some(parent) = node.parent() {
1433            if parent.kind() == "export_statement" {
1434                return node_range_with_decorators_inner(&parent, source, lang);
1435            }
1436        }
1437    }
1438
1439    node_range_with_decorators_inner(node, source, lang)
1440}
1441
1442/// Inner walk that handles preceding-sibling expansion (decorators, doc comments).
1443/// Split from `node_range_with_decorators` so the export-statement parent path can
1444/// recurse without re-checking the export wrapper.
1445fn node_range_with_decorators_inner(node: &Node, source: &str, lang: LangId) -> Range {
1446    let mut range = node_range(node);
1447
1448    let mut current = *node;
1449    while let Some(prev) = current.prev_sibling() {
1450        let kind = prev.kind();
1451        let should_include = match lang {
1452            LangId::Rust => {
1453                // Include #[...] attributes
1454                kind == "attribute_item"
1455                    // Include /// doc comments (but not regular // comments)
1456                    || (kind == "line_comment"
1457                        && node_text(source, &prev).starts_with("///"))
1458                    // Include /** ... */ doc comments
1459                    || (kind == "block_comment"
1460                        && node_text(source, &prev).starts_with("/**"))
1461            }
1462            LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1463                // Include @decorator
1464                kind == "decorator"
1465                    // Include adjacent /** JSDoc */ comments, but do not let a
1466                    // blank line attach file-level docs to the following symbol.
1467                    || (kind == "comment"
1468                        && node_text(source, &prev).starts_with("/**")
1469                        && is_adjacent_line(&prev, &current, source))
1470            }
1471            LangId::Go | LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp | LangId::Bash => {
1472                // Include doc comments only if immediately above (no blank line gap)
1473                kind == "comment" && is_adjacent_line(&prev, &current, source)
1474            }
1475            LangId::Solidity
1476            | LangId::Scss
1477            | LangId::Scala
1478            | LangId::Java
1479            | LangId::Kotlin
1480            | LangId::Swift
1481            | LangId::Php => {
1482                // Include `///` doc comments and `/** */` doc blocks if immediately above
1483                let text = node_text(source, &prev);
1484                (kind == "comment" || kind == "line_comment" || kind == "block_comment")
1485                    && (text.starts_with("///") || text.starts_with("/**"))
1486                    && is_adjacent_line(&prev, &current, source)
1487            }
1488            LangId::Ruby | LangId::Lua => {
1489                // Include adjacent `#`/`---` style comments used as documentation.
1490                let text = node_text(source, &prev);
1491                kind == "comment"
1492                    && (text.starts_with('#') || text.starts_with("---"))
1493                    && is_adjacent_line(&prev, &current, source)
1494            }
1495            LangId::Perl => false,
1496            LangId::Python => {
1497                // Decorators are handled by decorated_definition capture
1498                false
1499            }
1500            LangId::Html | LangId::Markdown | LangId::Vue | LangId::Json | LangId::Yaml => false,
1501        };
1502
1503        if should_include {
1504            range.start_line = prev.start_position().row as u32;
1505            range.start_col = prev.start_position().column as u32;
1506            current = prev;
1507        } else {
1508            break;
1509        }
1510    }
1511
1512    range
1513}
1514
1515/// Check if two nodes are on adjacent lines (no blank line between them).
1516fn is_adjacent_line(upper: &Node, lower: &Node, source: &str) -> bool {
1517    let upper_end = upper.end_position().row;
1518    let lower_start = lower.start_position().row;
1519
1520    if lower_start == 0 || lower_start <= upper_end {
1521        return true;
1522    }
1523
1524    // Check that there's no blank line between them
1525    let lines: Vec<&str> = source.lines().collect();
1526    for row in (upper_end + 1)..lower_start {
1527        if row < lines.len() && lines[row].trim().is_empty() {
1528            return false;
1529        }
1530    }
1531    true
1532}
1533
1534/// Extract the text of a node from source.
1535pub(crate) fn node_text<'a>(source: &'a str, node: &Node) -> &'a str {
1536    &source[node.byte_range()]
1537}
1538
1539fn lexical_declaration_has_function_value(node: &Node) -> bool {
1540    let mut cursor = node.walk();
1541    if !cursor.goto_first_child() {
1542        return false;
1543    }
1544
1545    loop {
1546        let child = cursor.node();
1547        if matches!(
1548            child.kind(),
1549            "arrow_function" | "function_expression" | "generator_function"
1550        ) {
1551            return true;
1552        }
1553
1554        if lexical_declaration_has_function_value(&child) {
1555            return true;
1556        }
1557
1558        if !cursor.goto_next_sibling() {
1559            break;
1560        }
1561    }
1562
1563    false
1564}
1565
1566fn variable_declarator_has_function_value(node: &Node) -> bool {
1567    node.child_by_field_name("value").is_some_and(|value| {
1568        matches!(
1569            value.kind(),
1570            "arrow_function" | "function_expression" | "generator_function"
1571        )
1572    })
1573}
1574
1575/// Collect byte ranges of all export_statement nodes from query matches.
1576fn collect_export_ranges(source: &str, root: &Node, query: &Query) -> Vec<std::ops::Range<usize>> {
1577    let export_idx = query
1578        .capture_names()
1579        .iter()
1580        .position(|n| *n == "export.stmt");
1581    let export_idx = match export_idx {
1582        Some(i) => i as u32,
1583        None => return vec![],
1584    };
1585
1586    let mut cursor = QueryCursor::new();
1587    let mut ranges = Vec::new();
1588    let mut matches = cursor.matches(query, *root, source.as_bytes());
1589
1590    while let Some(m) = {
1591        matches.advance();
1592        matches.get()
1593    } {
1594        for cap in m.captures {
1595            if cap.index == export_idx {
1596                ranges.push(cap.node.byte_range());
1597            }
1598        }
1599    }
1600    ranges
1601}
1602
1603/// Check if a node's byte range is contained within any export statement.
1604fn is_exported(node: &Node, export_ranges: &[std::ops::Range<usize>]) -> bool {
1605    let r = node.byte_range();
1606    export_ranges
1607        .iter()
1608        .any(|er| er.start <= r.start && r.end <= er.end)
1609}
1610
1611fn collect_exported_symbol_names(source: &str, root: &Node) -> HashSet<String> {
1612    let mut exported = HashSet::new();
1613    collect_exported_symbol_names_inner(source, root, &mut exported);
1614    exported
1615}
1616
1617fn collect_exported_symbol_names_inner(source: &str, node: &Node, exported: &mut HashSet<String>) {
1618    if node.kind() == "export_statement" {
1619        collect_names_from_export_statement(source, node, exported);
1620    }
1621
1622    let mut cursor = node.walk();
1623    if !cursor.goto_first_child() {
1624        return;
1625    }
1626
1627    loop {
1628        let child = cursor.node();
1629        collect_exported_symbol_names_inner(source, &child, exported);
1630        if !cursor.goto_next_sibling() {
1631            break;
1632        }
1633    }
1634}
1635
1636fn collect_names_from_export_statement(source: &str, node: &Node, exported: &mut HashSet<String>) {
1637    let mut cursor = node.walk();
1638    if !cursor.goto_first_child() {
1639        return;
1640    }
1641
1642    let mut saw_default = false;
1643    loop {
1644        let child = cursor.node();
1645        match child.kind() {
1646            "default" => saw_default = true,
1647            "export_clause" => collect_names_from_export_clause(source, &child, exported),
1648            "identifier" | "type_identifier" | "property_identifier" if saw_default => {
1649                exported.insert(node_text(source, &child).to_string());
1650                return;
1651            }
1652            _ => {}
1653        }
1654        if !cursor.goto_next_sibling() {
1655            break;
1656        }
1657    }
1658}
1659
1660fn collect_names_from_export_clause(source: &str, node: &Node, exported: &mut HashSet<String>) {
1661    let mut cursor = node.walk();
1662    if !cursor.goto_first_child() {
1663        return;
1664    }
1665
1666    loop {
1667        let child = cursor.node();
1668        if child.kind() == "export_specifier" {
1669            if let Some(exported_name) = last_identifier_text(source, &child) {
1670                exported.insert(exported_name);
1671            }
1672        }
1673        if !cursor.goto_next_sibling() {
1674            break;
1675        }
1676    }
1677}
1678
1679fn last_identifier_text(source: &str, node: &Node) -> Option<String> {
1680    let mut cursor = node.walk();
1681    if !cursor.goto_first_child() {
1682        return None;
1683    }
1684
1685    let mut last = None;
1686    loop {
1687        let child = cursor.node();
1688        if matches!(
1689            child.kind(),
1690            "identifier"
1691                | "type_identifier"
1692                | "property_identifier"
1693                | "shorthand_property_identifier"
1694        ) {
1695            last = Some(node_text(source, &child).to_string());
1696        }
1697        if !cursor.goto_next_sibling() {
1698            break;
1699        }
1700    }
1701    last
1702}
1703
1704fn mark_named_exports(symbols: &mut [Symbol], exported_names: &HashSet<String>) {
1705    for symbol in symbols {
1706        if symbol.scope_chain.is_empty()
1707            && symbol.parent.is_none()
1708            && exported_names.contains(&symbol.name)
1709        {
1710            symbol.exported = true;
1711        }
1712    }
1713}
1714
1715/// Extract the first line of a node as its signature.
1716fn extract_signature(source: &str, node: &Node) -> String {
1717    let text = node_text(source, node);
1718    let first_line = text.lines().next().unwrap_or(text);
1719    // Trim trailing opening brace if present
1720    let trimmed = first_line.trim_end();
1721    let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
1722    trimmed.to_string()
1723}
1724
1725fn push_default_export_symbol(
1726    symbols: &mut Vec<Symbol>,
1727    source: &str,
1728    lang: LangId,
1729    body_node: Node,
1730    def_node: Node,
1731) {
1732    let kind = if body_node.kind() == "class" {
1733        SymbolKind::Class
1734    } else {
1735        SymbolKind::Function
1736    };
1737
1738    symbols.push(Symbol {
1739        name: "default".to_string(),
1740        kind,
1741        range: node_range_with_decorators(&def_node, source, lang),
1742        signature: Some(extract_signature(source, &def_node)),
1743        scope_chain: vec![],
1744        exported: true,
1745        parent: None,
1746    });
1747}
1748
1749/// Extract symbols from TypeScript / TSX source.
1750fn extract_ts_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1751    let lang = LangId::TypeScript;
1752    let capture_names = query.capture_names();
1753
1754    let export_ranges = collect_export_ranges(source, root, query);
1755    let exported_names = collect_exported_symbol_names(source, root);
1756
1757    let mut symbols = Vec::new();
1758    let mut cursor = QueryCursor::new();
1759    let mut matches = cursor.matches(query, *root, source.as_bytes());
1760
1761    while let Some(m) = {
1762        matches.advance();
1763        matches.get()
1764    } {
1765        // Determine what kind of match this is by looking at capture names
1766        let mut fn_name_node = None;
1767        let mut fn_def_node = None;
1768        let mut arrow_name_node = None;
1769        let mut arrow_def_node = None;
1770        let mut arrow_decl_node = None;
1771        let mut class_name_node = None;
1772        let mut class_def_node = None;
1773        let mut method_class_name_node = None;
1774        let mut method_name_node = None;
1775        let mut method_def_node = None;
1776        let mut interface_name_node = None;
1777        let mut interface_def_node = None;
1778        let mut enum_name_node = None;
1779        let mut enum_def_node = None;
1780        let mut type_alias_name_node = None;
1781        let mut type_alias_def_node = None;
1782        let mut var_name_node = None;
1783        let mut var_def_node = None;
1784        let mut var_decl_node = None;
1785        let mut default_body_node = None;
1786        let mut default_def_node = None;
1787
1788        for cap in m.captures {
1789            let Some(&name) = capture_names.get(cap.index as usize) else {
1790                continue;
1791            };
1792            match name {
1793                "fn.name" => fn_name_node = Some(cap.node),
1794                "fn.def" => fn_def_node = Some(cap.node),
1795                "arrow.name" => arrow_name_node = Some(cap.node),
1796                "arrow.def" => arrow_def_node = Some(cap.node),
1797                "arrow.decl" => arrow_decl_node = Some(cap.node),
1798                "class.name" => class_name_node = Some(cap.node),
1799                "class.def" => class_def_node = Some(cap.node),
1800                "method.class_name" => method_class_name_node = Some(cap.node),
1801                "method.name" => method_name_node = Some(cap.node),
1802                "method.def" => method_def_node = Some(cap.node),
1803                "interface.name" => interface_name_node = Some(cap.node),
1804                "interface.def" => interface_def_node = Some(cap.node),
1805                "enum.name" => enum_name_node = Some(cap.node),
1806                "enum.def" => enum_def_node = Some(cap.node),
1807                "type_alias.name" => type_alias_name_node = Some(cap.node),
1808                "type_alias.def" => type_alias_def_node = Some(cap.node),
1809                "var.name" => var_name_node = Some(cap.node),
1810                "var.def" => var_def_node = Some(cap.node),
1811                "var.decl" => var_decl_node = Some(cap.node),
1812                "default.body" => default_body_node = Some(cap.node),
1813                "default.def" => default_def_node = Some(cap.node),
1814                _ => {}
1815            }
1816        }
1817
1818        // Function declaration
1819        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1820            symbols.push(Symbol {
1821                name: node_text(source, &name_node).to_string(),
1822                kind: SymbolKind::Function,
1823                range: node_range_with_decorators(&def_node, source, lang),
1824                signature: Some(extract_signature(source, &def_node)),
1825                scope_chain: vec![],
1826                exported: is_exported(&def_node, &export_ranges),
1827                parent: None,
1828            });
1829        }
1830
1831        // Arrow/function expression declarator
1832        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
1833            let range_node = arrow_decl_node.unwrap_or(def_node);
1834            symbols.push(Symbol {
1835                name: node_text(source, &name_node).to_string(),
1836                kind: SymbolKind::Function,
1837                range: node_range_with_decorators(&range_node, source, lang),
1838                signature: Some(extract_signature(source, &def_node)),
1839                scope_chain: vec![],
1840                exported: is_exported(&def_node, &export_ranges),
1841                parent: None,
1842            });
1843        }
1844
1845        // Anonymous/default function or class expression
1846        if let (Some(body_node), Some(def_node)) = (default_body_node, default_def_node) {
1847            push_default_export_symbol(&mut symbols, source, lang, body_node, def_node);
1848        }
1849
1850        // Class declaration
1851        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
1852            symbols.push(Symbol {
1853                name: node_text(source, &name_node).to_string(),
1854                kind: SymbolKind::Class,
1855                range: node_range_with_decorators(&def_node, source, lang),
1856                signature: Some(extract_signature(source, &def_node)),
1857                scope_chain: vec![],
1858                exported: is_exported(&def_node, &export_ranges),
1859                parent: None,
1860            });
1861        }
1862
1863        // Method definition
1864        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
1865            (method_class_name_node, method_name_node, method_def_node)
1866        {
1867            let class_name = node_text(source, &class_name_node).to_string();
1868            symbols.push(Symbol {
1869                name: node_text(source, &name_node).to_string(),
1870                kind: SymbolKind::Method,
1871                range: node_range_with_decorators(&def_node, source, lang),
1872                signature: Some(extract_signature(source, &def_node)),
1873                scope_chain: vec![class_name.clone()],
1874                exported: false, // methods inherit export from class
1875                parent: Some(class_name),
1876            });
1877        }
1878
1879        // Interface declaration
1880        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
1881            symbols.push(Symbol {
1882                name: node_text(source, &name_node).to_string(),
1883                kind: SymbolKind::Interface,
1884                range: node_range_with_decorators(&def_node, source, lang),
1885                signature: Some(extract_signature(source, &def_node)),
1886                scope_chain: vec![],
1887                exported: is_exported(&def_node, &export_ranges),
1888                parent: None,
1889            });
1890        }
1891
1892        // Enum declaration
1893        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
1894            symbols.push(Symbol {
1895                name: node_text(source, &name_node).to_string(),
1896                kind: SymbolKind::Enum,
1897                range: node_range_with_decorators(&def_node, source, lang),
1898                signature: Some(extract_signature(source, &def_node)),
1899                scope_chain: vec![],
1900                exported: is_exported(&def_node, &export_ranges),
1901                parent: None,
1902            });
1903        }
1904
1905        // Type alias
1906        if let (Some(name_node), Some(def_node)) = (type_alias_name_node, type_alias_def_node) {
1907            symbols.push(Symbol {
1908                name: node_text(source, &name_node).to_string(),
1909                kind: SymbolKind::TypeAlias,
1910                range: node_range_with_decorators(&def_node, source, lang),
1911                signature: Some(extract_signature(source, &def_node)),
1912                scope_chain: vec![],
1913                exported: is_exported(&def_node, &export_ranges),
1914                parent: None,
1915            });
1916        }
1917
1918        // Top-level const/let variable declaration (not arrow functions — those are handled above)
1919        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
1920            // Only include module-scope variables (parent is program/export_statement, not inside a function)
1921            let is_top_level = def_node
1922                .parent()
1923                .map(|p| p.kind() == "program" || p.kind() == "export_statement")
1924                .unwrap_or(false);
1925            let range_node = var_decl_node.unwrap_or(def_node);
1926            let is_function_like = if range_node.kind() == "variable_declarator" {
1927                variable_declarator_has_function_value(&range_node)
1928            } else {
1929                lexical_declaration_has_function_value(&def_node)
1930            };
1931            let name = node_text(source, &name_node).to_string();
1932            let already_captured = symbols.iter().any(|s| s.name == name);
1933            if is_top_level && !is_function_like && !already_captured {
1934                symbols.push(Symbol {
1935                    name,
1936                    kind: SymbolKind::Variable,
1937                    range: node_range_with_decorators(&range_node, source, lang),
1938                    signature: Some(extract_signature(source, &def_node)),
1939                    scope_chain: vec![],
1940                    exported: is_exported(&def_node, &export_ranges),
1941                    parent: None,
1942                });
1943            }
1944        }
1945    }
1946
1947    mark_named_exports(&mut symbols, &exported_names);
1948
1949    // Deduplicate: methods can appear as both class and method captures
1950    dedup_symbols(&mut symbols);
1951    Ok(symbols)
1952}
1953
1954/// Extract symbols from JavaScript source.
1955fn extract_js_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1956    let lang = LangId::JavaScript;
1957    let capture_names = query.capture_names();
1958
1959    let export_ranges = collect_export_ranges(source, root, query);
1960    let exported_names = collect_exported_symbol_names(source, root);
1961
1962    let mut symbols = Vec::new();
1963    let mut cursor = QueryCursor::new();
1964    let mut matches = cursor.matches(query, *root, source.as_bytes());
1965
1966    while let Some(m) = {
1967        matches.advance();
1968        matches.get()
1969    } {
1970        let mut fn_name_node = None;
1971        let mut fn_def_node = None;
1972        let mut arrow_name_node = None;
1973        let mut arrow_def_node = None;
1974        let mut arrow_decl_node = None;
1975        let mut class_name_node = None;
1976        let mut class_def_node = None;
1977        let mut method_class_name_node = None;
1978        let mut method_name_node = None;
1979        let mut method_def_node = None;
1980        let mut default_body_node = None;
1981        let mut default_def_node = None;
1982        let mut var_name_node = None;
1983        let mut var_def_node = None;
1984        let mut var_decl_node = None;
1985
1986        for cap in m.captures {
1987            let Some(&name) = capture_names.get(cap.index as usize) else {
1988                continue;
1989            };
1990            match name {
1991                "fn.name" => fn_name_node = Some(cap.node),
1992                "fn.def" => fn_def_node = Some(cap.node),
1993                "arrow.name" => arrow_name_node = Some(cap.node),
1994                "arrow.def" => arrow_def_node = Some(cap.node),
1995                "arrow.decl" => arrow_decl_node = Some(cap.node),
1996                "class.name" => class_name_node = Some(cap.node),
1997                "class.def" => class_def_node = Some(cap.node),
1998                "method.class_name" => method_class_name_node = Some(cap.node),
1999                "method.name" => method_name_node = Some(cap.node),
2000                "method.def" => method_def_node = Some(cap.node),
2001                "default.body" => default_body_node = Some(cap.node),
2002                "default.def" => default_def_node = Some(cap.node),
2003                "var.name" => var_name_node = Some(cap.node),
2004                "var.def" => var_def_node = Some(cap.node),
2005                "var.decl" => var_decl_node = Some(cap.node),
2006                _ => {}
2007            }
2008        }
2009
2010        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2011            symbols.push(Symbol {
2012                name: node_text(source, &name_node).to_string(),
2013                kind: SymbolKind::Function,
2014                range: node_range_with_decorators(&def_node, source, lang),
2015                signature: Some(extract_signature(source, &def_node)),
2016                scope_chain: vec![],
2017                exported: is_exported(&def_node, &export_ranges),
2018                parent: None,
2019            });
2020        }
2021
2022        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
2023            let range_node = arrow_decl_node.unwrap_or(def_node);
2024            symbols.push(Symbol {
2025                name: node_text(source, &name_node).to_string(),
2026                kind: SymbolKind::Function,
2027                range: node_range_with_decorators(&range_node, source, lang),
2028                signature: Some(extract_signature(source, &def_node)),
2029                scope_chain: vec![],
2030                exported: is_exported(&def_node, &export_ranges),
2031                parent: None,
2032            });
2033        }
2034
2035        if let (Some(body_node), Some(def_node)) = (default_body_node, default_def_node) {
2036            push_default_export_symbol(&mut symbols, source, lang, body_node, def_node);
2037        }
2038
2039        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
2040            symbols.push(Symbol {
2041                name: node_text(source, &name_node).to_string(),
2042                kind: SymbolKind::Class,
2043                range: node_range_with_decorators(&def_node, source, lang),
2044                signature: Some(extract_signature(source, &def_node)),
2045                scope_chain: vec![],
2046                exported: is_exported(&def_node, &export_ranges),
2047                parent: None,
2048            });
2049        }
2050
2051        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
2052            (method_class_name_node, method_name_node, method_def_node)
2053        {
2054            let class_name = node_text(source, &class_name_node).to_string();
2055            symbols.push(Symbol {
2056                name: node_text(source, &name_node).to_string(),
2057                kind: SymbolKind::Method,
2058                range: node_range_with_decorators(&def_node, source, lang),
2059                signature: Some(extract_signature(source, &def_node)),
2060                scope_chain: vec![class_name.clone()],
2061                exported: false,
2062                parent: Some(class_name),
2063            });
2064        }
2065
2066        // Top-level const/let/var declarations. JS_QUERY captures these the same
2067        // way TS does, but the JS extractor previously dropped them (they fell
2068        // into the `_ => {}` arm), so e.g. `export const VERSION = "1.0"` in a
2069        // .js file produced no symbol for outline/dead_code/callgraph. Mirror the
2070        // TS extractor: module-scope, non-function-valued, not already captured.
2071        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
2072            let is_top_level = def_node
2073                .parent()
2074                .map(|p| p.kind() == "program" || p.kind() == "export_statement")
2075                .unwrap_or(false);
2076            let range_node = var_decl_node.unwrap_or(def_node);
2077            let is_function_like = if range_node.kind() == "variable_declarator" {
2078                variable_declarator_has_function_value(&range_node)
2079            } else {
2080                lexical_declaration_has_function_value(&def_node)
2081            };
2082            let name = node_text(source, &name_node).to_string();
2083            let already_captured = symbols.iter().any(|s| s.name == name);
2084            if is_top_level && !is_function_like && !already_captured {
2085                symbols.push(Symbol {
2086                    name,
2087                    kind: SymbolKind::Variable,
2088                    range: node_range_with_decorators(&range_node, source, lang),
2089                    signature: Some(extract_signature(source, &def_node)),
2090                    scope_chain: vec![],
2091                    exported: is_exported(&def_node, &export_ranges),
2092                    parent: None,
2093                });
2094            }
2095        }
2096    }
2097
2098    mark_named_exports(&mut symbols, &exported_names);
2099    dedup_symbols(&mut symbols);
2100    Ok(symbols)
2101}
2102
2103/// Walk parent nodes to build a scope chain for Python symbols.
2104/// A function inside `class_definition > block` gets the class name in its scope.
2105fn py_scope_chain(node: &Node, source: &str) -> Vec<String> {
2106    let mut chain = Vec::new();
2107    let mut current = node.parent();
2108    while let Some(parent) = current {
2109        if parent.kind() == "class_definition" {
2110            if let Some(name_node) = parent.child_by_field_name("name") {
2111                chain.push(node_text(source, &name_node).to_string());
2112            }
2113        }
2114        current = parent.parent();
2115    }
2116    chain.reverse();
2117    chain
2118}
2119
2120/// Extract symbols from Python source.
2121fn extract_py_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2122    let lang = LangId::Python;
2123    let capture_names = query.capture_names();
2124
2125    let mut symbols = Vec::new();
2126    let mut cursor = QueryCursor::new();
2127    let mut matches = cursor.matches(query, *root, source.as_bytes());
2128
2129    // Track decorated definitions to avoid double-counting
2130    let mut decorated_fn_lines = std::collections::HashSet::new();
2131
2132    // First pass: collect decorated definition info
2133    {
2134        let mut cursor2 = QueryCursor::new();
2135        let mut matches2 = cursor2.matches(query, *root, source.as_bytes());
2136        while let Some(m) = {
2137            matches2.advance();
2138            matches2.get()
2139        } {
2140            let mut dec_def_node = None;
2141            let mut dec_decorator_node = None;
2142
2143            for cap in m.captures {
2144                let Some(&name) = capture_names.get(cap.index as usize) else {
2145                    continue;
2146                };
2147                match name {
2148                    "dec.def" => dec_def_node = Some(cap.node),
2149                    "dec.decorator" => dec_decorator_node = Some(cap.node),
2150                    _ => {}
2151                }
2152            }
2153
2154            if let (Some(def_node), Some(_dec_node)) = (dec_def_node, dec_decorator_node) {
2155                // Find the inner function_definition or class_definition
2156                let mut child_cursor = def_node.walk();
2157                if child_cursor.goto_first_child() {
2158                    loop {
2159                        let child = child_cursor.node();
2160                        if child.kind() == "function_definition"
2161                            || child.kind() == "class_definition"
2162                        {
2163                            decorated_fn_lines.insert(child.start_position().row);
2164                        }
2165                        if !child_cursor.goto_next_sibling() {
2166                            break;
2167                        }
2168                    }
2169                }
2170            }
2171        }
2172    }
2173
2174    while let Some(m) = {
2175        matches.advance();
2176        matches.get()
2177    } {
2178        let mut fn_name_node = None;
2179        let mut fn_def_node = None;
2180        let mut class_name_node = None;
2181        let mut class_def_node = None;
2182
2183        for cap in m.captures {
2184            let Some(&name) = capture_names.get(cap.index as usize) else {
2185                continue;
2186            };
2187            match name {
2188                "fn.name" => fn_name_node = Some(cap.node),
2189                "fn.def" => fn_def_node = Some(cap.node),
2190                "class.name" => class_name_node = Some(cap.node),
2191                "class.def" => class_def_node = Some(cap.node),
2192                _ => {}
2193            }
2194        }
2195
2196        // Function definition
2197        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2198            let scope = py_scope_chain(&def_node, source);
2199            let is_method = !scope.is_empty();
2200            let name = node_text(source, &name_node).to_string();
2201            // Skip __init__ and other dunders as separate symbols — they're methods
2202            let kind = if is_method {
2203                SymbolKind::Method
2204            } else {
2205                SymbolKind::Function
2206            };
2207
2208            // Build signature — include decorator if this is a decorated function
2209            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
2210                // Find the decorated_definition parent to get decorator text
2211                let mut sig_parts = Vec::new();
2212                let mut parent = def_node.parent();
2213                while let Some(p) = parent {
2214                    if p.kind() == "decorated_definition" {
2215                        // Get decorator lines
2216                        let mut dc = p.walk();
2217                        if dc.goto_first_child() {
2218                            loop {
2219                                if dc.node().kind() == "decorator" {
2220                                    sig_parts.push(node_text(source, &dc.node()).to_string());
2221                                }
2222                                if !dc.goto_next_sibling() {
2223                                    break;
2224                                }
2225                            }
2226                        }
2227                        break;
2228                    }
2229                    parent = p.parent();
2230                }
2231                sig_parts.push(extract_signature(source, &def_node));
2232                Some(sig_parts.join("\n"))
2233            } else {
2234                Some(extract_signature(source, &def_node))
2235            };
2236
2237            symbols.push(Symbol {
2238                name,
2239                kind,
2240                range: node_range_with_decorators(&def_node, source, lang),
2241                signature: sig,
2242                scope_chain: scope.clone(),
2243                exported: false, // Python has no export concept
2244                parent: scope.last().cloned(),
2245            });
2246        }
2247
2248        // Class definition
2249        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
2250            let scope = py_scope_chain(&def_node, source);
2251
2252            // Build signature — include decorator if decorated
2253            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
2254                let mut sig_parts = Vec::new();
2255                let mut parent = def_node.parent();
2256                while let Some(p) = parent {
2257                    if p.kind() == "decorated_definition" {
2258                        let mut dc = p.walk();
2259                        if dc.goto_first_child() {
2260                            loop {
2261                                if dc.node().kind() == "decorator" {
2262                                    sig_parts.push(node_text(source, &dc.node()).to_string());
2263                                }
2264                                if !dc.goto_next_sibling() {
2265                                    break;
2266                                }
2267                            }
2268                        }
2269                        break;
2270                    }
2271                    parent = p.parent();
2272                }
2273                sig_parts.push(extract_signature(source, &def_node));
2274                Some(sig_parts.join("\n"))
2275            } else {
2276                Some(extract_signature(source, &def_node))
2277            };
2278
2279            symbols.push(Symbol {
2280                name: node_text(source, &name_node).to_string(),
2281                kind: SymbolKind::Class,
2282                range: node_range_with_decorators(&def_node, source, lang),
2283                signature: sig,
2284                scope_chain: scope.clone(),
2285                exported: false,
2286                parent: scope.last().cloned(),
2287            });
2288        }
2289    }
2290
2291    dedup_symbols(&mut symbols);
2292    Ok(symbols)
2293}
2294
2295fn rust_mod_scope_chain(node: &Node, source: &str) -> Vec<String> {
2296    let mut scopes = Vec::new();
2297    let mut current = node.parent();
2298
2299    while let Some(parent) = current {
2300        if parent.kind() == "mod_item" {
2301            if let Some(name_node) = parent.child_by_field_name("name") {
2302                scopes.push(node_text(source, &name_node).to_string());
2303            }
2304        }
2305        current = parent.parent();
2306    }
2307
2308    scopes.reverse();
2309    scopes
2310}
2311
2312/// Extract symbols from Rust source.
2313/// Handles: free functions, struct, enum, trait (as Interface), impl methods with scope chains.
2314fn extract_rs_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2315    let lang = LangId::Rust;
2316    let capture_names = query.capture_names();
2317
2318    // Collect all visibility_modifier byte ranges first
2319    let mut vis_ranges: Vec<std::ops::Range<usize>> = Vec::new();
2320    {
2321        let vis_idx = capture_names.iter().position(|n| *n == "vis.mod");
2322        if let Some(idx) = vis_idx {
2323            let idx = idx as u32;
2324            let mut cursor = QueryCursor::new();
2325            let mut matches = cursor.matches(query, *root, source.as_bytes());
2326            while let Some(m) = {
2327                matches.advance();
2328                matches.get()
2329            } {
2330                for cap in m.captures {
2331                    if cap.index == idx {
2332                        vis_ranges.push(cap.node.byte_range());
2333                    }
2334                }
2335            }
2336        }
2337    }
2338
2339    let is_pub = |node: &Node| -> bool {
2340        // Check if the node has a visibility_modifier as a direct child
2341        let mut child_cursor = node.walk();
2342        if child_cursor.goto_first_child() {
2343            loop {
2344                if child_cursor.node().kind() == "visibility_modifier" {
2345                    return true;
2346                }
2347                if !child_cursor.goto_next_sibling() {
2348                    break;
2349                }
2350            }
2351        }
2352        false
2353    };
2354
2355    let mut symbols = Vec::new();
2356    let mut cursor = QueryCursor::new();
2357    let mut matches = cursor.matches(query, *root, source.as_bytes());
2358
2359    while let Some(m) = {
2360        matches.advance();
2361        matches.get()
2362    } {
2363        let mut fn_name_node = None;
2364        let mut fn_def_node = None;
2365        let mut struct_name_node = None;
2366        let mut struct_def_node = None;
2367        let mut enum_name_node = None;
2368        let mut enum_def_node = None;
2369        let mut trait_name_node = None;
2370        let mut trait_def_node = None;
2371        let mut impl_def_node = None;
2372
2373        for cap in m.captures {
2374            let Some(&name) = capture_names.get(cap.index as usize) else {
2375                continue;
2376            };
2377            match name {
2378                "fn.name" => fn_name_node = Some(cap.node),
2379                "fn.def" => fn_def_node = Some(cap.node),
2380                "struct.name" => struct_name_node = Some(cap.node),
2381                "struct.def" => struct_def_node = Some(cap.node),
2382                "enum.name" => enum_name_node = Some(cap.node),
2383                "enum.def" => enum_def_node = Some(cap.node),
2384                "trait.name" => trait_name_node = Some(cap.node),
2385                "trait.def" => trait_def_node = Some(cap.node),
2386                "impl.def" => impl_def_node = Some(cap.node),
2387                _ => {}
2388            }
2389        }
2390
2391        // Free function (skip impl/trait declaration lists; inline mod bodies are still free fns)
2392        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2393            let declaration_list_owner = def_node
2394                .parent()
2395                .filter(|parent| parent.kind() == "declaration_list")
2396                .and_then(|parent| parent.parent());
2397            let in_non_module_declaration_list = declaration_list_owner
2398                .as_ref()
2399                .is_some_and(|owner| owner.kind() != "mod_item");
2400
2401            if !in_non_module_declaration_list {
2402                let scope_chain = rust_mod_scope_chain(&def_node, source);
2403                symbols.push(Symbol {
2404                    name: node_text(source, &name_node).to_string(),
2405                    kind: SymbolKind::Function,
2406                    range: node_range_with_decorators(&def_node, source, lang),
2407                    signature: Some(extract_signature(source, &def_node)),
2408                    scope_chain: scope_chain.clone(),
2409                    exported: is_pub(&def_node),
2410                    parent: scope_chain.last().cloned(),
2411                });
2412            }
2413        }
2414
2415        // Struct
2416        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
2417            symbols.push(Symbol {
2418                name: node_text(source, &name_node).to_string(),
2419                kind: SymbolKind::Struct,
2420                range: node_range_with_decorators(&def_node, source, lang),
2421                signature: Some(extract_signature(source, &def_node)),
2422                scope_chain: vec![],
2423                exported: is_pub(&def_node),
2424                parent: None,
2425            });
2426        }
2427
2428        // Enum
2429        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
2430            symbols.push(Symbol {
2431                name: node_text(source, &name_node).to_string(),
2432                kind: SymbolKind::Enum,
2433                range: node_range_with_decorators(&def_node, source, lang),
2434                signature: Some(extract_signature(source, &def_node)),
2435                scope_chain: vec![],
2436                exported: is_pub(&def_node),
2437                parent: None,
2438            });
2439        }
2440
2441        // Trait (mapped to Interface kind)
2442        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
2443            symbols.push(Symbol {
2444                name: node_text(source, &name_node).to_string(),
2445                kind: SymbolKind::Interface,
2446                range: node_range_with_decorators(&def_node, source, lang),
2447                signature: Some(extract_signature(source, &def_node)),
2448                scope_chain: vec![],
2449                exported: is_pub(&def_node),
2450                parent: None,
2451            });
2452        }
2453
2454        // Impl block — extract methods from inside
2455        if let Some(impl_node) = impl_def_node {
2456            // Find the type name(s) from the impl
2457            // `impl TypeName { ... }` → scope = ["TypeName"]
2458            // `impl Trait for TypeName { ... }` → scope = ["Trait for TypeName"]
2459            let mut type_names: Vec<String> = Vec::new();
2460            let mut child_cursor = impl_node.walk();
2461            if child_cursor.goto_first_child() {
2462                loop {
2463                    let child = child_cursor.node();
2464                    if child.kind() == "type_identifier" || child.kind() == "generic_type" {
2465                        type_names.push(node_text(source, &child).to_string());
2466                    }
2467                    if !child_cursor.goto_next_sibling() {
2468                        break;
2469                    }
2470                }
2471            }
2472
2473            let scope_name = if type_names.len() >= 2 {
2474                // impl Trait for Type
2475                format!("{} for {}", type_names[0], type_names[1])
2476            } else if type_names.len() == 1 {
2477                type_names[0].clone()
2478            } else {
2479                String::new()
2480            };
2481
2482            let parent_name = type_names.last().cloned().unwrap_or_default();
2483
2484            // Find declaration_list and extract function_items
2485            let mut child_cursor = impl_node.walk();
2486            if child_cursor.goto_first_child() {
2487                loop {
2488                    let child = child_cursor.node();
2489                    if child.kind() == "declaration_list" {
2490                        let mut fn_cursor = child.walk();
2491                        if fn_cursor.goto_first_child() {
2492                            loop {
2493                                let fn_node = fn_cursor.node();
2494                                if fn_node.kind() == "function_item" {
2495                                    if let Some(name_node) = fn_node.child_by_field_name("name") {
2496                                        symbols.push(Symbol {
2497                                            name: node_text(source, &name_node).to_string(),
2498                                            kind: SymbolKind::Method,
2499                                            range: node_range_with_decorators(
2500                                                &fn_node, source, lang,
2501                                            ),
2502                                            signature: Some(extract_signature(source, &fn_node)),
2503                                            scope_chain: if scope_name.is_empty() {
2504                                                vec![]
2505                                            } else {
2506                                                vec![scope_name.clone()]
2507                                            },
2508                                            exported: is_pub(&fn_node),
2509                                            parent: if parent_name.is_empty() {
2510                                                None
2511                                            } else {
2512                                                Some(parent_name.clone())
2513                                            },
2514                                        });
2515                                    }
2516                                }
2517                                if !fn_cursor.goto_next_sibling() {
2518                                    break;
2519                                }
2520                            }
2521                        }
2522                    }
2523                    if !child_cursor.goto_next_sibling() {
2524                        break;
2525                    }
2526                }
2527            }
2528        }
2529    }
2530
2531    dedup_symbols(&mut symbols);
2532    Ok(symbols)
2533}
2534
2535/// Extract symbols from Go source.
2536/// Handles: functions, methods (with receiver scope chain), struct/interface types,
2537/// uppercase-first-letter export detection.
2538fn extract_go_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2539    let lang = LangId::Go;
2540    let capture_names = query.capture_names();
2541
2542    let is_go_exported = |name: &str| -> bool {
2543        name.chars()
2544            .next()
2545            .map(|c| c.is_uppercase())
2546            .unwrap_or(false)
2547    };
2548
2549    let mut symbols = Vec::new();
2550    let mut cursor = QueryCursor::new();
2551    let mut matches = cursor.matches(query, *root, source.as_bytes());
2552
2553    while let Some(m) = {
2554        matches.advance();
2555        matches.get()
2556    } {
2557        let mut fn_name_node = None;
2558        let mut fn_def_node = None;
2559        let mut method_name_node = None;
2560        let mut method_def_node = None;
2561        let mut type_name_node = None;
2562        let mut type_body_node = None;
2563        let mut type_def_node = None;
2564
2565        for cap in m.captures {
2566            let Some(&name) = capture_names.get(cap.index as usize) else {
2567                continue;
2568            };
2569            match name {
2570                "fn.name" => fn_name_node = Some(cap.node),
2571                "fn.def" => fn_def_node = Some(cap.node),
2572                "method.name" => method_name_node = Some(cap.node),
2573                "method.def" => method_def_node = Some(cap.node),
2574                "type.name" => type_name_node = Some(cap.node),
2575                "type.body" => type_body_node = Some(cap.node),
2576                "type.def" => type_def_node = Some(cap.node),
2577                _ => {}
2578            }
2579        }
2580
2581        // Function declaration
2582        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2583            let name = node_text(source, &name_node).to_string();
2584            symbols.push(Symbol {
2585                exported: is_go_exported(&name),
2586                name,
2587                kind: SymbolKind::Function,
2588                range: node_range_with_decorators(&def_node, source, lang),
2589                signature: Some(extract_signature(source, &def_node)),
2590                scope_chain: vec![],
2591                parent: None,
2592            });
2593        }
2594
2595        // Method declaration (with receiver)
2596        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
2597            let name = node_text(source, &name_node).to_string();
2598
2599            // Extract receiver type from the first parameter_list
2600            let receiver_type = extract_go_receiver_type(&def_node, source);
2601            let scope_chain = if let Some(ref rt) = receiver_type {
2602                vec![rt.clone()]
2603            } else {
2604                vec![]
2605            };
2606
2607            symbols.push(Symbol {
2608                exported: is_go_exported(&name),
2609                name,
2610                kind: SymbolKind::Method,
2611                range: node_range_with_decorators(&def_node, source, lang),
2612                signature: Some(extract_signature(source, &def_node)),
2613                scope_chain,
2614                parent: receiver_type,
2615            });
2616        }
2617
2618        // Type declarations (struct or interface)
2619        if let (Some(name_node), Some(body_node), Some(def_node)) =
2620            (type_name_node, type_body_node, type_def_node)
2621        {
2622            let name = node_text(source, &name_node).to_string();
2623            let kind = match body_node.kind() {
2624                "struct_type" => SymbolKind::Struct,
2625                "interface_type" => SymbolKind::Interface,
2626                _ => SymbolKind::TypeAlias,
2627            };
2628
2629            symbols.push(Symbol {
2630                exported: is_go_exported(&name),
2631                name,
2632                kind,
2633                range: node_range_with_decorators(&def_node, source, lang),
2634                signature: Some(extract_signature(source, &def_node)),
2635                scope_chain: vec![],
2636                parent: None,
2637            });
2638        }
2639    }
2640
2641    dedup_symbols(&mut symbols);
2642    Ok(symbols)
2643}
2644
2645/// Extract the receiver type from a Go method_declaration node.
2646/// e.g. `func (m *MyStruct) String()` → Some("MyStruct")
2647fn extract_go_receiver_type(method_node: &Node, source: &str) -> Option<String> {
2648    // The first parameter_list is the receiver
2649    let mut child_cursor = method_node.walk();
2650    if child_cursor.goto_first_child() {
2651        loop {
2652            let child = child_cursor.node();
2653            if child.kind() == "parameter_list" {
2654                // Walk into parameter_list to find type_identifier
2655                return find_type_identifier_recursive(&child, source);
2656            }
2657            if !child_cursor.goto_next_sibling() {
2658                break;
2659            }
2660        }
2661    }
2662    None
2663}
2664
2665fn split_scope_text(text: &str, separator: &str) -> Vec<String> {
2666    text.split(separator)
2667        .map(str::trim)
2668        .filter(|segment| !segment.is_empty())
2669        .map(ToString::to_string)
2670        .collect()
2671}
2672
2673fn last_scope_segment(text: &str, separator: &str) -> String {
2674    split_scope_text(text, separator)
2675        .pop()
2676        .unwrap_or_else(|| text.trim().to_string())
2677}
2678
2679fn zig_container_scope_chain(node: &Node, source: &str) -> Vec<String> {
2680    let mut chain = Vec::new();
2681    let mut current = node.parent();
2682
2683    while let Some(parent) = current {
2684        if matches!(
2685            parent.kind(),
2686            "struct_declaration" | "enum_declaration" | "union_declaration" | "opaque_declaration"
2687        ) {
2688            if let Some(container) = parent.parent() {
2689                if container.kind() == "variable_declaration" {
2690                    let mut cursor = container.walk();
2691                    if cursor.goto_first_child() {
2692                        loop {
2693                            let child = cursor.node();
2694                            if child.kind() == "identifier" {
2695                                chain.push(node_text(source, &child).to_string());
2696                                break;
2697                            }
2698                            if !cursor.goto_next_sibling() {
2699                                break;
2700                            }
2701                        }
2702                    }
2703                }
2704            }
2705        }
2706        current = parent.parent();
2707    }
2708
2709    chain.reverse();
2710    chain
2711}
2712
2713fn csharp_scope_chain(node: &Node, source: &str) -> Vec<String> {
2714    let mut chain = Vec::new();
2715    let mut current = node.parent();
2716
2717    while let Some(parent) = current {
2718        match parent.kind() {
2719            "namespace_declaration" | "file_scoped_namespace_declaration" => {
2720                if let Some(name_node) = parent.child_by_field_name("name") {
2721                    chain.push(node_text(source, &name_node).to_string());
2722                }
2723            }
2724            "class_declaration"
2725            | "interface_declaration"
2726            | "struct_declaration"
2727            | "record_declaration" => {
2728                if let Some(name_node) = parent.child_by_field_name("name") {
2729                    chain.push(node_text(source, &name_node).to_string());
2730                }
2731            }
2732            _ => {}
2733        }
2734        current = parent.parent();
2735    }
2736
2737    chain.reverse();
2738    chain
2739}
2740
2741fn cpp_parent_scope_chain(node: &Node, source: &str) -> Vec<String> {
2742    let mut chain = Vec::new();
2743    let mut current = node.parent();
2744
2745    while let Some(parent) = current {
2746        match parent.kind() {
2747            "namespace_definition" => {
2748                if let Some(name_node) = parent.child_by_field_name("name") {
2749                    chain.push(node_text(source, &name_node).to_string());
2750                }
2751            }
2752            "class_specifier" | "struct_specifier" => {
2753                if let Some(name_node) = parent.child_by_field_name("name") {
2754                    chain.push(last_scope_segment(node_text(source, &name_node), "::"));
2755                }
2756            }
2757            _ => {}
2758        }
2759        current = parent.parent();
2760    }
2761
2762    chain.reverse();
2763    chain
2764}
2765
2766fn template_signature(source: &str, template_node: &Node, item_node: &Node) -> String {
2767    format!(
2768        "{}\n{}",
2769        extract_signature(source, template_node),
2770        extract_signature(source, item_node)
2771    )
2772}
2773
2774/// Extract symbols from C source.
2775fn extract_c_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2776    let lang = LangId::C;
2777    let capture_names = query.capture_names();
2778
2779    let mut symbols = Vec::new();
2780    let mut cursor = QueryCursor::new();
2781    let mut matches = cursor.matches(query, *root, source.as_bytes());
2782
2783    while let Some(m) = {
2784        matches.advance();
2785        matches.get()
2786    } {
2787        let mut fn_name_node = None;
2788        let mut fn_def_node = None;
2789        let mut struct_name_node = None;
2790        let mut struct_def_node = None;
2791        let mut enum_name_node = None;
2792        let mut enum_def_node = None;
2793        let mut type_name_node = None;
2794        let mut type_def_node = None;
2795        let mut macro_name_node = None;
2796        let mut macro_def_node = None;
2797
2798        for cap in m.captures {
2799            let Some(&name) = capture_names.get(cap.index as usize) else {
2800                continue;
2801            };
2802            match name {
2803                "fn.name" => fn_name_node = Some(cap.node),
2804                "fn.def" => fn_def_node = Some(cap.node),
2805                "struct.name" => struct_name_node = Some(cap.node),
2806                "struct.def" => struct_def_node = Some(cap.node),
2807                "enum.name" => enum_name_node = Some(cap.node),
2808                "enum.def" => enum_def_node = Some(cap.node),
2809                "type.name" => type_name_node = Some(cap.node),
2810                "type.def" => type_def_node = Some(cap.node),
2811                "macro.name" => macro_name_node = Some(cap.node),
2812                "macro.def" => macro_def_node = Some(cap.node),
2813                _ => {}
2814            }
2815        }
2816
2817        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2818            symbols.push(Symbol {
2819                name: node_text(source, &name_node).to_string(),
2820                kind: SymbolKind::Function,
2821                range: node_range_with_decorators(&def_node, source, lang),
2822                signature: Some(extract_signature(source, &def_node)),
2823                scope_chain: vec![],
2824                exported: false,
2825                parent: None,
2826            });
2827        }
2828
2829        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
2830            symbols.push(Symbol {
2831                name: node_text(source, &name_node).to_string(),
2832                kind: SymbolKind::Struct,
2833                range: node_range_with_decorators(&def_node, source, lang),
2834                signature: Some(extract_signature(source, &def_node)),
2835                scope_chain: vec![],
2836                exported: false,
2837                parent: None,
2838            });
2839        }
2840
2841        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
2842            symbols.push(Symbol {
2843                name: node_text(source, &name_node).to_string(),
2844                kind: SymbolKind::Enum,
2845                range: node_range_with_decorators(&def_node, source, lang),
2846                signature: Some(extract_signature(source, &def_node)),
2847                scope_chain: vec![],
2848                exported: false,
2849                parent: None,
2850            });
2851        }
2852
2853        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
2854            symbols.push(Symbol {
2855                name: node_text(source, &name_node).to_string(),
2856                kind: SymbolKind::TypeAlias,
2857                range: node_range_with_decorators(&def_node, source, lang),
2858                signature: Some(extract_signature(source, &def_node)),
2859                scope_chain: vec![],
2860                exported: false,
2861                parent: None,
2862            });
2863        }
2864
2865        if let (Some(name_node), Some(def_node)) = (macro_name_node, macro_def_node) {
2866            symbols.push(Symbol {
2867                name: node_text(source, &name_node).to_string(),
2868                kind: SymbolKind::Variable,
2869                range: node_range(&def_node),
2870                signature: Some(extract_signature(source, &def_node)),
2871                scope_chain: vec![],
2872                exported: false,
2873                parent: None,
2874            });
2875        }
2876    }
2877
2878    dedup_symbols(&mut symbols);
2879    Ok(symbols)
2880}
2881
2882/// Extract symbols from C++ source.
2883fn extract_cpp_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
2884    let lang = LangId::Cpp;
2885    let capture_names = query.capture_names();
2886
2887    let mut type_names = HashSet::new();
2888    {
2889        let mut cursor = QueryCursor::new();
2890        let mut matches = cursor.matches(query, *root, source.as_bytes());
2891        while let Some(m) = {
2892            matches.advance();
2893            matches.get()
2894        } {
2895            for cap in m.captures {
2896                let Some(&name) = capture_names.get(cap.index as usize) else {
2897                    continue;
2898                };
2899                match name {
2900                    "class.name"
2901                    | "struct.name"
2902                    | "template.class.name"
2903                    | "template.struct.name" => {
2904                        type_names.insert(last_scope_segment(node_text(source, &cap.node), "::"));
2905                    }
2906                    _ => {}
2907                }
2908            }
2909        }
2910    }
2911
2912    let mut symbols = Vec::new();
2913    let mut cursor = QueryCursor::new();
2914    let mut matches = cursor.matches(query, *root, source.as_bytes());
2915
2916    while let Some(m) = {
2917        matches.advance();
2918        matches.get()
2919    } {
2920        let mut fn_name_node = None;
2921        let mut fn_def_node = None;
2922        let mut method_name_node = None;
2923        let mut method_def_node = None;
2924        let mut qual_scope_node = None;
2925        let mut qual_name_node = None;
2926        let mut qual_def_node = None;
2927        let mut class_name_node = None;
2928        let mut class_def_node = None;
2929        let mut struct_name_node = None;
2930        let mut struct_def_node = None;
2931        let mut enum_name_node = None;
2932        let mut enum_def_node = None;
2933        let mut namespace_name_node = None;
2934        let mut namespace_def_node = None;
2935        let mut template_class_name_node = None;
2936        let mut template_class_def_node = None;
2937        let mut template_class_item_node = None;
2938        let mut template_struct_name_node = None;
2939        let mut template_struct_def_node = None;
2940        let mut template_struct_item_node = None;
2941        let mut template_fn_name_node = None;
2942        let mut template_fn_def_node = None;
2943        let mut template_fn_item_node = None;
2944        let mut template_qual_scope_node = None;
2945        let mut template_qual_name_node = None;
2946        let mut template_qual_def_node = None;
2947        let mut template_qual_item_node = None;
2948
2949        for cap in m.captures {
2950            let Some(&name) = capture_names.get(cap.index as usize) else {
2951                continue;
2952            };
2953            match name {
2954                "fn.name" => fn_name_node = Some(cap.node),
2955                "fn.def" => fn_def_node = Some(cap.node),
2956                "method.name" => method_name_node = Some(cap.node),
2957                "method.def" => method_def_node = Some(cap.node),
2958                "qual.scope" => qual_scope_node = Some(cap.node),
2959                "qual.name" => qual_name_node = Some(cap.node),
2960                "qual.def" => qual_def_node = Some(cap.node),
2961                "class.name" => class_name_node = Some(cap.node),
2962                "class.def" => class_def_node = Some(cap.node),
2963                "struct.name" => struct_name_node = Some(cap.node),
2964                "struct.def" => struct_def_node = Some(cap.node),
2965                "enum.name" => enum_name_node = Some(cap.node),
2966                "enum.def" => enum_def_node = Some(cap.node),
2967                "namespace.name" => namespace_name_node = Some(cap.node),
2968                "namespace.def" => namespace_def_node = Some(cap.node),
2969                "template.class.name" => template_class_name_node = Some(cap.node),
2970                "template.class.def" => template_class_def_node = Some(cap.node),
2971                "template.class.item" => template_class_item_node = Some(cap.node),
2972                "template.struct.name" => template_struct_name_node = Some(cap.node),
2973                "template.struct.def" => template_struct_def_node = Some(cap.node),
2974                "template.struct.item" => template_struct_item_node = Some(cap.node),
2975                "template.fn.name" => template_fn_name_node = Some(cap.node),
2976                "template.fn.def" => template_fn_def_node = Some(cap.node),
2977                "template.fn.item" => template_fn_item_node = Some(cap.node),
2978                "template.qual.scope" => template_qual_scope_node = Some(cap.node),
2979                "template.qual.name" => template_qual_name_node = Some(cap.node),
2980                "template.qual.def" => template_qual_def_node = Some(cap.node),
2981                "template.qual.item" => template_qual_item_node = Some(cap.node),
2982                _ => {}
2983            }
2984        }
2985
2986        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
2987            let in_template = def_node
2988                .parent()
2989                .map(|parent| parent.kind() == "template_declaration")
2990                .unwrap_or(false);
2991            if !in_template {
2992                let scope_chain = cpp_parent_scope_chain(&def_node, source);
2993                symbols.push(Symbol {
2994                    name: node_text(source, &name_node).to_string(),
2995                    kind: SymbolKind::Function,
2996                    range: node_range_with_decorators(&def_node, source, lang),
2997                    signature: Some(extract_signature(source, &def_node)),
2998                    scope_chain: scope_chain.clone(),
2999                    exported: false,
3000                    parent: scope_chain.last().cloned(),
3001                });
3002            }
3003        }
3004
3005        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
3006            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3007            symbols.push(Symbol {
3008                name: node_text(source, &name_node).to_string(),
3009                kind: SymbolKind::Method,
3010                range: node_range_with_decorators(&def_node, source, lang),
3011                signature: Some(extract_signature(source, &def_node)),
3012                scope_chain: scope_chain.clone(),
3013                exported: false,
3014                parent: scope_chain.last().cloned(),
3015            });
3016        }
3017
3018        if let (Some(scope_node), Some(name_node), Some(def_node)) =
3019            (qual_scope_node, qual_name_node, qual_def_node)
3020        {
3021            let in_template = def_node
3022                .parent()
3023                .map(|parent| parent.kind() == "template_declaration")
3024                .unwrap_or(false);
3025            if !in_template {
3026                let scope_text = node_text(source, &scope_node);
3027                let scope_chain = split_scope_text(scope_text, "::");
3028                let parent = scope_chain.last().cloned();
3029                let kind = if parent
3030                    .as_ref()
3031                    .map(|segment| type_names.contains(segment))
3032                    .unwrap_or(false)
3033                {
3034                    SymbolKind::Method
3035                } else {
3036                    SymbolKind::Function
3037                };
3038
3039                symbols.push(Symbol {
3040                    name: node_text(source, &name_node).to_string(),
3041                    kind,
3042                    range: node_range_with_decorators(&def_node, source, lang),
3043                    signature: Some(extract_signature(source, &def_node)),
3044                    scope_chain,
3045                    exported: false,
3046                    parent,
3047                });
3048            }
3049        }
3050
3051        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
3052            let in_template = def_node
3053                .parent()
3054                .map(|parent| parent.kind() == "template_declaration")
3055                .unwrap_or(false);
3056            if !in_template {
3057                let scope_chain = cpp_parent_scope_chain(&def_node, source);
3058                let name = last_scope_segment(node_text(source, &name_node), "::");
3059                symbols.push(Symbol {
3060                    name: name.clone(),
3061                    kind: SymbolKind::Class,
3062                    range: node_range_with_decorators(&def_node, source, lang),
3063                    signature: Some(extract_signature(source, &def_node)),
3064                    scope_chain: scope_chain.clone(),
3065                    exported: false,
3066                    parent: scope_chain.last().cloned(),
3067                });
3068            }
3069        }
3070
3071        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
3072            let in_template = def_node
3073                .parent()
3074                .map(|parent| parent.kind() == "template_declaration")
3075                .unwrap_or(false);
3076            if !in_template {
3077                let scope_chain = cpp_parent_scope_chain(&def_node, source);
3078                let name = last_scope_segment(node_text(source, &name_node), "::");
3079                symbols.push(Symbol {
3080                    name: name.clone(),
3081                    kind: SymbolKind::Struct,
3082                    range: node_range_with_decorators(&def_node, source, lang),
3083                    signature: Some(extract_signature(source, &def_node)),
3084                    scope_chain: scope_chain.clone(),
3085                    exported: false,
3086                    parent: scope_chain.last().cloned(),
3087                });
3088            }
3089        }
3090
3091        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
3092            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3093            let name = last_scope_segment(node_text(source, &name_node), "::");
3094            symbols.push(Symbol {
3095                name: name.clone(),
3096                kind: SymbolKind::Enum,
3097                range: node_range_with_decorators(&def_node, source, lang),
3098                signature: Some(extract_signature(source, &def_node)),
3099                scope_chain: scope_chain.clone(),
3100                exported: false,
3101                parent: scope_chain.last().cloned(),
3102            });
3103        }
3104
3105        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
3106            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3107            symbols.push(Symbol {
3108                name: node_text(source, &name_node).to_string(),
3109                kind: SymbolKind::TypeAlias,
3110                range: node_range_with_decorators(&def_node, source, lang),
3111                signature: Some(extract_signature(source, &def_node)),
3112                scope_chain: scope_chain.clone(),
3113                exported: false,
3114                parent: scope_chain.last().cloned(),
3115            });
3116        }
3117
3118        if let (Some(name_node), Some(def_node), Some(item_node)) = (
3119            template_class_name_node,
3120            template_class_def_node,
3121            template_class_item_node,
3122        ) {
3123            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3124            let name = last_scope_segment(node_text(source, &name_node), "::");
3125            symbols.push(Symbol {
3126                name: name.clone(),
3127                kind: SymbolKind::Class,
3128                range: node_range_with_decorators(&def_node, source, lang),
3129                signature: Some(template_signature(source, &def_node, &item_node)),
3130                scope_chain: scope_chain.clone(),
3131                exported: false,
3132                parent: scope_chain.last().cloned(),
3133            });
3134        }
3135
3136        if let (Some(name_node), Some(def_node), Some(item_node)) = (
3137            template_struct_name_node,
3138            template_struct_def_node,
3139            template_struct_item_node,
3140        ) {
3141            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3142            let name = last_scope_segment(node_text(source, &name_node), "::");
3143            symbols.push(Symbol {
3144                name: name.clone(),
3145                kind: SymbolKind::Struct,
3146                range: node_range_with_decorators(&def_node, source, lang),
3147                signature: Some(template_signature(source, &def_node, &item_node)),
3148                scope_chain: scope_chain.clone(),
3149                exported: false,
3150                parent: scope_chain.last().cloned(),
3151            });
3152        }
3153
3154        if let (Some(name_node), Some(def_node), Some(item_node)) = (
3155            template_fn_name_node,
3156            template_fn_def_node,
3157            template_fn_item_node,
3158        ) {
3159            let scope_chain = cpp_parent_scope_chain(&def_node, source);
3160            symbols.push(Symbol {
3161                name: node_text(source, &name_node).to_string(),
3162                kind: SymbolKind::Function,
3163                range: node_range_with_decorators(&def_node, source, lang),
3164                signature: Some(template_signature(source, &def_node, &item_node)),
3165                scope_chain: scope_chain.clone(),
3166                exported: false,
3167                parent: scope_chain.last().cloned(),
3168            });
3169        }
3170
3171        if let (Some(scope_node), Some(name_node), Some(def_node), Some(item_node)) = (
3172            template_qual_scope_node,
3173            template_qual_name_node,
3174            template_qual_def_node,
3175            template_qual_item_node,
3176        ) {
3177            let scope_chain = split_scope_text(node_text(source, &scope_node), "::");
3178            let parent = scope_chain.last().cloned();
3179            let kind = if parent
3180                .as_ref()
3181                .map(|segment| type_names.contains(segment))
3182                .unwrap_or(false)
3183            {
3184                SymbolKind::Method
3185            } else {
3186                SymbolKind::Function
3187            };
3188
3189            symbols.push(Symbol {
3190                name: node_text(source, &name_node).to_string(),
3191                kind,
3192                range: node_range_with_decorators(&def_node, source, lang),
3193                signature: Some(template_signature(source, &def_node, &item_node)),
3194                scope_chain,
3195                exported: false,
3196                parent,
3197            });
3198        }
3199    }
3200
3201    dedup_symbols(&mut symbols);
3202    Ok(symbols)
3203}
3204
3205/// Extract symbols from Zig source.
3206fn extract_zig_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
3207    let lang = LangId::Zig;
3208    let capture_names = query.capture_names();
3209
3210    let mut symbols = Vec::new();
3211    let mut cursor = QueryCursor::new();
3212    let mut matches = cursor.matches(query, *root, source.as_bytes());
3213
3214    while let Some(m) = {
3215        matches.advance();
3216        matches.get()
3217    } {
3218        let mut fn_name_node = None;
3219        let mut fn_def_node = None;
3220        let mut struct_name_node = None;
3221        let mut struct_def_node = None;
3222        let mut enum_name_node = None;
3223        let mut enum_def_node = None;
3224        let mut union_name_node = None;
3225        let mut union_def_node = None;
3226        let mut const_name_node = None;
3227        let mut const_def_node = None;
3228        let mut test_name_node = None;
3229        let mut test_def_node = None;
3230
3231        for cap in m.captures {
3232            let Some(&name) = capture_names.get(cap.index as usize) else {
3233                continue;
3234            };
3235            match name {
3236                "fn.name" => fn_name_node = Some(cap.node),
3237                "fn.def" => fn_def_node = Some(cap.node),
3238                "struct.name" => struct_name_node = Some(cap.node),
3239                "struct.def" => struct_def_node = Some(cap.node),
3240                "enum.name" => enum_name_node = Some(cap.node),
3241                "enum.def" => enum_def_node = Some(cap.node),
3242                "union.name" => union_name_node = Some(cap.node),
3243                "union.def" => union_def_node = Some(cap.node),
3244                "const.name" => const_name_node = Some(cap.node),
3245                "const.def" => const_def_node = Some(cap.node),
3246                "test.name" => test_name_node = Some(cap.node),
3247                "test.def" => test_def_node = Some(cap.node),
3248                _ => {}
3249            }
3250        }
3251
3252        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
3253            let scope_chain = zig_container_scope_chain(&def_node, source);
3254            let kind = if scope_chain.is_empty() {
3255                SymbolKind::Function
3256            } else {
3257                SymbolKind::Method
3258            };
3259            symbols.push(Symbol {
3260                name: node_text(source, &name_node).to_string(),
3261                kind,
3262                range: node_range_with_decorators(&def_node, source, lang),
3263                signature: Some(extract_signature(source, &def_node)),
3264                scope_chain: scope_chain.clone(),
3265                exported: false,
3266                parent: scope_chain.last().cloned(),
3267            });
3268        }
3269
3270        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
3271            symbols.push(Symbol {
3272                name: node_text(source, &name_node).to_string(),
3273                kind: SymbolKind::Struct,
3274                range: node_range_with_decorators(&def_node, source, lang),
3275                signature: Some(extract_signature(source, &def_node)),
3276                scope_chain: vec![],
3277                exported: false,
3278                parent: None,
3279            });
3280        }
3281
3282        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
3283            symbols.push(Symbol {
3284                name: node_text(source, &name_node).to_string(),
3285                kind: SymbolKind::Enum,
3286                range: node_range_with_decorators(&def_node, source, lang),
3287                signature: Some(extract_signature(source, &def_node)),
3288                scope_chain: vec![],
3289                exported: false,
3290                parent: None,
3291            });
3292        }
3293
3294        if let (Some(name_node), Some(def_node)) = (union_name_node, union_def_node) {
3295            symbols.push(Symbol {
3296                name: node_text(source, &name_node).to_string(),
3297                kind: SymbolKind::TypeAlias,
3298                range: node_range_with_decorators(&def_node, source, lang),
3299                signature: Some(extract_signature(source, &def_node)),
3300                scope_chain: vec![],
3301                exported: false,
3302                parent: None,
3303            });
3304        }
3305
3306        if let (Some(name_node), Some(def_node)) = (const_name_node, const_def_node) {
3307            let signature = extract_signature(source, &def_node);
3308            let is_container = signature.contains("= struct")
3309                || signature.contains("= enum")
3310                || signature.contains("= union")
3311                || signature.contains("= opaque");
3312            let is_const = signature.trim_start().starts_with("const ");
3313            let name = node_text(source, &name_node).to_string();
3314            let already_captured = symbols.iter().any(|symbol| symbol.name == name);
3315            if is_const && !is_container && !already_captured {
3316                symbols.push(Symbol {
3317                    name,
3318                    kind: SymbolKind::Variable,
3319                    range: node_range_with_decorators(&def_node, source, lang),
3320                    signature: Some(signature),
3321                    scope_chain: vec![],
3322                    exported: false,
3323                    parent: None,
3324                });
3325            }
3326        }
3327
3328        if let (Some(name_node), Some(def_node)) = (test_name_node, test_def_node) {
3329            let scope_chain = zig_container_scope_chain(&def_node, source);
3330            symbols.push(Symbol {
3331                name: node_text(source, &name_node).trim_matches('"').to_string(),
3332                kind: SymbolKind::Function,
3333                range: node_range_with_decorators(&def_node, source, lang),
3334                signature: Some(extract_signature(source, &def_node)),
3335                scope_chain: scope_chain.clone(),
3336                exported: false,
3337                parent: scope_chain.last().cloned(),
3338            });
3339        }
3340    }
3341
3342    dedup_symbols(&mut symbols);
3343    Ok(symbols)
3344}
3345
3346/// Extract symbols from C# source.
3347fn extract_csharp_symbols(
3348    source: &str,
3349    root: &Node,
3350    query: &Query,
3351) -> Result<Vec<Symbol>, AftError> {
3352    let lang = LangId::CSharp;
3353    let capture_names = query.capture_names();
3354
3355    let mut symbols = Vec::new();
3356    let mut cursor = QueryCursor::new();
3357    let mut matches = cursor.matches(query, *root, source.as_bytes());
3358
3359    while let Some(m) = {
3360        matches.advance();
3361        matches.get()
3362    } {
3363        let mut class_name_node = None;
3364        let mut class_def_node = None;
3365        let mut interface_name_node = None;
3366        let mut interface_def_node = None;
3367        let mut struct_name_node = None;
3368        let mut struct_def_node = None;
3369        let mut enum_name_node = None;
3370        let mut enum_def_node = None;
3371        let mut method_name_node = None;
3372        let mut method_def_node = None;
3373        let mut property_name_node = None;
3374        let mut property_def_node = None;
3375        let mut namespace_name_node = None;
3376        let mut namespace_def_node = None;
3377
3378        for cap in m.captures {
3379            let Some(&name) = capture_names.get(cap.index as usize) else {
3380                continue;
3381            };
3382            match name {
3383                "class.name" => class_name_node = Some(cap.node),
3384                "class.def" => class_def_node = Some(cap.node),
3385                "interface.name" => interface_name_node = Some(cap.node),
3386                "interface.def" => interface_def_node = Some(cap.node),
3387                "struct.name" => struct_name_node = Some(cap.node),
3388                "struct.def" => struct_def_node = Some(cap.node),
3389                "enum.name" => enum_name_node = Some(cap.node),
3390                "enum.def" => enum_def_node = Some(cap.node),
3391                "method.name" => method_name_node = Some(cap.node),
3392                "method.def" => method_def_node = Some(cap.node),
3393                "property.name" => property_name_node = Some(cap.node),
3394                "property.def" => property_def_node = Some(cap.node),
3395                "namespace.name" => namespace_name_node = Some(cap.node),
3396                "namespace.def" => namespace_def_node = Some(cap.node),
3397                _ => {}
3398            }
3399        }
3400
3401        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
3402            let scope_chain = csharp_scope_chain(&def_node, source);
3403            symbols.push(Symbol {
3404                name: node_text(source, &name_node).to_string(),
3405                kind: SymbolKind::Class,
3406                range: node_range_with_decorators(&def_node, source, lang),
3407                signature: Some(extract_signature(source, &def_node)),
3408                scope_chain: scope_chain.clone(),
3409                exported: false,
3410                parent: scope_chain.last().cloned(),
3411            });
3412        }
3413
3414        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
3415            let scope_chain = csharp_scope_chain(&def_node, source);
3416            symbols.push(Symbol {
3417                name: node_text(source, &name_node).to_string(),
3418                kind: SymbolKind::Interface,
3419                range: node_range_with_decorators(&def_node, source, lang),
3420                signature: Some(extract_signature(source, &def_node)),
3421                scope_chain: scope_chain.clone(),
3422                exported: false,
3423                parent: scope_chain.last().cloned(),
3424            });
3425        }
3426
3427        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
3428            let scope_chain = csharp_scope_chain(&def_node, source);
3429            symbols.push(Symbol {
3430                name: node_text(source, &name_node).to_string(),
3431                kind: SymbolKind::Struct,
3432                range: node_range_with_decorators(&def_node, source, lang),
3433                signature: Some(extract_signature(source, &def_node)),
3434                scope_chain: scope_chain.clone(),
3435                exported: false,
3436                parent: scope_chain.last().cloned(),
3437            });
3438        }
3439
3440        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
3441            let scope_chain = csharp_scope_chain(&def_node, source);
3442            symbols.push(Symbol {
3443                name: node_text(source, &name_node).to_string(),
3444                kind: SymbolKind::Enum,
3445                range: node_range_with_decorators(&def_node, source, lang),
3446                signature: Some(extract_signature(source, &def_node)),
3447                scope_chain: scope_chain.clone(),
3448                exported: false,
3449                parent: scope_chain.last().cloned(),
3450            });
3451        }
3452
3453        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
3454            let scope_chain = csharp_scope_chain(&def_node, source);
3455            symbols.push(Symbol {
3456                name: node_text(source, &name_node).to_string(),
3457                kind: SymbolKind::Method,
3458                range: node_range_with_decorators(&def_node, source, lang),
3459                signature: Some(extract_signature(source, &def_node)),
3460                scope_chain: scope_chain.clone(),
3461                exported: false,
3462                parent: scope_chain.last().cloned(),
3463            });
3464        }
3465
3466        if let (Some(name_node), Some(def_node)) = (property_name_node, property_def_node) {
3467            let scope_chain = csharp_scope_chain(&def_node, source);
3468            symbols.push(Symbol {
3469                name: node_text(source, &name_node).to_string(),
3470                kind: SymbolKind::Variable,
3471                range: node_range_with_decorators(&def_node, source, lang),
3472                signature: Some(extract_signature(source, &def_node)),
3473                scope_chain: scope_chain.clone(),
3474                exported: false,
3475                parent: scope_chain.last().cloned(),
3476            });
3477        }
3478
3479        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
3480            let scope_chain = csharp_scope_chain(&def_node, source);
3481            symbols.push(Symbol {
3482                name: node_text(source, &name_node).to_string(),
3483                kind: SymbolKind::TypeAlias,
3484                range: node_range_with_decorators(&def_node, source, lang),
3485                signature: Some(extract_signature(source, &def_node)),
3486                scope_chain: scope_chain.clone(),
3487                exported: false,
3488                parent: scope_chain.last().cloned(),
3489            });
3490        }
3491    }
3492
3493    dedup_symbols(&mut symbols);
3494    Ok(symbols)
3495}
3496
3497/// Recursively find the first type_identifier node in a subtree.
3498fn find_type_identifier_recursive(node: &Node, source: &str) -> Option<String> {
3499    if node.kind() == "type_identifier" {
3500        return Some(node_text(source, node).to_string());
3501    }
3502    let mut cursor = node.walk();
3503    if cursor.goto_first_child() {
3504        loop {
3505            if let Some(result) = find_type_identifier_recursive(&cursor.node(), source) {
3506                return Some(result);
3507            }
3508            if !cursor.goto_next_sibling() {
3509                break;
3510            }
3511        }
3512    }
3513    None
3514}
3515
3516/// Extract HTML headings (h1-h6) as symbols.
3517/// Each heading becomes a symbol with kind `Heading`, and its range covers
3518/// the element itself. Headings are nested based on their level.
3519fn extract_bash_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
3520    let lang = LangId::Bash;
3521    let capture_names = query.capture_names();
3522
3523    let mut symbols = Vec::new();
3524    let mut cursor = QueryCursor::new();
3525    let mut matches = cursor.matches(query, *root, source.as_bytes());
3526
3527    while let Some(m) = {
3528        matches.advance();
3529        matches.get()
3530    } {
3531        let mut fn_name_node = None;
3532        let mut fn_def_node = None;
3533
3534        for cap in m.captures {
3535            let Some(&name) = capture_names.get(cap.index as usize) else {
3536                continue;
3537            };
3538            match name {
3539                "fn.name" => fn_name_node = Some(cap.node),
3540                "fn.def" => fn_def_node = Some(cap.node),
3541                _ => {}
3542            }
3543        }
3544
3545        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
3546            symbols.push(Symbol {
3547                name: node_text(source, &name_node).to_string(),
3548                kind: SymbolKind::Function,
3549                range: node_range_with_decorators(&def_node, source, lang),
3550                signature: Some(extract_signature(source, &def_node)),
3551                scope_chain: vec![],
3552                exported: false,
3553                parent: None,
3554            });
3555        }
3556    }
3557
3558    Ok(symbols)
3559}
3560
3561/// Walk up from `node` and collect the names of any enclosing
3562/// contract / library / interface, outermost first.
3563fn solidity_scope_chain(node: &Node, source: &str) -> Vec<String> {
3564    let mut chain = Vec::new();
3565    let mut current = node.parent();
3566
3567    while let Some(parent) = current {
3568        match parent.kind() {
3569            "contract_declaration" | "library_declaration" | "interface_declaration" => {
3570                if let Some(name_node) = parent.child_by_field_name("name") {
3571                    chain.push(node_text(source, &name_node).to_string());
3572                }
3573            }
3574            _ => {}
3575        }
3576        current = parent.parent();
3577    }
3578
3579    chain.reverse();
3580    chain
3581}
3582
3583fn extract_solidity_symbols(
3584    source: &str,
3585    root: &Node,
3586    query: &Query,
3587) -> Result<Vec<Symbol>, AftError> {
3588    let lang = LangId::Solidity;
3589    let capture_names = query.capture_names();
3590
3591    let mut symbols = Vec::new();
3592    let mut cursor = QueryCursor::new();
3593    let mut matches = cursor.matches(query, *root, source.as_bytes());
3594
3595    while let Some(m) = {
3596        matches.advance();
3597        matches.get()
3598    } {
3599        let mut contract_name_node = None;
3600        let mut contract_def_node = None;
3601        let mut library_name_node = None;
3602        let mut library_def_node = None;
3603        let mut interface_name_node = None;
3604        let mut interface_def_node = None;
3605        let mut fn_name_node = None;
3606        let mut fn_def_node = None;
3607        let mut modifier_name_node = None;
3608        let mut modifier_def_node = None;
3609        let mut constructor_def_node = None;
3610        let mut fallback_receive_def_node = None;
3611        let mut event_name_node = None;
3612        let mut event_def_node = None;
3613        let mut error_name_node = None;
3614        let mut error_def_node = None;
3615        let mut struct_name_node = None;
3616        let mut struct_def_node = None;
3617        let mut enum_name_node = None;
3618        let mut enum_def_node = None;
3619        let mut var_name_node = None;
3620        let mut var_def_node = None;
3621
3622        for cap in m.captures {
3623            let Some(&name) = capture_names.get(cap.index as usize) else {
3624                continue;
3625            };
3626            match name {
3627                "contract.name" => contract_name_node = Some(cap.node),
3628                "contract.def" => contract_def_node = Some(cap.node),
3629                "library.name" => library_name_node = Some(cap.node),
3630                "library.def" => library_def_node = Some(cap.node),
3631                "interface.name" => interface_name_node = Some(cap.node),
3632                "interface.def" => interface_def_node = Some(cap.node),
3633                "fn.name" => fn_name_node = Some(cap.node),
3634                "fn.def" => fn_def_node = Some(cap.node),
3635                "modifier.name" => modifier_name_node = Some(cap.node),
3636                "modifier.def" => modifier_def_node = Some(cap.node),
3637                "constructor.def" => constructor_def_node = Some(cap.node),
3638                "fallback_receive.def" => fallback_receive_def_node = Some(cap.node),
3639                "event.name" => event_name_node = Some(cap.node),
3640                "event.def" => event_def_node = Some(cap.node),
3641                "error.name" => error_name_node = Some(cap.node),
3642                "error.def" => error_def_node = Some(cap.node),
3643                "struct.name" => struct_name_node = Some(cap.node),
3644                "struct.def" => struct_def_node = Some(cap.node),
3645                "enum.name" => enum_name_node = Some(cap.node),
3646                "enum.def" => enum_def_node = Some(cap.node),
3647                "var.name" => var_name_node = Some(cap.node),
3648                "var.def" => var_def_node = Some(cap.node),
3649                _ => {}
3650            }
3651        }
3652
3653        // Contract
3654        if let (Some(name_node), Some(def_node)) = (contract_name_node, contract_def_node) {
3655            symbols.push(Symbol {
3656                name: node_text(source, &name_node).to_string(),
3657                kind: SymbolKind::Class,
3658                range: node_range_with_decorators(&def_node, source, lang),
3659                signature: Some(extract_signature(source, &def_node)),
3660                scope_chain: vec![],
3661                exported: true,
3662                parent: None,
3663            });
3664        }
3665
3666        // Library (treated like a contract — class-shaped container)
3667        if let (Some(name_node), Some(def_node)) = (library_name_node, library_def_node) {
3668            symbols.push(Symbol {
3669                name: node_text(source, &name_node).to_string(),
3670                kind: SymbolKind::Class,
3671                range: node_range_with_decorators(&def_node, source, lang),
3672                signature: Some(extract_signature(source, &def_node)),
3673                scope_chain: vec![],
3674                exported: true,
3675                parent: None,
3676            });
3677        }
3678
3679        // Interface
3680        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
3681            symbols.push(Symbol {
3682                name: node_text(source, &name_node).to_string(),
3683                kind: SymbolKind::Interface,
3684                range: node_range_with_decorators(&def_node, source, lang),
3685                signature: Some(extract_signature(source, &def_node)),
3686                scope_chain: vec![],
3687                exported: true,
3688                parent: None,
3689            });
3690        }
3691
3692        // Function — Method when inside a contract/library/interface, Function otherwise
3693        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
3694            let scope_chain = solidity_scope_chain(&def_node, source);
3695            let kind = if scope_chain.is_empty() {
3696                SymbolKind::Function
3697            } else {
3698                SymbolKind::Method
3699            };
3700            symbols.push(Symbol {
3701                name: node_text(source, &name_node).to_string(),
3702                kind,
3703                range: node_range_with_decorators(&def_node, source, lang),
3704                signature: Some(extract_signature(source, &def_node)),
3705                parent: scope_chain.last().cloned(),
3706                scope_chain,
3707                exported: true,
3708            });
3709        }
3710
3711        // Modifier — always inside a contract/library/interface, treat as Method
3712        if let (Some(name_node), Some(def_node)) = (modifier_name_node, modifier_def_node) {
3713            let scope_chain = solidity_scope_chain(&def_node, source);
3714            symbols.push(Symbol {
3715                name: node_text(source, &name_node).to_string(),
3716                kind: SymbolKind::Method,
3717                range: node_range_with_decorators(&def_node, source, lang),
3718                signature: Some(extract_signature(source, &def_node)),
3719                parent: scope_chain.last().cloned(),
3720                scope_chain,
3721                exported: true,
3722            });
3723        }
3724
3725        // Constructor — synthetic name "constructor", parent is the enclosing contract
3726        if let Some(def_node) = constructor_def_node {
3727            let scope_chain = solidity_scope_chain(&def_node, source);
3728            symbols.push(Symbol {
3729                name: "constructor".to_string(),
3730                kind: SymbolKind::Method,
3731                range: node_range_with_decorators(&def_node, source, lang),
3732                signature: Some(extract_signature(source, &def_node)),
3733                parent: scope_chain.last().cloned(),
3734                scope_chain,
3735                exported: true,
3736            });
3737        }
3738
3739        // receive() / fallback() — synthetic names, parent is the enclosing contract
3740        if let Some(def_node) = fallback_receive_def_node {
3741            let scope_chain = solidity_scope_chain(&def_node, source);
3742            let signature = extract_signature(source, &def_node);
3743            let name = if signature.trim_start().starts_with("receive") {
3744                "receive"
3745            } else {
3746                "fallback"
3747            };
3748            symbols.push(Symbol {
3749                name: name.to_string(),
3750                kind: SymbolKind::Method,
3751                range: node_range_with_decorators(&def_node, source, lang),
3752                signature: Some(signature),
3753                parent: scope_chain.last().cloned(),
3754                scope_chain,
3755                exported: true,
3756            });
3757        }
3758
3759        // Event
3760        if let (Some(name_node), Some(def_node)) = (event_name_node, event_def_node) {
3761            let scope_chain = solidity_scope_chain(&def_node, source);
3762            symbols.push(Symbol {
3763                name: node_text(source, &name_node).to_string(),
3764                kind: SymbolKind::Function,
3765                range: node_range_with_decorators(&def_node, source, lang),
3766                signature: Some(extract_signature(source, &def_node)),
3767                parent: scope_chain.last().cloned(),
3768                scope_chain,
3769                exported: true,
3770            });
3771        }
3772
3773        // Error (custom error declaration)
3774        if let (Some(name_node), Some(def_node)) = (error_name_node, error_def_node) {
3775            let scope_chain = solidity_scope_chain(&def_node, source);
3776            symbols.push(Symbol {
3777                name: node_text(source, &name_node).to_string(),
3778                kind: SymbolKind::TypeAlias,
3779                range: node_range_with_decorators(&def_node, source, lang),
3780                signature: Some(extract_signature(source, &def_node)),
3781                parent: scope_chain.last().cloned(),
3782                scope_chain,
3783                exported: true,
3784            });
3785        }
3786
3787        // Struct
3788        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
3789            let scope_chain = solidity_scope_chain(&def_node, source);
3790            symbols.push(Symbol {
3791                name: node_text(source, &name_node).to_string(),
3792                kind: SymbolKind::Struct,
3793                range: node_range_with_decorators(&def_node, source, lang),
3794                signature: Some(extract_signature(source, &def_node)),
3795                parent: scope_chain.last().cloned(),
3796                scope_chain,
3797                exported: true,
3798            });
3799        }
3800
3801        // Enum
3802        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
3803            let scope_chain = solidity_scope_chain(&def_node, source);
3804            symbols.push(Symbol {
3805                name: node_text(source, &name_node).to_string(),
3806                kind: SymbolKind::Enum,
3807                range: node_range_with_decorators(&def_node, source, lang),
3808                signature: Some(extract_signature(source, &def_node)),
3809                parent: scope_chain.last().cloned(),
3810                scope_chain,
3811                exported: true,
3812            });
3813        }
3814
3815        // State variable
3816        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
3817            let scope_chain = solidity_scope_chain(&def_node, source);
3818            symbols.push(Symbol {
3819                name: node_text(source, &name_node).to_string(),
3820                kind: SymbolKind::Variable,
3821                range: node_range_with_decorators(&def_node, source, lang),
3822                signature: Some(extract_signature(source, &def_node)),
3823                parent: scope_chain.last().cloned(),
3824                scope_chain,
3825                exported: true,
3826            });
3827        }
3828    }
3829
3830    dedup_symbols(&mut symbols);
3831    Ok(symbols)
3832}
3833
3834fn extract_json_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
3835    let Some(value) = root.named_child(0) else {
3836        return Ok(Vec::new());
3837    };
3838
3839    if value.kind() != "object" {
3840        return Ok(Vec::new());
3841    }
3842
3843    let mut symbols = Vec::new();
3844    let mut cursor = value.walk();
3845    for child in value.named_children(&mut cursor) {
3846        if child.kind() != "pair" {
3847            continue;
3848        }
3849        let Some(key_node) = child.child_by_field_name("key") else {
3850            continue;
3851        };
3852        let name = node_text(source, &key_node).trim_matches('"').to_string();
3853        if name.is_empty() {
3854            continue;
3855        }
3856        symbols.push(Symbol {
3857            name,
3858            kind: SymbolKind::Variable,
3859            range: node_range_with_decorators(&child, source, LangId::Json),
3860            signature: None,
3861            scope_chain: vec![],
3862            exported: false,
3863            parent: None,
3864        });
3865    }
3866
3867    Ok(symbols)
3868}
3869
3870/// Read a YAML scalar/key node as trimmed text, stripping surrounding quotes.
3871fn yaml_scalar_text(source: &str, node: &Node) -> String {
3872    node_text(source, node)
3873        .trim()
3874        .trim_matches('"')
3875        .trim_matches('\'')
3876        .to_string()
3877}
3878
3879/// Find the mapping that is the direct payload of `node`, descending only
3880/// through wrapper nodes (`document`, `block_node`, `flow_node`) rather than
3881/// arbitrary nested mappings. This keeps detection anchored to the document's
3882/// top-level mapping instead of latching onto a mapping buried inside a
3883/// sequence or nested value.
3884fn yaml_find_mapping<'a>(node: &Node<'a>) -> Option<Node<'a>> {
3885    let mut current = *node;
3886    loop {
3887        match current.kind() {
3888            "block_mapping" | "flow_mapping" => return Some(current),
3889            "document" | "block_node" | "flow_node" => {
3890                let mut cursor = current.walk();
3891                let next = current
3892                    .named_children(&mut cursor)
3893                    .find(|child| !matches!(child.kind(), "tag" | "anchor"));
3894                match next {
3895                    Some(inner) => current = inner,
3896                    None => return None,
3897                }
3898            }
3899            _ => return None,
3900        }
3901    }
3902}
3903
3904/// Look up a top-level key in a YAML mapping and return its value node.
3905fn yaml_get<'a>(mapping: &Node<'a>, source: &str, key: &str) -> Option<Node<'a>> {
3906    let mut cursor = mapping.walk();
3907    for pair in mapping.named_children(&mut cursor) {
3908        if pair.kind() != "block_mapping_pair" && pair.kind() != "flow_pair" {
3909            continue;
3910        }
3911        let Some(key_node) = pair.child_by_field_name("key") else {
3912            continue;
3913        };
3914        if yaml_scalar_text(source, &key_node) == key {
3915            return pair.child_by_field_name("value");
3916        }
3917    }
3918    None
3919}
3920
3921/// Flatten a YAML value node to clean text for embed_text. Scalars return their
3922/// trimmed text; block/flow sequences are joined as comma-separated scalar items
3923/// (so `verbs: [get, list, watch]` becomes `get,list,watch` instead of raw
3924/// multi-line `- get\n- list` text). Nested mappings are ignored here.
3925fn yaml_unwrap<'a>(node: &Node<'a>) -> Node<'a> {
3926    let mut current = *node;
3927    while current.kind() == "block_node" || current.kind() == "flow_node" {
3928        let mut cursor = current.walk();
3929        let next = current
3930            .named_children(&mut cursor)
3931            .find(|child| !matches!(child.kind(), "tag" | "anchor"));
3932        match next {
3933            Some(inner) => current = inner,
3934            None => break,
3935        }
3936    }
3937    current
3938}
3939
3940fn yaml_flatten_value(source: &str, node: &Node) -> String {
3941    let node = &yaml_unwrap(node);
3942    match node.kind() {
3943        "block_sequence" | "flow_sequence" => {
3944            let mut items = Vec::new();
3945            let mut cursor = node.walk();
3946            for child in node.named_children(&mut cursor) {
3947                // block_sequence_item wraps a value; flow_sequence holds nodes directly.
3948                let value = if child.kind() == "block_sequence_item" {
3949                    child.named_child(0)
3950                } else {
3951                    Some(child)
3952                };
3953                if let Some(v) = value {
3954                    let v = yaml_unwrap(&v);
3955                    // Only flatten scalar leaves; skip mappings (handled elsewhere).
3956                    if v.kind() != "block_mapping" && v.kind() != "flow_mapping" {
3957                        let text = yaml_scalar_text(source, &v);
3958                        if !text.is_empty() {
3959                            items.push(text);
3960                        }
3961                    }
3962                }
3963            }
3964            items.join(",")
3965        }
3966        // Mapping values (e.g. container `resources:` block) are not flattened
3967        // here — their high-signal leaves (cpu/memory) are collected separately.
3968        // Returning empty avoids dumping raw multi-line YAML into embed_text.
3969        "block_mapping" | "flow_mapping" => String::new(),
3970        _ => yaml_scalar_text(source, node),
3971    }
3972}
3973
3974/// Recursively collect `key=value` pairs for the given keys (e.g. image, cpu,
3975/// memory, verbs) to enrich embed_text for semantic search. Sequence values are
3976/// flattened to comma-joined scalars. Capped to avoid runaway output.
3977fn yaml_collect_values(
3978    source: &str,
3979    node: &Node,
3980    keys: &[&str],
3981    out: &mut Vec<String>,
3982    cap: usize,
3983) {
3984    if out.len() >= cap {
3985        return;
3986    }
3987    let mut cursor = node.walk();
3988    for child in node.named_children(&mut cursor) {
3989        if child.kind() == "block_mapping_pair" || child.kind() == "flow_pair" {
3990            if let Some(key_node) = child.child_by_field_name("key") {
3991                let key_text = yaml_scalar_text(source, &key_node);
3992                if keys.contains(&key_text.as_str()) {
3993                    if let Some(value_node) = child.child_by_field_name("value") {
3994                        let value_text = yaml_flatten_value(source, &value_node);
3995                        if !value_text.is_empty() && out.len() < cap {
3996                            out.push(format!("{}={}", key_text, value_text));
3997                        }
3998                    }
3999                }
4000            }
4001        }
4002        yaml_collect_values(source, &child, keys, out, cap);
4003    }
4004}
4005
4006/// Collect the `name:` field from every item of a `<parent_key>:` sequence of
4007/// mappings. The bare `name` key is too generic to match globally (it collides
4008/// with metadata/container names), so these are gathered by parent key and
4009/// emitted as `<label>=A,B,...`. Handles k8s `env: [{name,value}]` and Argo
4010/// Workflow `templates: [{name, ...}]`. Capped.
4011fn yaml_collect_named_items(
4012    source: &str,
4013    node: &Node,
4014    parent_key: &str,
4015    label: &str,
4016    out: &mut Vec<String>,
4017    cap: usize,
4018) {
4019    let mut cursor = node.walk();
4020    for child in node.named_children(&mut cursor) {
4021        if (child.kind() == "block_mapping_pair" || child.kind() == "flow_pair")
4022            && child
4023                .child_by_field_name("key")
4024                .map(|k| yaml_scalar_text(source, &k))
4025                .as_deref()
4026                == Some(parent_key)
4027        {
4028            if let Some(seq) = child.child_by_field_name("value") {
4029                let seq = yaml_unwrap(&seq);
4030                let mut names = Vec::new();
4031                let mut seq_cursor = seq.walk();
4032                for item in seq.named_children(&mut seq_cursor) {
4033                    let value = if item.kind() == "block_sequence_item" {
4034                        item.named_child(0)
4035                    } else {
4036                        Some(item)
4037                    };
4038                    if let Some(v) = value {
4039                        if let Some(map) = yaml_find_mapping(&v) {
4040                            if let Some(name_node) = yaml_get(&map, source, "name") {
4041                                let n = yaml_scalar_text(source, &name_node);
4042                                if !n.is_empty() && names.len() < 16 {
4043                                    names.push(n);
4044                                }
4045                            }
4046                        }
4047                    }
4048                }
4049                if !names.is_empty() && out.len() < cap {
4050                    out.push(format!("{}={}", label, names.join(",")));
4051                }
4052            }
4053        }
4054        yaml_collect_named_items(source, &child, parent_key, label, out, cap);
4055    }
4056}
4057
4058/// Tier 1: if a document is a Kubernetes resource (has both apiVersion + kind),
4059/// emit one rich symbol named `<ns>/<Kind>/<name>` with enriched signature.
4060/// Generalizes to arbitrary CRDs since apiVersion+kind is the CRD contract.
4061fn yaml_k8s_resource_symbol(source: &str, doc: &Node, mapping: &Node) -> Option<Symbol> {
4062    let api_version =
4063        yaml_get(mapping, source, "apiVersion").map(|n| yaml_scalar_text(source, &n))?;
4064    let kind = yaml_get(mapping, source, "kind").map(|n| yaml_scalar_text(source, &n))?;
4065    if api_version.is_empty() || kind.is_empty() {
4066        return None;
4067    }
4068
4069    let (name, namespace) = match yaml_get(mapping, source, "metadata") {
4070        Some(meta) => match yaml_find_mapping(&meta) {
4071            Some(meta_map) => {
4072                // Prefer `name`; fall back to `generateName` (common in Argo
4073                // Workflows submitted without a fixed name) so the symbol still
4074                // carries an identifier instead of collapsing to bare <Kind>.
4075                let name = yaml_get(&meta_map, source, "name")
4076                    .map(|n| yaml_scalar_text(source, &n))
4077                    .filter(|s| !s.is_empty())
4078                    .or_else(|| {
4079                        yaml_get(&meta_map, source, "generateName")
4080                            .map(|n| yaml_scalar_text(source, &n))
4081                            .filter(|s| !s.is_empty())
4082                    });
4083                (
4084                    name,
4085                    yaml_get(&meta_map, source, "namespace").map(|n| yaml_scalar_text(source, &n)),
4086                )
4087            }
4088            None => (None, None),
4089        },
4090        None => (None, None),
4091    };
4092
4093    let res_name = name.clone().filter(|s| !s.is_empty());
4094    let sym_name = match (&namespace, &res_name) {
4095        (Some(ns), Some(n)) if !ns.is_empty() => format!("{}/{}/{}", ns, kind, n),
4096        (_, Some(n)) => format!("{}/{}", kind, n),
4097        _ => kind.clone(),
4098    };
4099
4100    let mut sig = format!("apiVersion={} kind={}", api_version, kind);
4101    if let Some(ns) = namespace.as_ref().filter(|s| !s.is_empty()) {
4102        sig.push_str(&format!(" namespace={}", ns));
4103    }
4104    if let Some(n) = res_name.as_ref() {
4105        sig.push_str(&format!(" name={}", n));
4106    }
4107    // Enrich with high-signal spec fields so intent queries match. Covers
4108    // containers (image/ports), resource limits/requests (cpu/memory), RBAC
4109    // rules (verbs/resources/apiGroups), and storage (volumeMounts mountPath).
4110    let mut extras = Vec::new();
4111    yaml_collect_values(
4112        source,
4113        mapping,
4114        &[
4115            "image",
4116            "containerPort",
4117            "port",
4118            "targetPort",
4119            "cpu",
4120            "memory",
4121            "storage",
4122            "verbs",
4123            "resources",
4124            "apiGroups",
4125            "mountPath",
4126            "replicas",
4127            // Argo Workflow high-signal scalars.
4128            "entrypoint",
4129            "command",
4130            "args",
4131            "schedule",
4132        ],
4133        &mut extras,
4134        24,
4135    );
4136    // Sequence-of-mappings whose `name` key is ambiguous: collect by parent key.
4137    // k8s env vars and Argo Workflow templates both use the `{name: ...}` shape.
4138    yaml_collect_named_items(source, mapping, "env", "env", &mut extras, 24);
4139    yaml_collect_named_items(source, mapping, "templates", "templates", &mut extras, 24);
4140    if !extras.is_empty() {
4141        sig.push(' ');
4142        sig.push_str(&extras.join(" "));
4143    }
4144
4145    Some(Symbol {
4146        name: sym_name,
4147        kind: SymbolKind::Class,
4148        range: node_range(doc),
4149        signature: Some(sig),
4150        scope_chain: vec![],
4151        exported: true,
4152        parent: None,
4153    })
4154}
4155
4156/// Tier 2: generic YAML — emit top-level mapping keys as Variable symbols
4157/// (docker-compose services, CI jobs, Helm values.yaml, etc.).
4158fn yaml_generic_keys(source: &str, mapping: &Node, symbols: &mut Vec<Symbol>) {
4159    let mut cursor = mapping.walk();
4160    for pair in mapping.named_children(&mut cursor) {
4161        if pair.kind() != "block_mapping_pair" && pair.kind() != "flow_pair" {
4162            continue;
4163        }
4164        let Some(key_node) = pair.child_by_field_name("key") else {
4165            continue;
4166        };
4167        let name = yaml_scalar_text(source, &key_node);
4168        if name.is_empty() {
4169            continue;
4170        }
4171        symbols.push(Symbol {
4172            name,
4173            kind: SymbolKind::Variable,
4174            range: node_range(&pair),
4175            signature: None,
4176            scope_chain: vec![],
4177            exported: false,
4178            parent: None,
4179        });
4180    }
4181}
4182
4183/// Extract symbols from a YAML stream. Handles multi-document (`---`) streams:
4184/// each document becomes a Kubernetes resource symbol (Tier 1) when it carries
4185/// apiVersion+kind, otherwise its top-level keys are emitted (Tier 2). Helm/Go
4186/// templated YAML that yields no parseable mapping degrades gracefully (the
4187/// document is skipped rather than failing the whole file).
4188fn extract_yaml_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
4189    let mut symbols = Vec::new();
4190    let mut cursor = root.walk();
4191    for doc in root.named_children(&mut cursor) {
4192        if doc.kind() != "document" {
4193            continue;
4194        }
4195        let Some(mapping) = yaml_find_mapping(&doc) else {
4196            continue;
4197        };
4198        if let Some(symbol) = yaml_k8s_resource_symbol(source, &doc, &mapping) {
4199            symbols.push(symbol);
4200        } else {
4201            yaml_generic_keys(source, &mapping, &mut symbols);
4202        }
4203    }
4204    Ok(symbols)
4205}
4206
4207fn scala_scope_chain(node: &Node, source: &str) -> Vec<String> {
4208    let mut chain = Vec::new();
4209    let mut current = node.parent();
4210
4211    while let Some(parent) = current {
4212        match parent.kind() {
4213            "class_definition" | "object_definition" | "enum_definition" | "trait_definition" => {
4214                if let Some(name_node) = parent.child_by_field_name("name") {
4215                    chain.push(node_text(source, &name_node).to_string());
4216                }
4217            }
4218            _ => {}
4219        }
4220        current = parent.parent();
4221    }
4222
4223    chain.reverse();
4224    chain
4225}
4226
4227fn extract_scala_symbols(
4228    source: &str,
4229    root: &Node,
4230    query: &Query,
4231) -> Result<Vec<Symbol>, AftError> {
4232    let lang = LangId::Scala;
4233    let capture_names = query.capture_names();
4234
4235    let mut symbols = Vec::new();
4236    let mut cursor = QueryCursor::new();
4237    let mut matches = cursor.matches(query, *root, source.as_bytes());
4238
4239    while let Some(m) = {
4240        matches.advance();
4241        matches.get()
4242    } {
4243        let mut class_name_node = None;
4244        let mut class_def_node = None;
4245        let mut object_name_node = None;
4246        let mut object_def_node = None;
4247        let mut enum_name_node = None;
4248        let mut enum_def_node = None;
4249        let mut trait_name_node = None;
4250        let mut trait_def_node = None;
4251        let mut fn_name_node = None;
4252        let mut fn_def_node = None;
4253        let mut val_name_node = None;
4254        let mut val_def_node = None;
4255        let mut var_name_node = None;
4256        let mut var_def_node = None;
4257        let mut type_name_node = None;
4258        let mut type_def_node = None;
4259
4260        for cap in m.captures {
4261            let Some(&name) = capture_names.get(cap.index as usize) else {
4262                continue;
4263            };
4264            match name {
4265                "class.name" => class_name_node = Some(cap.node),
4266                "class.def" => class_def_node = Some(cap.node),
4267                "object.name" => object_name_node = Some(cap.node),
4268                "object.def" => object_def_node = Some(cap.node),
4269                "enum.name" => enum_name_node = Some(cap.node),
4270                "enum.def" => enum_def_node = Some(cap.node),
4271                "trait.name" => trait_name_node = Some(cap.node),
4272                "trait.def" => trait_def_node = Some(cap.node),
4273                "fn.name" => fn_name_node = Some(cap.node),
4274                "fn.def" => fn_def_node = Some(cap.node),
4275                "val.name" => val_name_node = Some(cap.node),
4276                "val.def" => val_def_node = Some(cap.node),
4277                "var.name" => var_name_node = Some(cap.node),
4278                "var.def" => var_def_node = Some(cap.node),
4279                "given.name" => val_name_node = Some(cap.node),
4280                "given.def" => val_def_node = Some(cap.node),
4281                "type.name" => type_name_node = Some(cap.node),
4282                "type.def" => type_def_node = Some(cap.node),
4283                _ => {}
4284            }
4285        }
4286
4287        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
4288            symbols.push(Symbol {
4289                name: node_text(source, &name_node).to_string(),
4290                kind: SymbolKind::Class,
4291                range: node_range_with_decorators(&def_node, source, lang),
4292                signature: Some(extract_signature(source, &def_node)),
4293                scope_chain: scala_scope_chain(&def_node, source),
4294                exported: true,
4295                parent: scala_scope_chain(&def_node, source).last().cloned(),
4296            });
4297        }
4298
4299        if let (Some(name_node), Some(def_node)) = (object_name_node, object_def_node) {
4300            symbols.push(Symbol {
4301                name: node_text(source, &name_node).to_string(),
4302                kind: SymbolKind::Class,
4303                range: node_range_with_decorators(&def_node, source, lang),
4304                signature: Some(extract_signature(source, &def_node)),
4305                scope_chain: scala_scope_chain(&def_node, source),
4306                exported: true,
4307                parent: scala_scope_chain(&def_node, source).last().cloned(),
4308            });
4309        }
4310
4311        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
4312            symbols.push(Symbol {
4313                name: node_text(source, &name_node).to_string(),
4314                kind: SymbolKind::Enum,
4315                range: node_range_with_decorators(&def_node, source, lang),
4316                signature: Some(extract_signature(source, &def_node)),
4317                scope_chain: scala_scope_chain(&def_node, source),
4318                exported: true,
4319                parent: scala_scope_chain(&def_node, source).last().cloned(),
4320            });
4321        }
4322
4323        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
4324            symbols.push(Symbol {
4325                name: node_text(source, &name_node).to_string(),
4326                kind: SymbolKind::Interface,
4327                range: node_range_with_decorators(&def_node, source, lang),
4328                signature: Some(extract_signature(source, &def_node)),
4329                scope_chain: scala_scope_chain(&def_node, source),
4330                exported: true,
4331                parent: scala_scope_chain(&def_node, source).last().cloned(),
4332            });
4333        }
4334
4335        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
4336            let scope_chain = scala_scope_chain(&def_node, source);
4337            let kind = if scope_chain.is_empty() {
4338                SymbolKind::Function
4339            } else {
4340                SymbolKind::Method
4341            };
4342            symbols.push(Symbol {
4343                name: node_text(source, &name_node).to_string(),
4344                kind,
4345                range: node_range_with_decorators(&def_node, source, lang),
4346                signature: Some(extract_signature(source, &def_node)),
4347                parent: scope_chain.last().cloned(),
4348                scope_chain,
4349                exported: true,
4350            });
4351        }
4352
4353        if let (Some(name_node), Some(def_node)) = (val_name_node, val_def_node) {
4354            let scope_chain = scala_scope_chain(&def_node, source);
4355            symbols.push(Symbol {
4356                name: node_text(source, &name_node).to_string(),
4357                kind: SymbolKind::Variable,
4358                range: node_range_with_decorators(&def_node, source, lang),
4359                signature: Some(extract_signature(source, &def_node)),
4360                parent: scope_chain.last().cloned(),
4361                scope_chain,
4362                exported: true,
4363            });
4364        }
4365
4366        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
4367            let scope_chain = scala_scope_chain(&def_node, source);
4368            symbols.push(Symbol {
4369                name: node_text(source, &name_node).to_string(),
4370                kind: SymbolKind::Variable,
4371                range: node_range_with_decorators(&def_node, source, lang),
4372                signature: Some(extract_signature(source, &def_node)),
4373                parent: scope_chain.last().cloned(),
4374                scope_chain,
4375                exported: true,
4376            });
4377        }
4378
4379        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
4380            let scope_chain = scala_scope_chain(&def_node, source);
4381            symbols.push(Symbol {
4382                name: node_text(source, &name_node).to_string(),
4383                kind: SymbolKind::TypeAlias,
4384                range: node_range_with_decorators(&def_node, source, lang),
4385                signature: Some(extract_signature(source, &def_node)),
4386                parent: scope_chain.last().cloned(),
4387                scope_chain,
4388                exported: true,
4389            });
4390        }
4391    }
4392
4393    dedup_symbols(&mut symbols);
4394    Ok(symbols)
4395}
4396
4397fn child_text_by_field_or_kind(
4398    node: &Node,
4399    source: &str,
4400    field_name: &str,
4401    kinds: &[&str],
4402) -> Option<String> {
4403    if let Some(name_node) = node.child_by_field_name(field_name) {
4404        return Some(node_text(source, &name_node).to_string());
4405    }
4406
4407    let mut cursor = node.walk();
4408    if !cursor.goto_first_child() {
4409        return None;
4410    }
4411
4412    loop {
4413        let child = cursor.node();
4414        if kinds.contains(&child.kind()) {
4415            return Some(node_text(source, &child).to_string());
4416        }
4417        if !cursor.goto_next_sibling() {
4418            break;
4419        }
4420    }
4421
4422    None
4423}
4424
4425fn push_captured_symbol(
4426    symbols: &mut Vec<Symbol>,
4427    source: &str,
4428    lang: LangId,
4429    name_node: Node,
4430    def_node: Node,
4431    kind: SymbolKind,
4432    scope_chain: Vec<String>,
4433    exported: bool,
4434) {
4435    symbols.push(Symbol {
4436        name: node_text(source, &name_node).to_string(),
4437        kind,
4438        range: node_range_with_decorators(&def_node, source, lang),
4439        signature: Some(extract_signature(source, &def_node)),
4440        parent: scope_chain.last().cloned(),
4441        scope_chain,
4442        exported,
4443    });
4444}
4445
4446fn java_scope_chain(node: &Node, source: &str) -> Vec<String> {
4447    let mut chain = Vec::new();
4448    let mut current = node.parent();
4449
4450    while let Some(parent) = current {
4451        if matches!(
4452            parent.kind(),
4453            "class_declaration"
4454                | "interface_declaration"
4455                | "annotation_type_declaration"
4456                | "enum_declaration"
4457                | "record_declaration"
4458        ) {
4459            if let Some(name_node) = parent.child_by_field_name("name") {
4460                chain.push(node_text(source, &name_node).to_string());
4461            }
4462        }
4463        current = parent.parent();
4464    }
4465
4466    chain.reverse();
4467    chain
4468}
4469
4470fn extract_java_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
4471    let lang = LangId::Java;
4472    let capture_names = query.capture_names();
4473    let mut symbols = Vec::new();
4474    let mut cursor = QueryCursor::new();
4475    let mut matches = cursor.matches(query, *root, source.as_bytes());
4476
4477    while let Some(m) = {
4478        matches.advance();
4479        matches.get()
4480    } {
4481        let mut class_name_node = None;
4482        let mut class_def_node = None;
4483        let mut interface_name_node = None;
4484        let mut interface_def_node = None;
4485        let mut enum_name_node = None;
4486        let mut enum_def_node = None;
4487        let mut struct_name_node = None;
4488        let mut struct_def_node = None;
4489        let mut fn_name_node = None;
4490        let mut fn_def_node = None;
4491        let mut var_name_node = None;
4492        let mut var_def_node = None;
4493
4494        for cap in m.captures {
4495            let Some(&name) = capture_names.get(cap.index as usize) else {
4496                continue;
4497            };
4498            match name {
4499                "class.name" => class_name_node = Some(cap.node),
4500                "class.def" => class_def_node = Some(cap.node),
4501                "interface.name" => interface_name_node = Some(cap.node),
4502                "interface.def" => interface_def_node = Some(cap.node),
4503                "enum.name" => enum_name_node = Some(cap.node),
4504                "enum.def" => enum_def_node = Some(cap.node),
4505                "struct.name" => struct_name_node = Some(cap.node),
4506                "struct.def" => struct_def_node = Some(cap.node),
4507                "fn.name" => fn_name_node = Some(cap.node),
4508                "fn.def" => fn_def_node = Some(cap.node),
4509                "var.name" => var_name_node = Some(cap.node),
4510                "var.def" => var_def_node = Some(cap.node),
4511                _ => {}
4512            }
4513        }
4514
4515        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
4516            push_captured_symbol(
4517                &mut symbols,
4518                source,
4519                lang,
4520                name_node,
4521                def_node,
4522                SymbolKind::Class,
4523                java_scope_chain(&def_node, source),
4524                true,
4525            );
4526        }
4527        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
4528            push_captured_symbol(
4529                &mut symbols,
4530                source,
4531                lang,
4532                name_node,
4533                def_node,
4534                SymbolKind::Interface,
4535                java_scope_chain(&def_node, source),
4536                true,
4537            );
4538        }
4539        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
4540            push_captured_symbol(
4541                &mut symbols,
4542                source,
4543                lang,
4544                name_node,
4545                def_node,
4546                SymbolKind::Enum,
4547                java_scope_chain(&def_node, source),
4548                true,
4549            );
4550        }
4551        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
4552            push_captured_symbol(
4553                &mut symbols,
4554                source,
4555                lang,
4556                name_node,
4557                def_node,
4558                SymbolKind::Struct,
4559                java_scope_chain(&def_node, source),
4560                true,
4561            );
4562        }
4563        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
4564            let scope_chain = java_scope_chain(&def_node, source);
4565            let kind = if scope_chain.is_empty() {
4566                SymbolKind::Function
4567            } else {
4568                SymbolKind::Method
4569            };
4570            push_captured_symbol(
4571                &mut symbols,
4572                source,
4573                lang,
4574                name_node,
4575                def_node,
4576                kind,
4577                scope_chain,
4578                true,
4579            );
4580        }
4581        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
4582            push_captured_symbol(
4583                &mut symbols,
4584                source,
4585                lang,
4586                name_node,
4587                def_node,
4588                SymbolKind::Variable,
4589                java_scope_chain(&def_node, source),
4590                true,
4591            );
4592        }
4593    }
4594
4595    dedup_symbols(&mut symbols);
4596    Ok(symbols)
4597}
4598
4599fn ruby_scope_chain(node: &Node, source: &str) -> Vec<String> {
4600    let mut chain = Vec::new();
4601    let mut current = node.parent();
4602
4603    while let Some(parent) = current {
4604        if matches!(parent.kind(), "class" | "module") {
4605            if let Some(name_node) = parent.child_by_field_name("name") {
4606                chain.push(node_text(source, &name_node).to_string());
4607            }
4608        }
4609        current = parent.parent();
4610    }
4611
4612    chain.reverse();
4613    chain
4614}
4615
4616fn extract_ruby_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
4617    let lang = LangId::Ruby;
4618    let capture_names = query.capture_names();
4619    let mut symbols = Vec::new();
4620    let mut cursor = QueryCursor::new();
4621    let mut matches = cursor.matches(query, *root, source.as_bytes());
4622
4623    while let Some(m) = {
4624        matches.advance();
4625        matches.get()
4626    } {
4627        let mut module_name_node = None;
4628        let mut module_def_node = None;
4629        let mut class_name_node = None;
4630        let mut class_def_node = None;
4631        let mut fn_name_node = None;
4632        let mut fn_def_node = None;
4633        let mut var_name_node = None;
4634        let mut var_def_node = None;
4635
4636        for cap in m.captures {
4637            let Some(&name) = capture_names.get(cap.index as usize) else {
4638                continue;
4639            };
4640            match name {
4641                "module.name" => module_name_node = Some(cap.node),
4642                "module.def" => module_def_node = Some(cap.node),
4643                "class.name" => class_name_node = Some(cap.node),
4644                "class.def" => class_def_node = Some(cap.node),
4645                "fn.name" => fn_name_node = Some(cap.node),
4646                "fn.def" => fn_def_node = Some(cap.node),
4647                "var.name" => var_name_node = Some(cap.node),
4648                "var.def" => var_def_node = Some(cap.node),
4649                _ => {}
4650            }
4651        }
4652
4653        if let (Some(name_node), Some(def_node)) = (module_name_node, module_def_node) {
4654            push_captured_symbol(
4655                &mut symbols,
4656                source,
4657                lang,
4658                name_node,
4659                def_node,
4660                SymbolKind::Class,
4661                ruby_scope_chain(&def_node, source),
4662                true,
4663            );
4664        }
4665        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
4666            push_captured_symbol(
4667                &mut symbols,
4668                source,
4669                lang,
4670                name_node,
4671                def_node,
4672                SymbolKind::Class,
4673                ruby_scope_chain(&def_node, source),
4674                true,
4675            );
4676        }
4677        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
4678            let scope_chain = ruby_scope_chain(&def_node, source);
4679            let kind = if scope_chain.is_empty() {
4680                SymbolKind::Function
4681            } else {
4682                SymbolKind::Method
4683            };
4684            push_captured_symbol(
4685                &mut symbols,
4686                source,
4687                lang,
4688                name_node,
4689                def_node,
4690                kind,
4691                scope_chain,
4692                true,
4693            );
4694        }
4695        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
4696            push_captured_symbol(
4697                &mut symbols,
4698                source,
4699                lang,
4700                name_node,
4701                def_node,
4702                SymbolKind::Variable,
4703                ruby_scope_chain(&def_node, source),
4704                true,
4705            );
4706        }
4707    }
4708
4709    dedup_symbols(&mut symbols);
4710    Ok(symbols)
4711}
4712
4713fn kotlin_scope_chain(node: &Node, source: &str) -> Vec<String> {
4714    let mut chain = Vec::new();
4715    let mut current = node.parent();
4716
4717    while let Some(parent) = current {
4718        if matches!(parent.kind(), "class_declaration" | "object_declaration") {
4719            if let Some(name) =
4720                child_text_by_field_or_kind(&parent, source, "name", &["type_identifier"])
4721            {
4722                chain.push(name);
4723            }
4724        }
4725        current = parent.parent();
4726    }
4727
4728    chain.reverse();
4729    chain
4730}
4731
4732fn extract_kotlin_symbols(
4733    source: &str,
4734    root: &Node,
4735    query: &Query,
4736) -> Result<Vec<Symbol>, AftError> {
4737    let lang = LangId::Kotlin;
4738    let capture_names = query.capture_names();
4739    let mut symbols = Vec::new();
4740    let mut cursor = QueryCursor::new();
4741    let mut matches = cursor.matches(query, *root, source.as_bytes());
4742
4743    while let Some(m) = {
4744        matches.advance();
4745        matches.get()
4746    } {
4747        let mut class_name_node = None;
4748        let mut class_def_node = None;
4749        let mut object_name_node = None;
4750        let mut object_def_node = None;
4751        let mut fn_name_node = None;
4752        let mut fn_def_node = None;
4753        let mut var_name_node = None;
4754        let mut var_def_node = None;
4755        let mut type_name_node = None;
4756        let mut type_def_node = None;
4757
4758        for cap in m.captures {
4759            let Some(&name) = capture_names.get(cap.index as usize) else {
4760                continue;
4761            };
4762            match name {
4763                "class.name" => class_name_node = Some(cap.node),
4764                "class.def" => class_def_node = Some(cap.node),
4765                "object.name" => object_name_node = Some(cap.node),
4766                "object.def" => object_def_node = Some(cap.node),
4767                "fn.name" => fn_name_node = Some(cap.node),
4768                "fn.def" => fn_def_node = Some(cap.node),
4769                "var.name" => var_name_node = Some(cap.node),
4770                "var.def" => var_def_node = Some(cap.node),
4771                "type.name" => type_name_node = Some(cap.node),
4772                "type.def" => type_def_node = Some(cap.node),
4773                _ => {}
4774            }
4775        }
4776
4777        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
4778            push_captured_symbol(
4779                &mut symbols,
4780                source,
4781                lang,
4782                name_node,
4783                def_node,
4784                SymbolKind::Class,
4785                kotlin_scope_chain(&def_node, source),
4786                true,
4787            );
4788        }
4789        if let (Some(name_node), Some(def_node)) = (object_name_node, object_def_node) {
4790            push_captured_symbol(
4791                &mut symbols,
4792                source,
4793                lang,
4794                name_node,
4795                def_node,
4796                SymbolKind::Class,
4797                kotlin_scope_chain(&def_node, source),
4798                true,
4799            );
4800        }
4801        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
4802            let scope_chain = kotlin_scope_chain(&def_node, source);
4803            let kind = if scope_chain.is_empty() {
4804                SymbolKind::Function
4805            } else {
4806                SymbolKind::Method
4807            };
4808            push_captured_symbol(
4809                &mut symbols,
4810                source,
4811                lang,
4812                name_node,
4813                def_node,
4814                kind,
4815                scope_chain,
4816                true,
4817            );
4818        }
4819        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
4820            push_captured_symbol(
4821                &mut symbols,
4822                source,
4823                lang,
4824                name_node,
4825                def_node,
4826                SymbolKind::Variable,
4827                kotlin_scope_chain(&def_node, source),
4828                true,
4829            );
4830        }
4831        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
4832            push_captured_symbol(
4833                &mut symbols,
4834                source,
4835                lang,
4836                name_node,
4837                def_node,
4838                SymbolKind::TypeAlias,
4839                kotlin_scope_chain(&def_node, source),
4840                true,
4841            );
4842        }
4843    }
4844
4845    dedup_symbols(&mut symbols);
4846    Ok(symbols)
4847}
4848
4849fn swift_scope_chain(node: &Node, source: &str) -> Vec<String> {
4850    let mut chain = Vec::new();
4851    let mut current = node.parent();
4852
4853    while let Some(parent) = current {
4854        if matches!(parent.kind(), "class_declaration" | "protocol_declaration") {
4855            if let Some(name_node) = parent.child_by_field_name("name") {
4856                chain.push(node_text(source, &name_node).to_string());
4857            }
4858        }
4859        current = parent.parent();
4860    }
4861
4862    chain.reverse();
4863    chain
4864}
4865
4866fn swift_type_kind(source: &str, node: &Node) -> SymbolKind {
4867    match node
4868        .child_by_field_name("declaration_kind")
4869        .map(|kind_node| node_text(source, &kind_node))
4870    {
4871        Some("struct") => SymbolKind::Struct,
4872        Some("enum") => SymbolKind::Enum,
4873        _ => SymbolKind::Class,
4874    }
4875}
4876
4877fn extract_swift_symbols(
4878    source: &str,
4879    root: &Node,
4880    query: &Query,
4881) -> Result<Vec<Symbol>, AftError> {
4882    let lang = LangId::Swift;
4883    let capture_names = query.capture_names();
4884    let mut symbols = Vec::new();
4885    let mut cursor = QueryCursor::new();
4886    let mut matches = cursor.matches(query, *root, source.as_bytes());
4887
4888    while let Some(m) = {
4889        matches.advance();
4890        matches.get()
4891    } {
4892        let mut class_name_node = None;
4893        let mut class_def_node = None;
4894        let mut interface_name_node = None;
4895        let mut interface_def_node = None;
4896        let mut fn_name_node = None;
4897        let mut fn_def_node = None;
4898        let mut var_name_node = None;
4899        let mut var_def_node = None;
4900        let mut type_name_node = None;
4901        let mut type_def_node = None;
4902
4903        for cap in m.captures {
4904            let Some(&name) = capture_names.get(cap.index as usize) else {
4905                continue;
4906            };
4907            match name {
4908                "class.name" => class_name_node = Some(cap.node),
4909                "class.def" => class_def_node = Some(cap.node),
4910                "interface.name" => interface_name_node = Some(cap.node),
4911                "interface.def" => interface_def_node = Some(cap.node),
4912                "fn.name" => fn_name_node = Some(cap.node),
4913                "fn.def" => fn_def_node = Some(cap.node),
4914                "var.name" => var_name_node = Some(cap.node),
4915                "var.def" => var_def_node = Some(cap.node),
4916                "type.name" => type_name_node = Some(cap.node),
4917                "type.def" => type_def_node = Some(cap.node),
4918                _ => {}
4919            }
4920        }
4921
4922        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
4923            let kind = swift_type_kind(source, &def_node);
4924            push_captured_symbol(
4925                &mut symbols,
4926                source,
4927                lang,
4928                name_node,
4929                def_node,
4930                kind,
4931                swift_scope_chain(&def_node, source),
4932                true,
4933            );
4934        }
4935        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
4936            push_captured_symbol(
4937                &mut symbols,
4938                source,
4939                lang,
4940                name_node,
4941                def_node,
4942                SymbolKind::Interface,
4943                swift_scope_chain(&def_node, source),
4944                true,
4945            );
4946        }
4947        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
4948            let scope_chain = swift_scope_chain(&def_node, source);
4949            let kind = if scope_chain.is_empty() {
4950                SymbolKind::Function
4951            } else {
4952                SymbolKind::Method
4953            };
4954            push_captured_symbol(
4955                &mut symbols,
4956                source,
4957                lang,
4958                name_node,
4959                def_node,
4960                kind,
4961                scope_chain,
4962                true,
4963            );
4964        }
4965        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
4966            push_captured_symbol(
4967                &mut symbols,
4968                source,
4969                lang,
4970                name_node,
4971                def_node,
4972                SymbolKind::Variable,
4973                swift_scope_chain(&def_node, source),
4974                true,
4975            );
4976        }
4977        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
4978            push_captured_symbol(
4979                &mut symbols,
4980                source,
4981                lang,
4982                name_node,
4983                def_node,
4984                SymbolKind::TypeAlias,
4985                swift_scope_chain(&def_node, source),
4986                true,
4987            );
4988        }
4989    }
4990
4991    dedup_symbols(&mut symbols);
4992    Ok(symbols)
4993}
4994
4995fn php_scope_chain(node: &Node, source: &str) -> Vec<String> {
4996    let mut chain = Vec::new();
4997    let mut current = node.parent();
4998
4999    while let Some(parent) = current {
5000        match parent.kind() {
5001            "namespace_definition"
5002            | "class_declaration"
5003            | "interface_declaration"
5004            | "trait_declaration"
5005            | "enum_declaration" => {
5006                if let Some(name_node) = parent.child_by_field_name("name") {
5007                    chain.push(node_text(source, &name_node).to_string());
5008                }
5009            }
5010            _ => {}
5011        }
5012        current = parent.parent();
5013    }
5014
5015    chain.reverse();
5016    chain
5017}
5018
5019fn extract_scss_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
5020    let lang = LangId::Scss;
5021    let capture_names = query.capture_names();
5022    let mut symbols = Vec::new();
5023    let mut cursor = QueryCursor::new();
5024    let mut matches = cursor.matches(query, *root, source.as_bytes());
5025
5026    while let Some(m) = {
5027        matches.advance();
5028        matches.get()
5029    } {
5030        let mut mixin_name_node = None;
5031        let mut mixin_def_node = None;
5032        let mut fn_name_node = None;
5033        let mut fn_def_node = None;
5034        let mut var_name_node = None;
5035        let mut var_def_node = None;
5036        let mut selector_name_node = None;
5037        let mut selector_def_node = None;
5038
5039        for cap in m.captures {
5040            let Some(&name) = capture_names.get(cap.index as usize) else {
5041                continue;
5042            };
5043            match name {
5044                "mixin.name" => mixin_name_node = Some(cap.node),
5045                "mixin.def" => mixin_def_node = Some(cap.node),
5046                "fn.name" => fn_name_node = Some(cap.node),
5047                "fn.def" => fn_def_node = Some(cap.node),
5048                "var.name" => var_name_node = Some(cap.node),
5049                "var.def" => var_def_node = Some(cap.node),
5050                "selector.name" => selector_name_node = Some(cap.node),
5051                "selector.def" => selector_def_node = Some(cap.node),
5052                _ => {}
5053            }
5054        }
5055
5056        if let (Some(name_node), Some(def_node)) = (mixin_name_node, mixin_def_node) {
5057            push_captured_symbol(
5058                &mut symbols,
5059                source,
5060                lang,
5061                name_node,
5062                def_node,
5063                SymbolKind::Function,
5064                vec![],
5065                true,
5066            );
5067        }
5068        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
5069            push_captured_symbol(
5070                &mut symbols,
5071                source,
5072                lang,
5073                name_node,
5074                def_node,
5075                SymbolKind::Function,
5076                vec![],
5077                true,
5078            );
5079        }
5080        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
5081            if !node_text(source, &name_node).starts_with('$') {
5082                continue;
5083            }
5084            push_captured_symbol(
5085                &mut symbols,
5086                source,
5087                lang,
5088                name_node,
5089                def_node,
5090                SymbolKind::Variable,
5091                vec![],
5092                true,
5093            );
5094        }
5095        if let (Some(name_node), Some(def_node)) = (selector_name_node, selector_def_node) {
5096            push_captured_symbol(
5097                &mut symbols,
5098                source,
5099                lang,
5100                name_node,
5101                def_node,
5102                SymbolKind::Class,
5103                vec![],
5104                true,
5105            );
5106        }
5107    }
5108
5109    Ok(symbols)
5110}
5111
5112fn extract_php_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
5113    let lang = LangId::Php;
5114    let capture_names = query.capture_names();
5115    let mut symbols = Vec::new();
5116    let mut cursor = QueryCursor::new();
5117    let mut matches = cursor.matches(query, *root, source.as_bytes());
5118
5119    while let Some(m) = {
5120        matches.advance();
5121        matches.get()
5122    } {
5123        let mut namespace_name_node = None;
5124        let mut namespace_def_node = None;
5125        let mut class_name_node = None;
5126        let mut class_def_node = None;
5127        let mut interface_name_node = None;
5128        let mut interface_def_node = None;
5129        let mut trait_name_node = None;
5130        let mut trait_def_node = None;
5131        let mut enum_name_node = None;
5132        let mut enum_def_node = None;
5133        let mut fn_name_node = None;
5134        let mut fn_def_node = None;
5135        let mut var_name_node = None;
5136        let mut var_def_node = None;
5137
5138        for cap in m.captures {
5139            let Some(&name) = capture_names.get(cap.index as usize) else {
5140                continue;
5141            };
5142            match name {
5143                "namespace.name" => namespace_name_node = Some(cap.node),
5144                "namespace.def" => namespace_def_node = Some(cap.node),
5145                "class.name" => class_name_node = Some(cap.node),
5146                "class.def" => class_def_node = Some(cap.node),
5147                "interface.name" => interface_name_node = Some(cap.node),
5148                "interface.def" => interface_def_node = Some(cap.node),
5149                "trait.name" => trait_name_node = Some(cap.node),
5150                "trait.def" => trait_def_node = Some(cap.node),
5151                "enum.name" => enum_name_node = Some(cap.node),
5152                "enum.def" => enum_def_node = Some(cap.node),
5153                "fn.name" => fn_name_node = Some(cap.node),
5154                "fn.def" => fn_def_node = Some(cap.node),
5155                "var.name" => var_name_node = Some(cap.node),
5156                "var.def" => var_def_node = Some(cap.node),
5157                _ => {}
5158            }
5159        }
5160
5161        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
5162            push_captured_symbol(
5163                &mut symbols,
5164                source,
5165                lang,
5166                name_node,
5167                def_node,
5168                SymbolKind::Class,
5169                php_scope_chain(&def_node, source),
5170                true,
5171            );
5172        }
5173        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
5174            push_captured_symbol(
5175                &mut symbols,
5176                source,
5177                lang,
5178                name_node,
5179                def_node,
5180                SymbolKind::Class,
5181                php_scope_chain(&def_node, source),
5182                true,
5183            );
5184        }
5185        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
5186            push_captured_symbol(
5187                &mut symbols,
5188                source,
5189                lang,
5190                name_node,
5191                def_node,
5192                SymbolKind::Interface,
5193                php_scope_chain(&def_node, source),
5194                true,
5195            );
5196        }
5197        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
5198            push_captured_symbol(
5199                &mut symbols,
5200                source,
5201                lang,
5202                name_node,
5203                def_node,
5204                SymbolKind::Interface,
5205                php_scope_chain(&def_node, source),
5206                true,
5207            );
5208        }
5209        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
5210            push_captured_symbol(
5211                &mut symbols,
5212                source,
5213                lang,
5214                name_node,
5215                def_node,
5216                SymbolKind::Enum,
5217                php_scope_chain(&def_node, source),
5218                true,
5219            );
5220        }
5221        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
5222            let scope_chain = php_scope_chain(&def_node, source);
5223            let kind = if scope_chain.is_empty() || def_node.kind() == "function_definition" {
5224                SymbolKind::Function
5225            } else {
5226                SymbolKind::Method
5227            };
5228            push_captured_symbol(
5229                &mut symbols,
5230                source,
5231                lang,
5232                name_node,
5233                def_node,
5234                kind,
5235                scope_chain,
5236                true,
5237            );
5238        }
5239        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
5240            push_captured_symbol(
5241                &mut symbols,
5242                source,
5243                lang,
5244                name_node,
5245                def_node,
5246                SymbolKind::Variable,
5247                php_scope_chain(&def_node, source),
5248                true,
5249            );
5250        }
5251    }
5252
5253    dedup_symbols(&mut symbols);
5254    Ok(symbols)
5255}
5256
5257fn lua_scope_chain(node: &Node, source: &str) -> Vec<String> {
5258    let mut chain = Vec::new();
5259
5260    if node.kind() == "function_declaration" {
5261        if let Some(name_node) = node.child_by_field_name("name") {
5262            match name_node.kind() {
5263                "dot_index_expression" | "method_index_expression" => {
5264                    if let Some(table_node) = name_node.child_by_field_name("table") {
5265                        chain.push(node_text(source, &table_node).to_string());
5266                    }
5267                }
5268                _ => {}
5269            }
5270        }
5271    }
5272
5273    chain
5274}
5275
5276fn extract_lua_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
5277    let lang = LangId::Lua;
5278    let capture_names = query.capture_names();
5279    let mut symbols = Vec::new();
5280    let mut cursor = QueryCursor::new();
5281    let mut matches = cursor.matches(query, *root, source.as_bytes());
5282
5283    while let Some(m) = {
5284        matches.advance();
5285        matches.get()
5286    } {
5287        let mut fn_name_node = None;
5288        let mut fn_def_node = None;
5289        let mut var_name_node = None;
5290        let mut var_def_node = None;
5291
5292        for cap in m.captures {
5293            let Some(&name) = capture_names.get(cap.index as usize) else {
5294                continue;
5295            };
5296            match name {
5297                "fn.name" => fn_name_node = Some(cap.node),
5298                "fn.def" => fn_def_node = Some(cap.node),
5299                "var.name" => var_name_node = Some(cap.node),
5300                "var.def" => var_def_node = Some(cap.node),
5301                _ => {}
5302            }
5303        }
5304
5305        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
5306            let scope_chain = lua_scope_chain(&def_node, source);
5307            let kind = if scope_chain.is_empty() {
5308                SymbolKind::Function
5309            } else {
5310                SymbolKind::Method
5311            };
5312            push_captured_symbol(
5313                &mut symbols,
5314                source,
5315                lang,
5316                name_node,
5317                def_node,
5318                kind,
5319                scope_chain,
5320                true,
5321            );
5322        }
5323        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
5324            push_captured_symbol(
5325                &mut symbols,
5326                source,
5327                lang,
5328                name_node,
5329                def_node,
5330                SymbolKind::Variable,
5331                vec![],
5332                true,
5333            );
5334        }
5335    }
5336
5337    dedup_symbols(&mut symbols);
5338    Ok(symbols)
5339}
5340
5341fn perl_package_name(source: &str, node: &Node) -> Option<String> {
5342    let mut cursor = node.walk();
5343    if !cursor.goto_first_child() {
5344        return None;
5345    }
5346
5347    loop {
5348        let child = cursor.node();
5349        if child.kind() == "package_name" {
5350            return Some(node_text(source, &child).to_string());
5351        }
5352        if !cursor.goto_next_sibling() {
5353            break;
5354        }
5355    }
5356
5357    None
5358}
5359
5360fn perl_scope_chain(node: &Node, source: &str) -> Vec<String> {
5361    let mut chain = Vec::new();
5362    let mut current = node.parent();
5363
5364    while let Some(parent) = current {
5365        if parent.kind() == "package_statement" {
5366            if let Some(name) = perl_package_name(source, &parent) {
5367                chain.push(name);
5368            }
5369        }
5370        current = parent.parent();
5371    }
5372
5373    if chain.is_empty() && node.kind() != "package_statement" {
5374        let mut sibling = node.prev_sibling();
5375        while let Some(prev) = sibling {
5376            if prev.kind() == "package_statement" {
5377                if let Some(name) = perl_package_name(source, &prev) {
5378                    chain.push(name);
5379                }
5380                break;
5381            }
5382            sibling = prev.prev_sibling();
5383        }
5384    }
5385
5386    chain.reverse();
5387    chain
5388}
5389
5390fn extract_perl_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
5391    let lang = LangId::Perl;
5392    let capture_names = query.capture_names();
5393    let mut symbols = Vec::new();
5394    let mut cursor = QueryCursor::new();
5395    let mut matches = cursor.matches(query, *root, source.as_bytes());
5396
5397    while let Some(m) = {
5398        matches.advance();
5399        matches.get()
5400    } {
5401        let mut package_name_node = None;
5402        let mut package_def_node = None;
5403        let mut fn_name_node = None;
5404        let mut fn_def_node = None;
5405        let mut var_name_node = None;
5406        let mut var_def_node = None;
5407
5408        for cap in m.captures {
5409            let Some(&name) = capture_names.get(cap.index as usize) else {
5410                continue;
5411            };
5412            match name {
5413                "package.name" => package_name_node = Some(cap.node),
5414                "package.def" => package_def_node = Some(cap.node),
5415                "fn.name" => fn_name_node = Some(cap.node),
5416                "fn.def" => fn_def_node = Some(cap.node),
5417                "var.name" => var_name_node = Some(cap.node),
5418                "var.def" => var_def_node = Some(cap.node),
5419                _ => {}
5420            }
5421        }
5422
5423        if let (Some(name_node), Some(def_node)) = (package_name_node, package_def_node) {
5424            push_captured_symbol(
5425                &mut symbols,
5426                source,
5427                lang,
5428                name_node,
5429                def_node,
5430                SymbolKind::Class,
5431                vec![],
5432                true,
5433            );
5434        }
5435        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
5436            let scope_chain = perl_scope_chain(&def_node, source);
5437            let kind = if scope_chain.is_empty() {
5438                SymbolKind::Function
5439            } else {
5440                SymbolKind::Method
5441            };
5442            push_captured_symbol(
5443                &mut symbols,
5444                source,
5445                lang,
5446                name_node,
5447                def_node,
5448                kind,
5449                scope_chain,
5450                true,
5451            );
5452        }
5453        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
5454            push_captured_symbol(
5455                &mut symbols,
5456                source,
5457                lang,
5458                name_node,
5459                def_node,
5460                SymbolKind::Variable,
5461                perl_scope_chain(&def_node, source),
5462                true,
5463            );
5464        }
5465    }
5466
5467    dedup_symbols(&mut symbols);
5468    Ok(symbols)
5469}
5470
5471fn extract_vue_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
5472    let mut symbols = Vec::new();
5473    collect_vue_sections(source, root, &mut symbols, true);
5474    dedup_symbols(&mut symbols);
5475    Ok(symbols)
5476}
5477
5478fn collect_vue_sections(
5479    source: &str,
5480    node: &Node,
5481    symbols: &mut Vec<Symbol>,
5482    allow_sections: bool,
5483) {
5484    let mut cursor = node.walk();
5485    if !cursor.goto_first_child() {
5486        return;
5487    }
5488
5489    loop {
5490        let child = cursor.node();
5491        if let Some(section_name) = vue_section_name(&child) {
5492            if allow_sections {
5493                symbols.push(Symbol {
5494                    name: section_name.to_string(),
5495                    kind: SymbolKind::Heading,
5496                    range: node_range(&child),
5497                    signature: vue_opening_tag_signature(source, &child),
5498                    scope_chain: vec![],
5499                    exported: false,
5500                    parent: None,
5501                });
5502            }
5503        } else {
5504            collect_vue_sections(
5505                source,
5506                &child,
5507                symbols,
5508                allow_sections && child.kind() == "document",
5509            );
5510        }
5511
5512        if !cursor.goto_next_sibling() {
5513            break;
5514        }
5515    }
5516}
5517
5518fn vue_section_name(node: &Node) -> Option<&'static str> {
5519    match node.kind() {
5520        "template_element" => Some("template"),
5521        "script_element" => Some("script"),
5522        "style_element" => Some("style"),
5523        _ => None,
5524    }
5525}
5526
5527fn vue_opening_tag_signature(source: &str, node: &Node) -> Option<String> {
5528    find_child_by_kind(*node, "start_tag")
5529        .or_else(|| find_child_by_kind(*node, "script_start_tag"))
5530        .or_else(|| find_child_by_kind(*node, "style_start_tag"))
5531        .or_else(|| find_child_by_kind(*node, "template_start_tag"))
5532        .map(|tag| node_text(source, &tag).trim().to_string())
5533}
5534
5535fn source_line_end_col(source: &str, line: u32) -> u32 {
5536    source
5537        .lines()
5538        .nth(line as usize)
5539        .map(|line| line.len() as u32)
5540        .unwrap_or(0)
5541}
5542
5543fn extract_html_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
5544    let mut headings: Vec<(u8, Symbol)> = Vec::new();
5545    collect_html_headings(source, root, &mut headings);
5546
5547    let total_lines = source.lines().count() as u32;
5548
5549    // Extend each heading's end_line to just before the next heading at the
5550    // same or shallower level (or EOF). This makes aft_zoom return the full
5551    // section content rather than just the heading element's single line.
5552    for i in 0..headings.len() {
5553        let level = headings[i].0;
5554        let section_end = headings[i + 1..]
5555            .iter()
5556            .find(|(l, _)| *l <= level)
5557            .map(|(_, s)| s.range.start_line.saturating_sub(1))
5558            .unwrap_or_else(|| total_lines.saturating_sub(1));
5559        headings[i].1.range.end_line = section_end;
5560        if section_end != headings[i].1.range.start_line {
5561            headings[i].1.range.end_col = source_line_end_col(source, section_end);
5562        }
5563    }
5564
5565    // Build hierarchy: assign scope_chain and parent based on heading level
5566    let mut scope_stack: Vec<(u8, String)> = Vec::new(); // (level, name)
5567    for (level, symbol) in headings.iter_mut() {
5568        // Pop scope entries that are at the same level or deeper
5569        while scope_stack.last().is_some_and(|(l, _)| *l >= *level) {
5570            scope_stack.pop();
5571        }
5572        symbol.scope_chain = scope_stack.iter().map(|(_, name)| name.clone()).collect();
5573        symbol.parent = scope_stack.last().map(|(_, name)| name.clone());
5574        scope_stack.push((*level, symbol.name.clone()));
5575    }
5576
5577    Ok(headings.into_iter().map(|(_, s)| s).collect())
5578}
5579
5580/// Recursively collect h1-h6 elements from the HTML tree.
5581fn collect_html_headings(source: &str, node: &Node, headings: &mut Vec<(u8, Symbol)>) {
5582    let mut cursor = node.walk();
5583    if !cursor.goto_first_child() {
5584        return;
5585    }
5586
5587    loop {
5588        let child = cursor.node();
5589        if child.kind() == "element" {
5590            // Check if this element's start tag is h1-h6
5591            if let Some(start_tag) = child
5592                .child_by_field_name("start_tag")
5593                .or_else(|| child.child(0).filter(|c| c.kind() == "start_tag"))
5594            {
5595                if let Some(tag_name_node) = start_tag
5596                    .child_by_field_name("tag_name")
5597                    .or_else(|| start_tag.child(1).filter(|c| c.kind() == "tag_name"))
5598                {
5599                    let tag_name = node_text(source, &tag_name_node).to_lowercase();
5600                    if let Some(level) = match tag_name.as_str() {
5601                        "h1" => Some(1u8),
5602                        "h2" => Some(2),
5603                        "h3" => Some(3),
5604                        "h4" => Some(4),
5605                        "h5" => Some(5),
5606                        "h6" => Some(6),
5607                        _ => None,
5608                    } {
5609                        // Extract text content from the element
5610                        let text = extract_element_text(source, &child).trim().to_string();
5611                        if !text.is_empty() {
5612                            let range = node_range(&child);
5613                            let signature = format!("<h{}> {}", level, text);
5614                            headings.push((
5615                                level,
5616                                Symbol {
5617                                    name: text,
5618                                    kind: SymbolKind::Heading,
5619                                    range,
5620                                    signature: Some(signature),
5621                                    scope_chain: vec![], // filled later
5622                                    exported: false,
5623                                    parent: None, // filled later
5624                                },
5625                            ));
5626                        }
5627                    }
5628                }
5629            }
5630            // Recurse into element children (nested headings)
5631            collect_html_headings(source, &child, headings);
5632        } else {
5633            // Recurse into other node types (document, body, etc.)
5634            collect_html_headings(source, &child, headings);
5635        }
5636
5637        if !cursor.goto_next_sibling() {
5638            break;
5639        }
5640    }
5641}
5642
5643/// Extract text content from an HTML element, stripping tags.
5644fn extract_element_text(source: &str, node: &Node) -> String {
5645    let mut text = String::new();
5646    let mut cursor = node.walk();
5647    if !cursor.goto_first_child() {
5648        return text;
5649    }
5650    loop {
5651        let child = cursor.node();
5652        match child.kind() {
5653            "text" => {
5654                text.push_str(node_text(source, &child));
5655            }
5656            "element" => {
5657                // Recurse into nested elements (e.g., <strong>, <em>, <a>)
5658                text.push_str(&extract_element_text(source, &child));
5659            }
5660            _ => {}
5661        }
5662        if !cursor.goto_next_sibling() {
5663            break;
5664        }
5665    }
5666    text
5667}
5668
5669fn markdown_atx_heading_level(line: &str) -> Option<u8> {
5670    let trimmed = line.trim_start();
5671    let level = trimmed.bytes().take_while(|byte| *byte == b'#').count();
5672    if !(1..=6).contains(&level) {
5673        return None;
5674    }
5675
5676    let rest = &trimmed[level..];
5677    let marker_is_closed = match rest.chars().next() {
5678        Some(ch) => ch.is_whitespace(),
5679        None => true,
5680    };
5681    marker_is_closed.then_some(level as u8)
5682}
5683
5684/// Extract markdown headings as symbols.
5685/// Each heading becomes a symbol with kind `Heading`, and its range covers the entire
5686/// section (from the heading to the next heading at the same or higher level, or EOF).
5687fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
5688    let mut symbols = Vec::new();
5689    extract_md_sections(source, root, &mut symbols, &[]);
5690    Ok(symbols)
5691}
5692
5693/// Recursively walk `section` nodes to build the heading hierarchy.
5694fn extract_md_sections(
5695    source: &str,
5696    node: &Node,
5697    symbols: &mut Vec<Symbol>,
5698    scope_chain: &[String],
5699) {
5700    let mut cursor = node.walk();
5701    if !cursor.goto_first_child() {
5702        return;
5703    }
5704
5705    loop {
5706        let child = cursor.node();
5707        match child.kind() {
5708            "section" => {
5709                // A section contains an atx_heading as its first child,
5710                // followed by content and possibly nested sections.
5711                let mut section_cursor = child.walk();
5712                let mut heading_name = String::new();
5713                let mut heading_level: u8 = 0;
5714
5715                if section_cursor.goto_first_child() {
5716                    loop {
5717                        let section_child = section_cursor.node();
5718                        if section_child.kind() == "atx_heading" {
5719                            // Extract heading level from marker type
5720                            let mut h_cursor = section_child.walk();
5721                            if h_cursor.goto_first_child() {
5722                                loop {
5723                                    let h_child = h_cursor.node();
5724                                    let kind = h_child.kind();
5725                                    if kind.starts_with("atx_h") && kind.ends_with("_marker") {
5726                                        // "atx_h1_marker" → level 1, "atx_h2_marker" → level 2, etc.
5727                                        heading_level = kind
5728                                            .strip_prefix("atx_h")
5729                                            .and_then(|s| s.strip_suffix("_marker"))
5730                                            .and_then(|s| s.parse::<u8>().ok())
5731                                            .unwrap_or(1);
5732                                    } else if h_child.kind() == "inline" {
5733                                        heading_name =
5734                                            node_text(source, &h_child).trim().to_string();
5735                                    }
5736                                    if !h_cursor.goto_next_sibling() {
5737                                        break;
5738                                    }
5739                                }
5740                            }
5741                        }
5742                        if !section_cursor.goto_next_sibling() {
5743                            break;
5744                        }
5745                    }
5746                }
5747
5748                if !heading_name.is_empty() {
5749                    let mut range = node_range(&child);
5750                    if let Some(next_heading_level) = source
5751                        .lines()
5752                        .nth(range.end_line as usize)
5753                        .and_then(markdown_atx_heading_level)
5754                    {
5755                        if next_heading_level <= heading_level && range.end_line > range.start_line
5756                        {
5757                            range.end_line -= 1;
5758                            range.end_col = source_line_end_col(source, range.end_line);
5759                        }
5760                    }
5761                    let signature = format!(
5762                        "{} {}",
5763                        "#".repeat((heading_level as usize).min(6)),
5764                        heading_name
5765                    );
5766
5767                    symbols.push(Symbol {
5768                        name: heading_name.clone(),
5769                        kind: SymbolKind::Heading,
5770                        range,
5771                        signature: Some(signature),
5772                        scope_chain: scope_chain.to_vec(),
5773                        exported: false,
5774                        parent: scope_chain.last().cloned(),
5775                    });
5776
5777                    // Recurse into the section for nested headings
5778                    let mut new_scope = scope_chain.to_vec();
5779                    new_scope.push(heading_name);
5780                    extract_md_sections(source, &child, symbols, &new_scope);
5781                }
5782            }
5783            _ => {}
5784        }
5785
5786        if !cursor.goto_next_sibling() {
5787            break;
5788        }
5789    }
5790}
5791
5792/// Remove duplicate symbols based on (name, kind, start_line).
5793/// Class declarations can match both "class" and "method" patterns,
5794/// producing duplicates.
5795fn dedup_symbols(symbols: &mut Vec<Symbol>) {
5796    let mut seen = std::collections::HashSet::new();
5797    symbols.retain(|s| {
5798        let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
5799        seen.insert(key)
5800    });
5801}
5802
5803/// Provider that uses tree-sitter for real symbol extraction.
5804/// Implements the `LanguageProvider` trait from `language.rs`.
5805pub struct TreeSitterProvider {
5806    parser: RefCell<FileParser>,
5807}
5808
5809#[derive(Debug, Clone)]
5810struct ReExportTarget {
5811    file: PathBuf,
5812    symbol_name: String,
5813}
5814
5815impl TreeSitterProvider {
5816    /// Create a new `TreeSitterProvider` backed by a fresh `FileParser`.
5817    pub fn new() -> Self {
5818        Self::with_symbol_cache(Arc::new(RwLock::new(SymbolCache::new())))
5819    }
5820
5821    /// Create a new `TreeSitterProvider` backed by a shared symbol cache.
5822    pub fn with_symbol_cache(symbol_cache: SharedSymbolCache) -> Self {
5823        Self {
5824            parser: RefCell::new(FileParser::with_symbol_cache(symbol_cache)),
5825        }
5826    }
5827
5828    /// Return shared symbol cache entries for status reporting.
5829    pub fn symbol_cache_len(&self) -> usize {
5830        let parser = self.parser.borrow();
5831        parser.symbol_cache_len()
5832    }
5833
5834    /// Shared symbol cache backing this provider.
5835    pub fn symbol_cache(&self) -> SharedSymbolCache {
5836        let parser = self.parser.borrow();
5837        parser.symbol_cache()
5838    }
5839
5840    fn resolve_symbol_inner(
5841        &self,
5842        file: &Path,
5843        name: &str,
5844        depth: usize,
5845        visited: &mut HashSet<(PathBuf, String)>,
5846    ) -> Result<Vec<SymbolMatch>, AftError> {
5847        if depth > MAX_REEXPORT_DEPTH {
5848            return Ok(Vec::new());
5849        }
5850
5851        let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
5852        if !visited.insert((canonical_file, name.to_string())) {
5853            return Ok(Vec::new());
5854        }
5855
5856        let symbols = self.parser.borrow_mut().extract_symbols(file)?;
5857        let local_matches = symbol_matches_in_file(file, &symbols, name);
5858        if !local_matches.is_empty() {
5859            return Ok(local_matches);
5860        }
5861
5862        if name == "default" {
5863            let default_matches = self.resolve_local_default_export(file, &symbols)?;
5864            if !default_matches.is_empty() {
5865                return Ok(default_matches);
5866            }
5867        }
5868
5869        let reexport_targets = self.collect_reexport_targets(file, name)?;
5870        let mut matches = Vec::new();
5871        let mut seen = HashSet::new();
5872        for target in reexport_targets {
5873            for resolved in
5874                self.resolve_symbol_inner(&target.file, &target.symbol_name, depth + 1, visited)?
5875            {
5876                let key = format!(
5877                    "{}:{}:{}:{}:{}:{}",
5878                    resolved.file,
5879                    resolved.symbol.name,
5880                    resolved.symbol.range.start_line,
5881                    resolved.symbol.range.start_col,
5882                    resolved.symbol.range.end_line,
5883                    resolved.symbol.range.end_col
5884                );
5885                if seen.insert(key) {
5886                    matches.push(resolved);
5887                }
5888            }
5889        }
5890
5891        Ok(matches)
5892    }
5893
5894    fn collect_reexport_targets(
5895        &self,
5896        file: &Path,
5897        requested_name: &str,
5898    ) -> Result<Vec<ReExportTarget>, AftError> {
5899        let (source, tree, lang) = self.read_parsed_file(file)?;
5900        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
5901            return Ok(Vec::new());
5902        }
5903
5904        let mut targets = Vec::new();
5905        let root = tree.root_node();
5906        let from_dir = file.parent().unwrap_or_else(|| Path::new("."));
5907
5908        let mut cursor = root.walk();
5909        if !cursor.goto_first_child() {
5910            return Ok(targets);
5911        }
5912
5913        loop {
5914            let node = cursor.node();
5915            if node.kind() == "export_statement" {
5916                let Some(source_node) = node.child_by_field_name("source") else {
5917                    if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
5918                        if let Some(symbol_name) =
5919                            resolve_export_clause_name(&source, &export_clause, requested_name)
5920                        {
5921                            targets.push(ReExportTarget {
5922                                file: file.to_path_buf(),
5923                                symbol_name,
5924                            });
5925                        }
5926                    }
5927                    if !cursor.goto_next_sibling() {
5928                        break;
5929                    }
5930                    continue;
5931                };
5932
5933                let Some(module_path) = string_content(&source, &source_node) else {
5934                    if !cursor.goto_next_sibling() {
5935                        break;
5936                    }
5937                    continue;
5938                };
5939
5940                let Some(target_file) = resolve_module_path(from_dir, &module_path) else {
5941                    if !cursor.goto_next_sibling() {
5942                        break;
5943                    }
5944                    continue;
5945                };
5946
5947                if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
5948                    if let Some(symbol_name) =
5949                        resolve_export_clause_name(&source, &export_clause, requested_name)
5950                    {
5951                        targets.push(ReExportTarget {
5952                            file: target_file,
5953                            symbol_name,
5954                        });
5955                    }
5956                } else if export_statement_has_wildcard(&source, &node) {
5957                    targets.push(ReExportTarget {
5958                        file: target_file,
5959                        symbol_name: requested_name.to_string(),
5960                    });
5961                }
5962            }
5963
5964            if !cursor.goto_next_sibling() {
5965                break;
5966            }
5967        }
5968
5969        Ok(targets)
5970    }
5971
5972    fn resolve_local_default_export(
5973        &self,
5974        file: &Path,
5975        symbols: &[Symbol],
5976    ) -> Result<Vec<SymbolMatch>, AftError> {
5977        let (source, tree, lang) = self.read_parsed_file(file)?;
5978        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
5979            return Ok(Vec::new());
5980        }
5981
5982        let root = tree.root_node();
5983        let mut matches = Vec::new();
5984        let mut seen = HashSet::new();
5985
5986        let mut cursor = root.walk();
5987        if !cursor.goto_first_child() {
5988            return Ok(matches);
5989        }
5990
5991        loop {
5992            let node = cursor.node();
5993            if node.kind() == "export_statement"
5994                && node.child_by_field_name("source").is_none()
5995                && node_contains_token(&source, &node, "default")
5996            {
5997                if let Some(target_name) = default_export_target_name(&source, &node) {
5998                    for symbol_match in symbol_matches_in_file(file, symbols, &target_name) {
5999                        let key = format!(
6000                            "{}:{}:{}:{}:{}:{}",
6001                            symbol_match.file,
6002                            symbol_match.symbol.name,
6003                            symbol_match.symbol.range.start_line,
6004                            symbol_match.symbol.range.start_col,
6005                            symbol_match.symbol.range.end_line,
6006                            symbol_match.symbol.range.end_col
6007                        );
6008                        if seen.insert(key) {
6009                            matches.push(symbol_match);
6010                        }
6011                    }
6012                }
6013            }
6014
6015            if !cursor.goto_next_sibling() {
6016                break;
6017            }
6018        }
6019
6020        Ok(matches)
6021    }
6022
6023    fn read_parsed_file(&self, file: &Path) -> Result<(String, Tree, LangId), AftError> {
6024        let source = std::fs::read_to_string(file).map_err(|e| AftError::FileNotFound {
6025            path: format!("{}: {}", file.display(), e),
6026        })?;
6027        let (tree, lang) = {
6028            let mut parser = self.parser.borrow_mut();
6029            parser.parse_cloned(file)?
6030        };
6031        Ok((source, tree, lang))
6032    }
6033}
6034
6035fn symbol_matches_in_file(file: &Path, symbols: &[Symbol], name: &str) -> Vec<SymbolMatch> {
6036    symbols
6037        .iter()
6038        .filter(|symbol| symbol.name == name)
6039        .cloned()
6040        .map(|symbol| SymbolMatch {
6041            file: file.display().to_string(),
6042            symbol,
6043        })
6044        .collect()
6045}
6046
6047fn string_content(source: &str, node: &Node) -> Option<String> {
6048    let text = node_text(source, node);
6049    if text.len() < 2 {
6050        return None;
6051    }
6052
6053    Some(
6054        text.trim_start_matches(|c| c == '\'' || c == '"')
6055            .trim_end_matches(|c| c == '\'' || c == '"')
6056            .to_string(),
6057    )
6058}
6059
6060fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
6061    let mut cursor = node.walk();
6062    if !cursor.goto_first_child() {
6063        return None;
6064    }
6065
6066    loop {
6067        let child = cursor.node();
6068        if child.kind() == kind {
6069            return Some(child);
6070        }
6071        if !cursor.goto_next_sibling() {
6072            break;
6073        }
6074    }
6075
6076    None
6077}
6078
6079fn resolve_export_clause_name(
6080    source: &str,
6081    export_clause: &Node,
6082    requested_name: &str,
6083) -> Option<String> {
6084    let mut cursor = export_clause.walk();
6085    if !cursor.goto_first_child() {
6086        return None;
6087    }
6088
6089    loop {
6090        let child = cursor.node();
6091        if child.kind() == "export_specifier" {
6092            let (source_name, exported_name) = export_specifier_names(source, &child)?;
6093            if exported_name == requested_name {
6094                return Some(source_name);
6095            }
6096        }
6097
6098        if !cursor.goto_next_sibling() {
6099            break;
6100        }
6101    }
6102
6103    None
6104}
6105
6106fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
6107    let source_name = specifier
6108        .child_by_field_name("name")
6109        .map(|node| node_text(source, &node).to_string());
6110    let alias_name = specifier
6111        .child_by_field_name("alias")
6112        .map(|node| node_text(source, &node).to_string());
6113
6114    if let Some(source_name) = source_name {
6115        let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
6116        return Some((source_name, exported_name));
6117    }
6118
6119    let mut names = Vec::new();
6120    let mut cursor = specifier.walk();
6121    if cursor.goto_first_child() {
6122        loop {
6123            let child = cursor.node();
6124            let child_text = node_text(source, &child).trim();
6125            if matches!(
6126                child.kind(),
6127                "identifier" | "type_identifier" | "property_identifier"
6128            ) || child_text == "default"
6129            {
6130                names.push(child_text.to_string());
6131            }
6132            if !cursor.goto_next_sibling() {
6133                break;
6134            }
6135        }
6136    }
6137
6138    match names.as_slice() {
6139        [name] => Some((name.clone(), name.clone())),
6140        [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
6141        _ => None,
6142    }
6143}
6144
6145fn export_statement_has_wildcard(source: &str, node: &Node) -> bool {
6146    let mut cursor = node.walk();
6147    if !cursor.goto_first_child() {
6148        return false;
6149    }
6150
6151    loop {
6152        if node_text(source, &cursor.node()).trim() == "*" {
6153            return true;
6154        }
6155        if !cursor.goto_next_sibling() {
6156            break;
6157        }
6158    }
6159
6160    false
6161}
6162
6163fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
6164    let mut cursor = node.walk();
6165    if !cursor.goto_first_child() {
6166        return false;
6167    }
6168
6169    loop {
6170        if node_text(source, &cursor.node()).trim() == token {
6171            return true;
6172        }
6173        if !cursor.goto_next_sibling() {
6174            break;
6175        }
6176    }
6177
6178    false
6179}
6180
6181fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
6182    if let Some(value_node) = export_stmt.child_by_field_name("value") {
6183        if let Some(name) = default_export_node_name(source, &value_node) {
6184            return Some(name);
6185        }
6186    }
6187
6188    if let Some(declaration_node) = export_stmt.child_by_field_name("declaration") {
6189        if let Some(name) = default_export_node_name(source, &declaration_node) {
6190            return Some(name);
6191        }
6192    }
6193
6194    let mut cursor = export_stmt.walk();
6195    if !cursor.goto_first_child() {
6196        return None;
6197    }
6198
6199    loop {
6200        let child = cursor.node();
6201        if let Some(name) = default_export_node_name(source, &child) {
6202            return Some(name);
6203        }
6204
6205        if !cursor.goto_next_sibling() {
6206            break;
6207        }
6208    }
6209
6210    None
6211}
6212
6213fn default_export_node_name(source: &str, node: &Node) -> Option<String> {
6214    match node.kind() {
6215        "function_declaration"
6216        | "generator_function_declaration"
6217        | "function_expression"
6218        | "generator_function"
6219        | "class_declaration"
6220        | "class" => node
6221            .child_by_field_name("name")
6222            .map(|name_node| node_text(source, &name_node).to_string())
6223            .or_else(|| Some("default".to_string())),
6224        "interface_declaration" | "enum_declaration" | "type_alias_declaration" => node
6225            .child_by_field_name("name")
6226            .map(|name_node| node_text(source, &name_node).to_string()),
6227        "lexical_declaration" => lexical_declaration_name(source, node),
6228        "identifier" | "type_identifier" => {
6229            let text = node_text(source, node);
6230            (text != "export" && text != "default").then(|| text.to_string())
6231        }
6232        _ => None,
6233    }
6234}
6235
6236fn lexical_declaration_name(source: &str, node: &Node) -> Option<String> {
6237    let mut cursor = node.walk();
6238    if !cursor.goto_first_child() {
6239        return None;
6240    }
6241
6242    loop {
6243        let child = cursor.node();
6244        if child.kind() == "variable_declarator" {
6245            if let Some(name_node) = child.child_by_field_name("name") {
6246                return Some(node_text(source, &name_node).to_string());
6247            }
6248        }
6249        if !cursor.goto_next_sibling() {
6250            break;
6251        }
6252    }
6253
6254    None
6255}
6256
6257impl crate::language::LanguageProvider for TreeSitterProvider {
6258    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
6259        let matches = self.resolve_symbol_inner(file, name, 0, &mut HashSet::new())?;
6260
6261        if matches.is_empty() {
6262            Err(AftError::SymbolNotFound {
6263                name: name.to_string(),
6264                file: file.display().to_string(),
6265            })
6266        } else {
6267            Ok(matches)
6268        }
6269    }
6270
6271    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
6272        self.parser.borrow_mut().extract_symbols(file)
6273    }
6274
6275    fn as_any(&self) -> &dyn std::any::Any {
6276        self
6277    }
6278}
6279
6280#[cfg(test)]
6281mod tests {
6282    use super::*;
6283    use crate::language::LanguageProvider;
6284    use crate::symbol_cache_disk;
6285    use std::path::{Path, PathBuf};
6286
6287    fn fixture_path(name: &str) -> PathBuf {
6288        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6289            .join("tests")
6290            .join("fixtures")
6291            .join(name)
6292    }
6293
6294    fn test_symbol(name: &str) -> Symbol {
6295        Symbol {
6296            name: name.to_string(),
6297            kind: SymbolKind::Function,
6298            range: Range {
6299                start_line: 0,
6300                start_col: 0,
6301                end_line: 0,
6302                end_col: 10,
6303            },
6304            signature: Some(format!("fn {name}()")),
6305            scope_chain: Vec::new(),
6306            exported: true,
6307            parent: None,
6308        }
6309    }
6310
6311    #[test]
6312    fn inc_and_scss_extensions_are_detected() {
6313        assert_eq!(
6314            detect_language(Path::new("template.inc")),
6315            Some(LangId::Php)
6316        );
6317        assert_eq!(
6318            detect_language(Path::new("styles.scss")),
6319            Some(LangId::Scss)
6320        );
6321        assert_eq!(detect_language(Path::new("template.tpl")), None);
6322    }
6323
6324    #[test]
6325    fn inc_files_parse_with_php_grammar() {
6326        let tmp = tempfile::tempdir().expect("create temp dir");
6327        let file = tmp.path().join("partial.inc");
6328        std::fs::write(&file, "<?php\nfunction render_partial() { return 1; }\n")
6329            .expect("write inc file");
6330
6331        let mut parser = FileParser::new();
6332        let symbols = parser
6333            .extract_symbols(&file)
6334            .expect("extract php inc symbols");
6335        let function = symbols
6336            .iter()
6337            .find(|symbol| symbol.name == "render_partial")
6338            .expect("find PHP function in .inc");
6339        assert_eq!(function.kind, SymbolKind::Function);
6340    }
6341
6342    #[test]
6343    fn scss_symbols_include_mixin_variable_function_and_rule() {
6344        let tmp = tempfile::tempdir().expect("create temp dir");
6345        let file = tmp.path().join("styles.scss");
6346        std::fs::write(
6347            &file,
6348            r#"$brand-color: #336699;
6349
6350@mixin button-base($padding) {
6351  padding: $padding;
6352}
6353
6354@function double($value) {
6355  @return $value * 2;
6356}
6357
6358.card, .panel {
6359  color: $brand-color;
6360}
6361"#,
6362        )
6363        .expect("write scss file");
6364
6365        let mut parser = FileParser::new();
6366        let symbols = parser.extract_symbols(&file).expect("extract scss symbols");
6367        let get = |name: &str| {
6368            symbols
6369                .iter()
6370                .find(|symbol| symbol.name == name)
6371                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
6372        };
6373
6374        assert_eq!(get("button-base").kind, SymbolKind::Function);
6375        assert_eq!(get("double").kind, SymbolKind::Function);
6376        assert_eq!(get("$brand-color").kind, SymbolKind::Variable);
6377        assert_eq!(get(".card, .panel").kind, SymbolKind::Class);
6378    }
6379
6380    #[test]
6381    fn symbol_cache_load_from_disk_round_trips_synthetic_entry() {
6382        let project = tempfile::tempdir().expect("create project dir");
6383        let storage = tempfile::tempdir().expect("create storage dir");
6384        let source = project.path().join("src/lib.rs");
6385        std::fs::create_dir_all(source.parent().expect("source parent"))
6386            .expect("create source dir");
6387        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
6388        let mtime = std::fs::metadata(&source)
6389            .expect("stat source")
6390            .modified()
6391            .expect("source mtime");
6392        let content = std::fs::read(&source).expect("read source");
6393        let size = content.len() as u64;
6394        let hash = crate::cache_freshness::hash_bytes(&content);
6395
6396        let mut cache = SymbolCache::new();
6397        cache.set_project_root(project.path().to_path_buf());
6398        cache.insert(
6399            source.clone(),
6400            mtime,
6401            size,
6402            hash,
6403            vec![test_symbol("cached")],
6404        );
6405        symbol_cache_disk::write_to_disk(&cache, storage.path(), "unit-project")
6406            .expect("write symbol cache");
6407
6408        let mut restored = SymbolCache::new();
6409        let loaded = restored.load_from_disk(storage.path(), "unit-project", project.path());
6410        let symbols = restored.get(&source, mtime).expect("restored symbols");
6411
6412        assert_eq!(loaded, 1);
6413        assert_eq!(symbols.len(), 1);
6414        assert_eq!(symbols[0].name, "cached");
6415    }
6416
6417    #[test]
6418    fn symbol_cache_load_from_disk_drops_stale_synthetic_entry() {
6419        let project = tempfile::tempdir().expect("create project dir");
6420        let storage = tempfile::tempdir().expect("create storage dir");
6421        let source = project.path().join("src/lib.rs");
6422        std::fs::create_dir_all(source.parent().expect("source parent"))
6423            .expect("create source dir");
6424        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
6425        let mtime = std::fs::metadata(&source)
6426            .expect("stat source")
6427            .modified()
6428            .expect("source mtime");
6429        let content = std::fs::read(&source).expect("read source");
6430        let size = content.len() as u64;
6431        let hash = crate::cache_freshness::hash_bytes(&content);
6432
6433        let mut cache = SymbolCache::new();
6434        cache.set_project_root(project.path().to_path_buf());
6435        cache.insert(
6436            source.clone(),
6437            mtime,
6438            size,
6439            hash,
6440            vec![test_symbol("cached")],
6441        );
6442        symbol_cache_disk::write_to_disk(&cache, storage.path(), "stale-unit-project")
6443            .expect("write symbol cache");
6444
6445        std::fs::write(&source, "pub fn cached() {}\npub fn fresh() {}\n").expect("change source");
6446
6447        let mut restored = SymbolCache::new();
6448        let loaded = restored.load_from_disk(storage.path(), "stale-unit-project", project.path());
6449
6450        assert_eq!(loaded, 0);
6451        assert_eq!(restored.len(), 0);
6452    }
6453
6454    #[test]
6455    fn stale_prewarm_generation_cannot_repopulate_symbol_cache_after_reset() {
6456        let project = tempfile::tempdir().expect("create project dir");
6457        let storage = tempfile::tempdir().expect("create storage dir");
6458        let source = project.path().join("src/lib.rs");
6459        std::fs::create_dir_all(source.parent().expect("source parent"))
6460            .expect("create source dir");
6461        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
6462        let mtime = std::fs::metadata(&source)
6463            .expect("stat source")
6464            .modified()
6465            .expect("source mtime");
6466        let content = std::fs::read(&source).expect("read source");
6467        let size = content.len() as u64;
6468        let hash = crate::cache_freshness::hash_bytes(&content);
6469
6470        let mut disk_cache = SymbolCache::new();
6471        disk_cache.set_project_root(project.path().to_path_buf());
6472        disk_cache.insert(
6473            source.clone(),
6474            mtime,
6475            size,
6476            hash,
6477            vec![test_symbol("cached")],
6478        );
6479        symbol_cache_disk::write_to_disk(&disk_cache, storage.path(), "prewarm-reset")
6480            .expect("write symbol cache");
6481
6482        let shared = Arc::new(RwLock::new(SymbolCache::new()));
6483        let stale_generation = shared.write().unwrap().reset();
6484        let active_generation = shared.write().unwrap().reset();
6485        assert_ne!(stale_generation, active_generation);
6486
6487        {
6488            let mut cache = shared.write().unwrap();
6489            assert!(!cache
6490                .set_project_root_for_generation(stale_generation, project.path().to_path_buf()));
6491            assert_eq!(
6492                cache.load_from_disk_for_generation(
6493                    stale_generation,
6494                    storage.path(),
6495                    "prewarm-reset",
6496                    project.path()
6497                ),
6498                0
6499            );
6500        }
6501
6502        let mut stale_parser =
6503            FileParser::with_symbol_cache_generation(Arc::clone(&shared), Some(stale_generation));
6504        stale_parser
6505            .extract_symbols(&source)
6506            .expect("stale prewarm parses source but must not write cache");
6507
6508        let cache = shared.read().unwrap();
6509        assert_eq!(cache.generation(), active_generation);
6510        assert_eq!(cache.len(), 0);
6511        assert!(cache.project_root().is_none());
6512        assert!(!cache.contains_key(&source));
6513    }
6514
6515    // --- Language detection ---
6516
6517    #[test]
6518    fn detect_ts() {
6519        assert_eq!(
6520            detect_language(Path::new("foo.ts")),
6521            Some(LangId::TypeScript)
6522        );
6523    }
6524
6525    #[test]
6526    fn detect_tsx() {
6527        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
6528    }
6529
6530    #[test]
6531    fn detect_js() {
6532        assert_eq!(
6533            detect_language(Path::new("foo.js")),
6534            Some(LangId::JavaScript)
6535        );
6536    }
6537
6538    #[test]
6539    fn detect_jsx() {
6540        assert_eq!(
6541            detect_language(Path::new("foo.jsx")),
6542            Some(LangId::JavaScript)
6543        );
6544    }
6545
6546    #[test]
6547    fn detect_py() {
6548        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
6549    }
6550
6551    #[test]
6552    fn detect_rs() {
6553        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
6554    }
6555
6556    #[test]
6557    fn detect_go() {
6558        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
6559    }
6560
6561    #[test]
6562    fn detect_c() {
6563        assert_eq!(detect_language(Path::new("foo.c")), Some(LangId::C));
6564    }
6565
6566    #[test]
6567    fn detect_h() {
6568        assert_eq!(detect_language(Path::new("foo.h")), Some(LangId::C));
6569    }
6570
6571    #[test]
6572    fn detect_cc() {
6573        assert_eq!(detect_language(Path::new("foo.cc")), Some(LangId::Cpp));
6574    }
6575
6576    #[test]
6577    fn detect_cpp() {
6578        assert_eq!(detect_language(Path::new("foo.cpp")), Some(LangId::Cpp));
6579    }
6580
6581    #[test]
6582    fn detect_cxx() {
6583        assert_eq!(detect_language(Path::new("foo.cxx")), Some(LangId::Cpp));
6584    }
6585
6586    #[test]
6587    fn detect_hpp() {
6588        assert_eq!(detect_language(Path::new("foo.hpp")), Some(LangId::Cpp));
6589    }
6590
6591    #[test]
6592    fn detect_hh() {
6593        assert_eq!(detect_language(Path::new("foo.hh")), Some(LangId::Cpp));
6594    }
6595
6596    #[test]
6597    fn detect_zig() {
6598        assert_eq!(detect_language(Path::new("foo.zig")), Some(LangId::Zig));
6599    }
6600
6601    #[test]
6602    fn detect_cs() {
6603        assert_eq!(detect_language(Path::new("foo.cs")), Some(LangId::CSharp));
6604    }
6605
6606    #[test]
6607    fn detect_unknown_returns_none() {
6608        assert_eq!(detect_language(Path::new("foo.txt")), None);
6609    }
6610
6611    // --- Unsupported extension error ---
6612
6613    #[test]
6614    fn unsupported_extension_returns_invalid_request() {
6615        // Use a file that exists but has an unsupported extension
6616        let path = fixture_path("sample.ts");
6617        let bad_path = path.with_extension("txt");
6618        // Create a dummy file so the error comes from language detection, not I/O
6619        std::fs::write(&bad_path, "hello").unwrap();
6620        let provider = TreeSitterProvider::new();
6621        let result = provider.list_symbols(&bad_path);
6622        std::fs::remove_file(&bad_path).ok();
6623        match result {
6624            Err(AftError::InvalidRequest { message }) => {
6625                assert!(
6626                    message.contains("unsupported file extension"),
6627                    "msg: {}",
6628                    message
6629                );
6630                assert!(message.contains("txt"), "msg: {}", message);
6631            }
6632            other => panic!("expected InvalidRequest, got {:?}", other),
6633        }
6634    }
6635
6636    // --- TypeScript extraction ---
6637
6638    #[test]
6639    fn ts_extracts_all_symbol_kinds() {
6640        let provider = TreeSitterProvider::new();
6641        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6642
6643        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
6644        assert!(
6645            names.contains(&"greet"),
6646            "missing function greet: {:?}",
6647            names
6648        );
6649        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
6650        assert!(
6651            names.contains(&"UserService"),
6652            "missing class UserService: {:?}",
6653            names
6654        );
6655        assert!(
6656            names.contains(&"Config"),
6657            "missing interface Config: {:?}",
6658            names
6659        );
6660        assert!(
6661            names.contains(&"Status"),
6662            "missing enum Status: {:?}",
6663            names
6664        );
6665        assert!(
6666            names.contains(&"UserId"),
6667            "missing type alias UserId: {:?}",
6668            names
6669        );
6670        assert!(
6671            names.contains(&"internalHelper"),
6672            "missing non-exported fn: {:?}",
6673            names
6674        );
6675
6676        // At least 6 unique symbols as required
6677        assert!(
6678            symbols.len() >= 6,
6679            "expected ≥6 symbols, got {}: {:?}",
6680            symbols.len(),
6681            names
6682        );
6683    }
6684
6685    #[test]
6686    fn ts_symbol_kinds_correct() {
6687        let provider = TreeSitterProvider::new();
6688        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6689
6690        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
6691
6692        assert_eq!(find("greet").kind, SymbolKind::Function);
6693        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
6694        assert_eq!(find("UserService").kind, SymbolKind::Class);
6695        assert_eq!(find("Config").kind, SymbolKind::Interface);
6696        assert_eq!(find("Status").kind, SymbolKind::Enum);
6697        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
6698    }
6699
6700    #[test]
6701    fn ts_export_detection() {
6702        let provider = TreeSitterProvider::new();
6703        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6704
6705        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
6706
6707        assert!(find("greet").exported, "greet should be exported");
6708        assert!(find("add").exported, "add should be exported");
6709        assert!(
6710            find("UserService").exported,
6711            "UserService should be exported"
6712        );
6713        assert!(find("Config").exported, "Config should be exported");
6714        assert!(find("Status").exported, "Status should be exported");
6715        assert!(
6716            !find("internalHelper").exported,
6717            "internalHelper should not be exported"
6718        );
6719    }
6720
6721    #[test]
6722    fn ts_method_scope_chain() {
6723        let provider = TreeSitterProvider::new();
6724        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6725
6726        let methods: Vec<&Symbol> = symbols
6727            .iter()
6728            .filter(|s| s.kind == SymbolKind::Method)
6729            .collect();
6730        assert!(!methods.is_empty(), "should have at least one method");
6731
6732        for method in &methods {
6733            assert_eq!(
6734                method.scope_chain,
6735                vec!["UserService"],
6736                "method {} should have UserService in scope chain",
6737                method.name
6738            );
6739            assert_eq!(method.parent.as_deref(), Some("UserService"));
6740        }
6741    }
6742
6743    #[test]
6744    fn ts_signatures_present() {
6745        let provider = TreeSitterProvider::new();
6746        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6747
6748        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
6749
6750        let greet_sig = find("greet").signature.as_ref().unwrap();
6751        assert!(
6752            greet_sig.contains("greet"),
6753            "signature should contain function name: {}",
6754            greet_sig
6755        );
6756    }
6757
6758    #[test]
6759    fn ts_ranges_valid() {
6760        let provider = TreeSitterProvider::new();
6761        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
6762
6763        for s in &symbols {
6764            assert!(
6765                s.range.end_line >= s.range.start_line,
6766                "symbol {} has invalid range: {:?}",
6767                s.name,
6768                s.range
6769            );
6770        }
6771    }
6772
6773    // --- JavaScript extraction ---
6774
6775    #[test]
6776    fn js_extracts_core_symbols() {
6777        let provider = TreeSitterProvider::new();
6778        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
6779
6780        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
6781        assert!(
6782            names.contains(&"multiply"),
6783            "missing function multiply: {:?}",
6784            names
6785        );
6786        assert!(
6787            names.contains(&"divide"),
6788            "missing arrow fn divide: {:?}",
6789            names
6790        );
6791        assert!(
6792            names.contains(&"EventEmitter"),
6793            "missing class EventEmitter: {:?}",
6794            names
6795        );
6796        assert!(
6797            names.contains(&"main"),
6798            "missing default export fn main: {:?}",
6799            names
6800        );
6801
6802        assert!(
6803            symbols.len() >= 4,
6804            "expected ≥4 symbols, got {}: {:?}",
6805            symbols.len(),
6806            names
6807        );
6808    }
6809
6810    #[test]
6811    fn js_arrow_fn_correctly_named() {
6812        let provider = TreeSitterProvider::new();
6813        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
6814
6815        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
6816        assert_eq!(divide.kind, SymbolKind::Function);
6817        assert!(divide.exported, "divide should be exported");
6818
6819        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
6820        assert_eq!(internal.kind, SymbolKind::Function);
6821        assert!(!internal.exported, "internalUtil should not be exported");
6822    }
6823
6824    #[test]
6825    fn js_method_scope_chain() {
6826        let provider = TreeSitterProvider::new();
6827        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
6828
6829        let methods: Vec<&Symbol> = symbols
6830            .iter()
6831            .filter(|s| s.kind == SymbolKind::Method)
6832            .collect();
6833
6834        for method in &methods {
6835            assert_eq!(
6836                method.scope_chain,
6837                vec!["EventEmitter"],
6838                "method {} should have EventEmitter in scope chain",
6839                method.name
6840            );
6841        }
6842    }
6843
6844    // --- TSX extraction ---
6845
6846    #[test]
6847    fn tsx_extracts_react_component() {
6848        let provider = TreeSitterProvider::new();
6849        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
6850
6851        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
6852        assert!(
6853            names.contains(&"Button"),
6854            "missing React component Button: {:?}",
6855            names
6856        );
6857        assert!(
6858            names.contains(&"Counter"),
6859            "missing class Counter: {:?}",
6860            names
6861        );
6862        assert!(
6863            names.contains(&"formatLabel"),
6864            "missing function formatLabel: {:?}",
6865            names
6866        );
6867
6868        assert!(
6869            symbols.len() >= 2,
6870            "expected ≥2 symbols, got {}: {:?}",
6871            symbols.len(),
6872            names
6873        );
6874    }
6875
6876    #[test]
6877    fn tsx_jsx_doesnt_break_parser() {
6878        // Main assertion: TSX grammar handles JSX without errors
6879        let provider = TreeSitterProvider::new();
6880        let result = provider.list_symbols(&fixture_path("sample.tsx"));
6881        assert!(
6882            result.is_ok(),
6883            "TSX parsing should succeed: {:?}",
6884            result.err()
6885        );
6886    }
6887
6888    // --- resolve_symbol ---
6889
6890    #[test]
6891    fn resolve_symbol_finds_match() {
6892        let provider = TreeSitterProvider::new();
6893        let matches = provider
6894            .resolve_symbol(&fixture_path("sample.ts"), "greet")
6895            .unwrap();
6896        assert_eq!(matches.len(), 1);
6897        assert_eq!(matches[0].symbol.name, "greet");
6898        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
6899    }
6900
6901    #[test]
6902    fn resolve_symbol_not_found() {
6903        let provider = TreeSitterProvider::new();
6904        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
6905        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
6906    }
6907
6908    #[test]
6909    fn resolve_symbol_follows_reexport_chains() {
6910        let dir = tempfile::tempdir().unwrap();
6911        let config = dir.path().join("config.ts");
6912        let barrel1 = dir.path().join("barrel1.ts");
6913        let barrel2 = dir.path().join("barrel2.ts");
6914        let barrel3 = dir.path().join("barrel3.ts");
6915        let index = dir.path().join("index.ts");
6916
6917        std::fs::write(
6918            &config,
6919            "export class Config {}\nexport default class DefaultConfig {}\n",
6920        )
6921        .unwrap();
6922        std::fs::write(
6923            &barrel1,
6924            "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
6925        )
6926        .unwrap();
6927        std::fs::write(
6928            &barrel2,
6929            "export { Config as RenamedConfig } from './barrel1';\n",
6930        )
6931        .unwrap();
6932        std::fs::write(
6933            &barrel3,
6934            "export * from './barrel2';\nexport * from './barrel1';\n",
6935        )
6936        .unwrap();
6937        std::fs::write(
6938            &index,
6939            "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
6940        )
6941        .unwrap();
6942
6943        let provider = TreeSitterProvider::new();
6944        let config_canon = std::fs::canonicalize(&config).unwrap();
6945
6946        let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
6947        assert_eq!(direct.len(), 1);
6948        assert_eq!(direct[0].symbol.name, "Config");
6949        assert_eq!(direct[0].file, config_canon.display().to_string());
6950
6951        let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
6952        assert_eq!(renamed.len(), 1);
6953        assert_eq!(renamed[0].symbol.name, "Config");
6954        assert_eq!(renamed[0].file, config_canon.display().to_string());
6955
6956        let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
6957        assert_eq!(wildcard_chain.len(), 1);
6958        assert_eq!(wildcard_chain[0].symbol.name, "Config");
6959        assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
6960
6961        let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
6962        assert_eq!(wildcard_default.len(), 1);
6963        assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
6964        assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
6965
6966        let local = provider.resolve_symbol(&index, "Config").unwrap();
6967        assert_eq!(local.len(), 1);
6968        assert_eq!(local[0].symbol.name, "Config");
6969        assert_eq!(local[0].file, index.display().to_string());
6970    }
6971
6972    // --- Parse tree caching ---
6973
6974    #[test]
6975    fn symbol_range_includes_rust_attributes() {
6976        let dir = tempfile::tempdir().unwrap();
6977        let path = dir.path().join("test_attrs.rs");
6978        std::fs::write(
6979            &path,
6980            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
6981        )
6982        .unwrap();
6983
6984        let provider = TreeSitterProvider::new();
6985        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
6986        assert_eq!(matches.len(), 1);
6987        assert_eq!(
6988            matches[0].symbol.range.start_line, 0,
6989            "symbol range should include preceding /// doc comment, got start_line={}",
6990            matches[0].symbol.range.start_line
6991        );
6992    }
6993
6994    #[test]
6995    fn symbol_range_includes_go_doc_comment() {
6996        let dir = tempfile::tempdir().unwrap();
6997        let path = dir.path().join("test_doc.go");
6998        std::fs::write(
6999            &path,
7000            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
7001        )
7002        .unwrap();
7003
7004        let provider = TreeSitterProvider::new();
7005        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
7006        assert_eq!(matches.len(), 1);
7007        assert_eq!(
7008            matches[0].symbol.range.start_line, 2,
7009            "symbol range should include preceding doc comments, got start_line={}",
7010            matches[0].symbol.range.start_line
7011        );
7012    }
7013
7014    #[test]
7015    fn symbol_range_skips_unrelated_comments() {
7016        let dir = tempfile::tempdir().unwrap();
7017        let path = dir.path().join("test_gap.go");
7018        std::fs::write(
7019            &path,
7020            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
7021        )
7022        .unwrap();
7023
7024        let provider = TreeSitterProvider::new();
7025        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
7026        assert_eq!(matches.len(), 1);
7027        assert_eq!(
7028            matches[0].symbol.range.start_line, 4,
7029            "symbol range should NOT include comment separated by blank line, got start_line={}",
7030            matches[0].symbol.range.start_line
7031        );
7032    }
7033
7034    #[test]
7035    fn parse_cache_returns_same_tree() {
7036        let mut parser = FileParser::new();
7037        let path = fixture_path("sample.ts");
7038
7039        let (tree1, _) = parser.parse(&path).unwrap();
7040        let tree1_root = tree1.root_node().byte_range();
7041
7042        let (tree2, _) = parser.parse(&path).unwrap();
7043        let tree2_root = tree2.root_node().byte_range();
7044
7045        // Same tree (cache hit) should return identical root node range
7046        assert_eq!(tree1_root, tree2_root);
7047    }
7048
7049    #[test]
7050    fn extract_symbols_from_tree_matches_list_symbols() {
7051        let path = fixture_path("sample.rs");
7052        let source = std::fs::read_to_string(&path).unwrap();
7053
7054        let provider = TreeSitterProvider::new();
7055        let listed = provider.list_symbols(&path).unwrap();
7056
7057        let mut parser = FileParser::new();
7058        let (tree, lang) = parser.parse(&path).unwrap();
7059        let extracted = extract_symbols_from_tree(&source, tree, lang).unwrap();
7060
7061        assert_eq!(symbols_as_debug(&extracted), symbols_as_debug(&listed));
7062    }
7063
7064    fn symbols_as_debug(symbols: &[Symbol]) -> Vec<String> {
7065        symbols
7066            .iter()
7067            .map(|symbol| {
7068                format!(
7069                    "{}|{:?}|{}:{}-{}:{}|{:?}|{:?}|{}|{:?}",
7070                    symbol.name,
7071                    symbol.kind,
7072                    symbol.range.start_line,
7073                    symbol.range.start_col,
7074                    symbol.range.end_line,
7075                    symbol.range.end_col,
7076                    symbol.signature,
7077                    symbol.scope_chain,
7078                    symbol.exported,
7079                    symbol.parent,
7080                )
7081            })
7082            .collect()
7083    }
7084
7085    // --- Python extraction ---
7086
7087    #[test]
7088    fn py_extracts_all_symbols() {
7089        let provider = TreeSitterProvider::new();
7090        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7091
7092        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7093        assert!(
7094            names.contains(&"top_level_function"),
7095            "missing top_level_function: {:?}",
7096            names
7097        );
7098        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
7099        assert!(
7100            names.contains(&"instance_method"),
7101            "missing method instance_method: {:?}",
7102            names
7103        );
7104        assert!(
7105            names.contains(&"decorated_function"),
7106            "missing decorated_function: {:?}",
7107            names
7108        );
7109
7110        // Plan requires ≥4 symbols
7111        assert!(
7112            symbols.len() >= 4,
7113            "expected ≥4 symbols, got {}: {:?}",
7114            symbols.len(),
7115            names
7116        );
7117    }
7118
7119    #[test]
7120    fn py_symbol_kinds_correct() {
7121        let provider = TreeSitterProvider::new();
7122        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7123
7124        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7125
7126        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
7127        assert_eq!(find("MyClass").kind, SymbolKind::Class);
7128        assert_eq!(find("instance_method").kind, SymbolKind::Method);
7129        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
7130        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
7131        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
7132        assert_eq!(find("inner_method").kind, SymbolKind::Method);
7133        assert_eq!(find("outer_method").kind, SymbolKind::Method);
7134    }
7135
7136    #[test]
7137    fn py_method_scope_chain() {
7138        let provider = TreeSitterProvider::new();
7139        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7140
7141        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7142
7143        // Method inside MyClass
7144        assert_eq!(
7145            find("instance_method").scope_chain,
7146            vec!["MyClass"],
7147            "instance_method should have MyClass in scope chain"
7148        );
7149        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
7150
7151        // Method inside OuterClass > InnerClass
7152        assert_eq!(
7153            find("inner_method").scope_chain,
7154            vec!["OuterClass", "InnerClass"],
7155            "inner_method should have nested scope chain"
7156        );
7157
7158        // InnerClass itself should have OuterClass in scope
7159        assert_eq!(
7160            find("InnerClass").scope_chain,
7161            vec!["OuterClass"],
7162            "InnerClass should have OuterClass in scope"
7163        );
7164
7165        // Top-level function has empty scope
7166        assert!(
7167            find("top_level_function").scope_chain.is_empty(),
7168            "top-level function should have empty scope chain"
7169        );
7170    }
7171
7172    #[test]
7173    fn py_decorated_function_signature() {
7174        let provider = TreeSitterProvider::new();
7175        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7176
7177        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7178
7179        let sig = find("decorated_function").signature.as_ref().unwrap();
7180        assert!(
7181            sig.contains("@staticmethod"),
7182            "decorated function signature should include decorator: {}",
7183            sig
7184        );
7185        assert!(
7186            sig.contains("def decorated_function"),
7187            "signature should include function def: {}",
7188            sig
7189        );
7190    }
7191
7192    #[test]
7193    fn py_ranges_valid() {
7194        let provider = TreeSitterProvider::new();
7195        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7196
7197        for s in &symbols {
7198            assert!(
7199                s.range.end_line >= s.range.start_line,
7200                "symbol {} has invalid range: {:?}",
7201                s.name,
7202                s.range
7203            );
7204        }
7205    }
7206
7207    // --- Rust extraction ---
7208
7209    #[test]
7210    fn rs_extracts_all_symbols() {
7211        let provider = TreeSitterProvider::new();
7212        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7213
7214        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7215        assert!(
7216            names.contains(&"public_function"),
7217            "missing public_function: {:?}",
7218            names
7219        );
7220        assert!(
7221            names.contains(&"private_function"),
7222            "missing private_function: {:?}",
7223            names
7224        );
7225        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
7226        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
7227        assert!(
7228            names.contains(&"Drawable"),
7229            "missing trait Drawable: {:?}",
7230            names
7231        );
7232        // impl methods
7233        assert!(
7234            names.contains(&"new"),
7235            "missing impl method new: {:?}",
7236            names
7237        );
7238
7239        // Plan requires ≥6 symbols
7240        assert!(
7241            symbols.len() >= 6,
7242            "expected ≥6 symbols, got {}: {:?}",
7243            symbols.len(),
7244            names
7245        );
7246    }
7247
7248    #[test]
7249    fn rs_symbol_kinds_correct() {
7250        let provider = TreeSitterProvider::new();
7251        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7252
7253        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7254
7255        assert_eq!(find("public_function").kind, SymbolKind::Function);
7256        assert_eq!(find("private_function").kind, SymbolKind::Function);
7257        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
7258        assert_eq!(find("Color").kind, SymbolKind::Enum);
7259        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
7260        assert_eq!(find("new").kind, SymbolKind::Method);
7261    }
7262
7263    #[test]
7264    fn rs_pub_export_detection() {
7265        let provider = TreeSitterProvider::new();
7266        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7267
7268        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7269
7270        assert!(
7271            find("public_function").exported,
7272            "pub fn should be exported"
7273        );
7274        assert!(
7275            !find("private_function").exported,
7276            "non-pub fn should not be exported"
7277        );
7278        assert!(find("MyStruct").exported, "pub struct should be exported");
7279        assert!(find("Color").exported, "pub enum should be exported");
7280        assert!(find("Drawable").exported, "pub trait should be exported");
7281        assert!(
7282            find("new").exported,
7283            "pub fn inside impl should be exported"
7284        );
7285        assert!(
7286            !find("helper").exported,
7287            "non-pub fn inside impl should not be exported"
7288        );
7289    }
7290
7291    #[test]
7292    fn rs_impl_method_scope_chain() {
7293        let provider = TreeSitterProvider::new();
7294        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7295
7296        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7297
7298        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
7299        assert_eq!(
7300            find("new").scope_chain,
7301            vec!["MyStruct"],
7302            "impl method should have type in scope chain"
7303        );
7304        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
7305
7306        // Free function has empty scope chain
7307        assert!(
7308            find("public_function").scope_chain.is_empty(),
7309            "free function should have empty scope chain"
7310        );
7311    }
7312
7313    #[test]
7314    fn rs_trait_impl_scope_chain() {
7315        let provider = TreeSitterProvider::new();
7316        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7317
7318        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
7319        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
7320        assert_eq!(
7321            draw.scope_chain,
7322            vec!["Drawable for MyStruct"],
7323            "trait impl method should have 'Trait for Type' scope"
7324        );
7325        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
7326    }
7327
7328    #[test]
7329    fn rs_ranges_valid() {
7330        let provider = TreeSitterProvider::new();
7331        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7332
7333        for s in &symbols {
7334            assert!(
7335                s.range.end_line >= s.range.start_line,
7336                "symbol {} has invalid range: {:?}",
7337                s.name,
7338                s.range
7339            );
7340        }
7341    }
7342
7343    // --- Go extraction ---
7344
7345    #[test]
7346    fn go_extracts_all_symbols() {
7347        let provider = TreeSitterProvider::new();
7348        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7349
7350        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7351        assert!(
7352            names.contains(&"ExportedFunction"),
7353            "missing ExportedFunction: {:?}",
7354            names
7355        );
7356        assert!(
7357            names.contains(&"unexportedFunction"),
7358            "missing unexportedFunction: {:?}",
7359            names
7360        );
7361        assert!(
7362            names.contains(&"MyStruct"),
7363            "missing struct MyStruct: {:?}",
7364            names
7365        );
7366        assert!(
7367            names.contains(&"Reader"),
7368            "missing interface Reader: {:?}",
7369            names
7370        );
7371        // receiver method
7372        assert!(
7373            names.contains(&"String"),
7374            "missing receiver method String: {:?}",
7375            names
7376        );
7377
7378        // Plan requires ≥4 symbols
7379        assert!(
7380            symbols.len() >= 4,
7381            "expected ≥4 symbols, got {}: {:?}",
7382            symbols.len(),
7383            names
7384        );
7385    }
7386
7387    #[test]
7388    fn go_symbol_kinds_correct() {
7389        let provider = TreeSitterProvider::new();
7390        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7391
7392        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7393
7394        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
7395        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
7396        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
7397        assert_eq!(find("Reader").kind, SymbolKind::Interface);
7398        assert_eq!(find("String").kind, SymbolKind::Method);
7399        assert_eq!(find("helper").kind, SymbolKind::Method);
7400    }
7401
7402    #[test]
7403    fn go_uppercase_export_detection() {
7404        let provider = TreeSitterProvider::new();
7405        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7406
7407        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7408
7409        assert!(
7410            find("ExportedFunction").exported,
7411            "ExportedFunction (uppercase) should be exported"
7412        );
7413        assert!(
7414            !find("unexportedFunction").exported,
7415            "unexportedFunction (lowercase) should not be exported"
7416        );
7417        assert!(
7418            find("MyStruct").exported,
7419            "MyStruct (uppercase) should be exported"
7420        );
7421        assert!(
7422            find("Reader").exported,
7423            "Reader (uppercase) should be exported"
7424        );
7425        assert!(
7426            find("String").exported,
7427            "String method (uppercase) should be exported"
7428        );
7429        assert!(
7430            !find("helper").exported,
7431            "helper method (lowercase) should not be exported"
7432        );
7433    }
7434
7435    #[test]
7436    fn go_receiver_method_scope_chain() {
7437        let provider = TreeSitterProvider::new();
7438        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7439
7440        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7441
7442        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
7443        assert_eq!(
7444            find("String").scope_chain,
7445            vec!["MyStruct"],
7446            "receiver method should have type in scope chain"
7447        );
7448        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
7449
7450        // Regular function has empty scope chain
7451        assert!(
7452            find("ExportedFunction").scope_chain.is_empty(),
7453            "regular function should have empty scope chain"
7454        );
7455    }
7456
7457    #[test]
7458    fn go_ranges_valid() {
7459        let provider = TreeSitterProvider::new();
7460        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7461
7462        for s in &symbols {
7463            assert!(
7464                s.range.end_line >= s.range.start_line,
7465                "symbol {} has invalid range: {:?}",
7466                s.name,
7467                s.range
7468            );
7469        }
7470    }
7471
7472    // --- Cross-language ---
7473
7474    #[test]
7475    fn cross_language_all_six_produce_symbols() {
7476        let provider = TreeSitterProvider::new();
7477
7478        let fixtures = [
7479            ("sample.ts", "TypeScript"),
7480            ("sample.tsx", "TSX"),
7481            ("sample.js", "JavaScript"),
7482            ("sample.py", "Python"),
7483            ("sample.rs", "Rust"),
7484            ("sample.go", "Go"),
7485        ];
7486
7487        for (fixture, lang) in &fixtures {
7488            let symbols = provider
7489                .list_symbols(&fixture_path(fixture))
7490                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
7491            assert!(
7492                symbols.len() >= 2,
7493                "{} should produce ≥2 symbols, got {}: {:?}",
7494                lang,
7495                symbols.len(),
7496                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
7497            );
7498        }
7499    }
7500
7501    // --- Symbol cache tests ---
7502
7503    #[test]
7504    fn symbol_cache_returns_cached_results_on_second_call() {
7505        let dir = tempfile::tempdir().unwrap();
7506        let file = dir.path().join("test.rs");
7507        std::fs::write(&file, "pub fn hello() {}\npub fn world() {}").unwrap();
7508
7509        let mut parser = FileParser::new();
7510
7511        let symbols1 = parser.extract_symbols(&file).unwrap();
7512        assert_eq!(symbols1.len(), 2);
7513
7514        // Second call should return cached result
7515        let symbols2 = parser.extract_symbols(&file).unwrap();
7516        assert_eq!(symbols2.len(), 2);
7517        assert_eq!(symbols1[0].name, symbols2[0].name);
7518
7519        // Verify cache is populated
7520        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
7521    }
7522
7523    #[test]
7524    fn symbol_cache_invalidates_on_file_change() {
7525        let dir = tempfile::tempdir().unwrap();
7526        let file = dir.path().join("test.rs");
7527        std::fs::write(&file, "pub fn hello() {}").unwrap();
7528
7529        let mut parser = FileParser::new();
7530
7531        let symbols1 = parser.extract_symbols(&file).unwrap();
7532        assert_eq!(symbols1.len(), 1);
7533        assert_eq!(symbols1[0].name, "hello");
7534
7535        // Wait to ensure mtime changes (filesystem resolution can be 1s on some OS)
7536        std::thread::sleep(std::time::Duration::from_millis(50));
7537
7538        // Modify file — add a second function
7539        std::fs::write(&file, "pub fn hello() {}\npub fn goodbye() {}").unwrap();
7540
7541        // Should detect mtime change and re-extract
7542        let symbols2 = parser.extract_symbols(&file).unwrap();
7543        assert_eq!(symbols2.len(), 2);
7544        assert!(symbols2.iter().any(|s| s.name == "goodbye"));
7545    }
7546
7547    #[test]
7548    fn symbol_cache_invalidate_method_clears_entry() {
7549        let dir = tempfile::tempdir().unwrap();
7550        let file = dir.path().join("test.rs");
7551        std::fs::write(&file, "pub fn hello() {}").unwrap();
7552
7553        let mut parser = FileParser::new();
7554        parser.extract_symbols(&file).unwrap();
7555        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
7556
7557        parser.invalidate_symbols(&file);
7558        assert!(!parser.symbol_cache.read().unwrap().contains_key(&file));
7559        // Parse tree cache should also be cleared
7560        assert!(!parser.cache.contains_key(&file));
7561    }
7562
7563    #[test]
7564    fn symbol_cache_works_for_multiple_languages() {
7565        let dir = tempfile::tempdir().unwrap();
7566        let rs_file = dir.path().join("lib.rs");
7567        let ts_file = dir.path().join("app.ts");
7568        let py_file = dir.path().join("main.py");
7569
7570        std::fs::write(&rs_file, "pub fn rust_fn() {}").unwrap();
7571        std::fs::write(&ts_file, "export function tsFn() {}").unwrap();
7572        std::fs::write(&py_file, "def py_fn():\n    pass").unwrap();
7573
7574        let mut parser = FileParser::new();
7575
7576        let rs_syms = parser.extract_symbols(&rs_file).unwrap();
7577        let ts_syms = parser.extract_symbols(&ts_file).unwrap();
7578        let py_syms = parser.extract_symbols(&py_file).unwrap();
7579
7580        assert!(rs_syms.iter().any(|s| s.name == "rust_fn"));
7581        assert!(ts_syms.iter().any(|s| s.name == "tsFn"));
7582        assert!(py_syms.iter().any(|s| s.name == "py_fn"));
7583
7584        // All should be cached now
7585        assert_eq!(parser.symbol_cache.read().unwrap().len(), 3);
7586
7587        // Re-extract should return same results from cache
7588        let rs_syms2 = parser.extract_symbols(&rs_file).unwrap();
7589        assert_eq!(rs_syms.len(), rs_syms2.len());
7590    }
7591
7592    #[test]
7593    fn extract_json_symbols_top_level_keys() {
7594        let dir = tempfile::tempdir().unwrap();
7595        let file = dir.path().join("package.json");
7596        std::fs::write(&file, r#"{"name": "x", "version": "1"}"#).unwrap();
7597
7598        let mut parser = FileParser::new();
7599        let symbols = parser.extract_symbols(&file).unwrap();
7600
7601        assert_eq!(symbols.len(), 2);
7602        assert!(symbols
7603            .iter()
7604            .any(|s| s.name == "name" && s.kind == SymbolKind::Variable));
7605        assert!(symbols
7606            .iter()
7607            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
7608    }
7609
7610    #[test]
7611    fn extract_json_symbols_root_array() {
7612        let dir = tempfile::tempdir().unwrap();
7613        let file = dir.path().join("array.json");
7614        std::fs::write(&file, "[1,2,3]").unwrap();
7615
7616        let mut parser = FileParser::new();
7617        let symbols = parser.extract_symbols(&file).unwrap();
7618
7619        assert_eq!(symbols.len(), 0);
7620    }
7621
7622    #[test]
7623    fn extract_json_symbols_no_recursion_into_nested() {
7624        let dir = tempfile::tempdir().unwrap();
7625        let file = dir.path().join("nested.json");
7626        std::fs::write(&file, r#"{"scripts": {"build": "tsc"}}"#).unwrap();
7627
7628        let mut parser = FileParser::new();
7629        let symbols = parser.extract_symbols(&file).unwrap();
7630
7631        assert_eq!(symbols.len(), 1);
7632        assert_eq!(symbols[0].name, "scripts");
7633        assert!(!symbols.iter().any(|s| s.name == "build"));
7634    }
7635
7636    #[test]
7637    fn extract_scala_symbols_object_and_method() {
7638        let dir = tempfile::tempdir().unwrap();
7639        let file = dir.path().join("Greeter.scala");
7640        std::fs::write(
7641            &file,
7642            "object Greeter {\n  def hello(name: String): String = s\"hi $name\"\n}",
7643        )
7644        .unwrap();
7645
7646        let mut parser = FileParser::new();
7647        let symbols = parser.extract_symbols(&file).unwrap();
7648
7649        assert!(symbols
7650            .iter()
7651            .any(|s| s.name == "Greeter" && s.kind == SymbolKind::Class));
7652        assert!(symbols.iter().any(|s| s.name == "hello"
7653            && s.kind == SymbolKind::Method
7654            && s.scope_chain == vec!["Greeter".to_string()]));
7655    }
7656
7657    #[test]
7658    fn extract_scala_symbols_class_and_trait() {
7659        let dir = tempfile::tempdir().unwrap();
7660        let file = dir.path().join("Types.scala");
7661        std::fs::write(&file, "class Foo\ntrait Bar").unwrap();
7662
7663        let mut parser = FileParser::new();
7664        let symbols = parser.extract_symbols(&file).unwrap();
7665
7666        assert!(symbols
7667            .iter()
7668            .any(|s| s.name == "Foo" && s.kind == SymbolKind::Class));
7669        assert!(symbols
7670            .iter()
7671            .any(|s| s.name == "Bar" && s.kind == SymbolKind::Interface));
7672    }
7673
7674    #[test]
7675    fn extract_yaml_symbols_k8s_resource() {
7676        let dir = tempfile::tempdir().unwrap();
7677        let file = dir.path().join("deployment.yaml");
7678        std::fs::write(
7679            &file,
7680            r#"apiVersion: apps/v1
7681kind: Deployment
7682metadata:
7683  name: nginx
7684  namespace: web
7685"#,
7686        )
7687        .unwrap();
7688
7689        let mut parser = FileParser::new();
7690        let symbols = parser.extract_symbols(&file).unwrap();
7691
7692        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Deployment");
7693        let sym = &symbols[0];
7694        assert_eq!(sym.name, "web/Deployment/nginx");
7695        assert_eq!(sym.kind, SymbolKind::Class);
7696        assert!(sym.exported);
7697    }
7698
7699    #[test]
7700    fn extract_yaml_symbols_k8s_no_namespace() {
7701        let dir = tempfile::tempdir().unwrap();
7702        let file = dir.path().join("service.yaml");
7703        std::fs::write(
7704            &file,
7705            r#"apiVersion: v1
7706kind: Service
7707metadata:
7708  name: nginx-svc
7709"#,
7710        )
7711        .unwrap();
7712
7713        let mut parser = FileParser::new();
7714        let symbols = parser.extract_symbols(&file).unwrap();
7715
7716        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Service");
7717        let sym = &symbols[0];
7718        assert_eq!(sym.name, "Service/nginx-svc");
7719        assert_eq!(sym.kind, SymbolKind::Class);
7720    }
7721
7722    #[test]
7723    fn extract_yaml_symbols_multidoc() {
7724        let dir = tempfile::tempdir().unwrap();
7725        let file = dir.path().join("multidoc.yaml");
7726        std::fs::write(
7727            &file,
7728            r#"apiVersion: apps/v1
7729kind: Deployment
7730metadata:
7731  name: a
7732---
7733apiVersion: v1
7734kind: Service
7735metadata:
7736  name: b
7737"#,
7738        )
7739        .unwrap();
7740
7741        let mut parser = FileParser::new();
7742        let symbols = parser.extract_symbols(&file).unwrap();
7743
7744        assert_eq!(symbols.len(), 2, "Expected 2 symbols for multi-doc YAML");
7745        assert!(symbols.iter().any(|s| s.name == "Deployment/a"));
7746        assert!(symbols.iter().any(|s| s.name == "Service/b"));
7747    }
7748
7749    #[test]
7750    fn extract_yaml_symbols_generic_fallback() {
7751        let dir = tempfile::tempdir().unwrap();
7752        let file = dir.path().join("compose.yaml");
7753        std::fs::write(
7754            &file,
7755            r#"version: "3"
7756services:
7757  web: {}
7758volumes:
7759  data: {}
7760"#,
7761        )
7762        .unwrap();
7763
7764        let mut parser = FileParser::new();
7765        let symbols = parser.extract_symbols(&file).unwrap();
7766
7767        // Should have top-level keys: version, services, volumes
7768        assert_eq!(symbols.len(), 3, "Expected 3 symbols for generic YAML");
7769        assert!(symbols
7770            .iter()
7771            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
7772        assert!(symbols
7773            .iter()
7774            .any(|s| s.name == "services" && s.kind == SymbolKind::Variable));
7775        assert!(symbols
7776            .iter()
7777            .any(|s| s.name == "volumes" && s.kind == SymbolKind::Variable));
7778    }
7779
7780    #[test]
7781    fn extract_yaml_symbols_empty() {
7782        let dir = tempfile::tempdir().unwrap();
7783        let file = dir.path().join("empty.yaml");
7784        std::fs::write(&file, "").unwrap();
7785
7786        let mut parser = FileParser::new();
7787        let symbols = parser.extract_symbols(&file).unwrap();
7788
7789        assert_eq!(symbols.len(), 0, "Expected 0 symbols for empty YAML");
7790    }
7791
7792    #[test]
7793    fn extract_yaml_symbols_resource_limits() {
7794        let dir = tempfile::tempdir().unwrap();
7795        let file = dir.path().join("deployment.yaml");
7796        std::fs::write(
7797            &file,
7798            r#"apiVersion: apps/v1
7799kind: Deployment
7800metadata:
7801  name: app
7802spec:
7803  template:
7804    spec:
7805      containers:
7806      - name: main
7807        image: myapp:1.0
7808        resources:
7809          limits:
7810            cpu: "2"
7811            memory: 1Gi
7812          requests:
7813            cpu: "1"
7814            memory: 512Mi
7815"#,
7816        )
7817        .unwrap();
7818
7819        let mut parser = FileParser::new();
7820        let symbols = parser.extract_symbols(&file).unwrap();
7821
7822        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
7823        let sym = &symbols[0];
7824        assert_eq!(sym.name, "Deployment/app");
7825        let sig = sym.signature.as_ref().unwrap();
7826        assert!(sig.contains("cpu="), "Signature should contain cpu= field");
7827        assert!(
7828            sig.contains("memory="),
7829            "Signature should contain memory= field"
7830        );
7831    }
7832
7833    #[test]
7834    fn extract_yaml_symbols_env_names() {
7835        let dir = tempfile::tempdir().unwrap();
7836        let file = dir.path().join("deployment.yaml");
7837        std::fs::write(
7838            &file,
7839            r#"apiVersion: apps/v1
7840kind: Deployment
7841metadata:
7842  name: app
7843spec:
7844  template:
7845    spec:
7846      containers:
7847      - name: main
7848        image: myapp:1.0
7849        env:
7850        - name: FOO
7851          value: "bar"
7852        - name: BAR
7853          value: "baz"
7854"#,
7855        )
7856        .unwrap();
7857
7858        let mut parser = FileParser::new();
7859        let symbols = parser.extract_symbols(&file).unwrap();
7860
7861        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
7862        let sym = &symbols[0];
7863        let sig = sym.signature.as_ref().unwrap();
7864        assert!(
7865            sig.contains("env=FOO,BAR"),
7866            "Signature should contain env=FOO,BAR"
7867        );
7868    }
7869
7870    #[test]
7871    fn extract_yaml_symbols_rbac_rules() {
7872        let dir = tempfile::tempdir().unwrap();
7873        let file = dir.path().join("role.yaml");
7874        std::fs::write(
7875            &file,
7876            r#"apiVersion: rbac.authorization.k8s.io/v1
7877kind: Role
7878metadata:
7879  name: reader
7880rules:
7881- apiGroups: [""]
7882  resources: [pods, services]
7883  verbs: [get, list, watch]
7884"#,
7885        )
7886        .unwrap();
7887
7888        let mut parser = FileParser::new();
7889        let symbols = parser.extract_symbols(&file).unwrap();
7890
7891        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Role");
7892        let sym = &symbols[0];
7893        let sig = sym.signature.as_ref().unwrap();
7894        assert!(
7895            sig.contains("verbs=get,list,watch"),
7896            "Signature should contain verbs=get,list,watch"
7897        );
7898        assert!(
7899            sig.contains("resources=pods,services"),
7900            "Signature should contain resources=pods,services"
7901        );
7902    }
7903
7904    #[test]
7905    fn extract_yaml_symbols_argo_workflow() {
7906        let dir = tempfile::tempdir().unwrap();
7907        let file = dir.path().join("workflow.yaml");
7908        std::fs::write(
7909            &file,
7910            r#"apiVersion: argoproj.io/v1alpha1
7911kind: Workflow
7912metadata:
7913  name: hello-world
7914spec:
7915  entrypoint: main
7916  templates:
7917  - name: main
7918    container:
7919      image: alpine:3.18
7920      command: [echo]
7921      args: ["hello"]
7922  - name: print
7923    container:
7924      image: alpine:3.18
7925      command: [echo]
7926      args: ["world"]
7927"#,
7928        )
7929        .unwrap();
7930
7931        let mut parser = FileParser::new();
7932        let symbols = parser.extract_symbols(&file).unwrap();
7933
7934        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
7935        let sym = &symbols[0];
7936        assert!(
7937            sym.name.contains("Workflow"),
7938            "Symbol name should contain Workflow"
7939        );
7940        let sig = sym.signature.as_ref().unwrap();
7941        assert!(
7942            sig.contains("entrypoint=main"),
7943            "Signature should contain entrypoint=main"
7944        );
7945        assert!(
7946            sig.contains("templates=main,print"),
7947            "Signature should contain templates=main,print"
7948        );
7949        assert!(
7950            sig.contains("image=alpine:3.18"),
7951            "Signature should contain image=alpine:3.18"
7952        );
7953        assert!(
7954            sig.contains("command=echo"),
7955            "Signature should contain command=echo"
7956        );
7957    }
7958
7959    #[test]
7960    fn extract_yaml_symbols_generatename_fallback() {
7961        let dir = tempfile::tempdir().unwrap();
7962        let file = dir.path().join("workflow.yaml");
7963        std::fs::write(
7964            &file,
7965            r#"apiVersion: argoproj.io/v1alpha1
7966kind: Workflow
7967metadata:
7968  generateName: hello-
7969spec:
7970  entrypoint: main
7971  templates:
7972  - name: main
7973    container:
7974      image: alpine:3.18
7975"#,
7976        )
7977        .unwrap();
7978
7979        let mut parser = FileParser::new();
7980        let symbols = parser.extract_symbols(&file).unwrap();
7981
7982        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
7983        let sym = &symbols[0];
7984        assert!(
7985            sym.name.contains("hello-"),
7986            "Symbol name should contain generateName fallback 'hello-'"
7987        );
7988    }
7989}