Skip to main content

aft/
parser.rs

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