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.name == name)
6581        .cloned()
6582        .map(|symbol| SymbolMatch {
6583            file: file.display().to_string(),
6584            symbol,
6585        })
6586        .collect()
6587}
6588
6589fn string_content(source: &str, node: &Node) -> Option<String> {
6590    let text = node_text(source, node);
6591    if text.len() < 2 {
6592        return None;
6593    }
6594
6595    Some(
6596        text.trim_start_matches(|c| c == '\'' || c == '"')
6597            .trim_end_matches(|c| c == '\'' || c == '"')
6598            .to_string(),
6599    )
6600}
6601
6602fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
6603    let mut cursor = node.walk();
6604    if !cursor.goto_first_child() {
6605        return None;
6606    }
6607
6608    loop {
6609        let child = cursor.node();
6610        if child.kind() == kind {
6611            return Some(child);
6612        }
6613        if !cursor.goto_next_sibling() {
6614            break;
6615        }
6616    }
6617
6618    None
6619}
6620
6621fn resolve_export_clause_name(
6622    source: &str,
6623    export_clause: &Node,
6624    requested_name: &str,
6625) -> Option<String> {
6626    let mut cursor = export_clause.walk();
6627    if !cursor.goto_first_child() {
6628        return None;
6629    }
6630
6631    loop {
6632        let child = cursor.node();
6633        if child.kind() == "export_specifier" {
6634            let (source_name, exported_name) = export_specifier_names(source, &child)?;
6635            if exported_name == requested_name {
6636                return Some(source_name);
6637            }
6638        }
6639
6640        if !cursor.goto_next_sibling() {
6641            break;
6642        }
6643    }
6644
6645    None
6646}
6647
6648fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
6649    let source_name = specifier
6650        .child_by_field_name("name")
6651        .map(|node| node_text(source, &node).to_string());
6652    let alias_name = specifier
6653        .child_by_field_name("alias")
6654        .map(|node| node_text(source, &node).to_string());
6655
6656    if let Some(source_name) = source_name {
6657        let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
6658        return Some((source_name, exported_name));
6659    }
6660
6661    let mut names = Vec::new();
6662    let mut cursor = specifier.walk();
6663    if cursor.goto_first_child() {
6664        loop {
6665            let child = cursor.node();
6666            let child_text = node_text(source, &child).trim();
6667            if matches!(
6668                child.kind(),
6669                "identifier" | "type_identifier" | "property_identifier"
6670            ) || child_text == "default"
6671            {
6672                names.push(child_text.to_string());
6673            }
6674            if !cursor.goto_next_sibling() {
6675                break;
6676            }
6677        }
6678    }
6679
6680    match names.as_slice() {
6681        [name] => Some((name.clone(), name.clone())),
6682        [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
6683        _ => None,
6684    }
6685}
6686
6687fn export_statement_has_wildcard(source: &str, node: &Node) -> bool {
6688    let mut cursor = node.walk();
6689    if !cursor.goto_first_child() {
6690        return false;
6691    }
6692
6693    loop {
6694        if node_text(source, &cursor.node()).trim() == "*" {
6695            return true;
6696        }
6697        if !cursor.goto_next_sibling() {
6698            break;
6699        }
6700    }
6701
6702    false
6703}
6704
6705fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
6706    let mut cursor = node.walk();
6707    if !cursor.goto_first_child() {
6708        return false;
6709    }
6710
6711    loop {
6712        if node_text(source, &cursor.node()).trim() == token {
6713            return true;
6714        }
6715        if !cursor.goto_next_sibling() {
6716            break;
6717        }
6718    }
6719
6720    false
6721}
6722
6723fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
6724    if let Some(value_node) = export_stmt.child_by_field_name("value") {
6725        if let Some(name) = default_export_node_name(source, &value_node) {
6726            return Some(name);
6727        }
6728    }
6729
6730    if let Some(declaration_node) = export_stmt.child_by_field_name("declaration") {
6731        if let Some(name) = default_export_node_name(source, &declaration_node) {
6732            return Some(name);
6733        }
6734    }
6735
6736    let mut cursor = export_stmt.walk();
6737    if !cursor.goto_first_child() {
6738        return None;
6739    }
6740
6741    loop {
6742        let child = cursor.node();
6743        if let Some(name) = default_export_node_name(source, &child) {
6744            return Some(name);
6745        }
6746
6747        if !cursor.goto_next_sibling() {
6748            break;
6749        }
6750    }
6751
6752    None
6753}
6754
6755fn default_export_node_name(source: &str, node: &Node) -> Option<String> {
6756    match node.kind() {
6757        "function_declaration"
6758        | "generator_function_declaration"
6759        | "function_expression"
6760        | "generator_function"
6761        | "class_declaration"
6762        | "class" => node
6763            .child_by_field_name("name")
6764            .map(|name_node| node_text(source, &name_node).to_string())
6765            .or_else(|| Some("default".to_string())),
6766        "interface_declaration" | "enum_declaration" | "type_alias_declaration" => node
6767            .child_by_field_name("name")
6768            .map(|name_node| node_text(source, &name_node).to_string()),
6769        "lexical_declaration" => lexical_declaration_name(source, node),
6770        "identifier" | "type_identifier" => {
6771            let text = node_text(source, node);
6772            (text != "export" && text != "default").then(|| text.to_string())
6773        }
6774        _ => None,
6775    }
6776}
6777
6778fn lexical_declaration_name(source: &str, node: &Node) -> Option<String> {
6779    let mut cursor = node.walk();
6780    if !cursor.goto_first_child() {
6781        return None;
6782    }
6783
6784    loop {
6785        let child = cursor.node();
6786        if child.kind() == "variable_declarator" {
6787            if let Some(name_node) = child.child_by_field_name("name") {
6788                return Some(node_text(source, &name_node).to_string());
6789            }
6790        }
6791        if !cursor.goto_next_sibling() {
6792            break;
6793        }
6794    }
6795
6796    None
6797}
6798
6799impl crate::language::LanguageProvider for TreeSitterProvider {
6800    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
6801        let mut parser = FileParser::with_symbol_cache(self.symbol_cache());
6802        let matches = self.resolve_symbol_inner(&mut parser, file, name, 0, &mut HashSet::new())?;
6803
6804        if matches.is_empty() {
6805            Err(AftError::SymbolNotFound {
6806                name: name.to_string(),
6807                file: file.display().to_string(),
6808            })
6809        } else {
6810            Ok(matches)
6811        }
6812    }
6813
6814    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
6815        let mut parser = FileParser::with_symbol_cache(self.symbol_cache());
6816        parser.extract_symbols(file)
6817    }
6818
6819    fn as_any(&self) -> &dyn std::any::Any {
6820        self
6821    }
6822}
6823
6824#[cfg(test)]
6825mod tests {
6826    use super::*;
6827    use crate::language::LanguageProvider;
6828    use crate::symbol_cache_disk;
6829    use std::path::{Path, PathBuf};
6830
6831    fn fixture_path(name: &str) -> PathBuf {
6832        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6833            .join("tests")
6834            .join("fixtures")
6835            .join(name)
6836    }
6837
6838    fn test_symbol(name: &str) -> Symbol {
6839        Symbol {
6840            name: name.to_string(),
6841            kind: SymbolKind::Function,
6842            range: Range {
6843                start_line: 0,
6844                start_col: 0,
6845                end_line: 0,
6846                end_col: 10,
6847            },
6848            signature: Some(format!("fn {name}()")),
6849            scope_chain: Vec::new(),
6850            exported: true,
6851            parent: None,
6852        }
6853    }
6854
6855    #[test]
6856    fn inc_and_scss_extensions_are_detected() {
6857        assert_eq!(
6858            detect_language(Path::new("template.inc")),
6859            Some(LangId::Php)
6860        );
6861        assert_eq!(
6862            detect_language(Path::new("styles.scss")),
6863            Some(LangId::Scss)
6864        );
6865        assert_eq!(detect_language(Path::new("main.pas")), Some(LangId::Pascal));
6866        assert_eq!(detect_language(Path::new("main.pp")), Some(LangId::Pascal));
6867        assert_eq!(detect_language(Path::new("main.dpr")), Some(LangId::Pascal));
6868        assert_eq!(detect_language(Path::new("main.dpk")), Some(LangId::Pascal));
6869        assert_eq!(detect_language(Path::new("main.lpr")), Some(LangId::Pascal));
6870        assert_eq!(detect_language(Path::new("template.tpl")), None);
6871    }
6872
6873    #[test]
6874    fn inc_files_parse_with_php_grammar() {
6875        let tmp = tempfile::tempdir().expect("create temp dir");
6876        let file = tmp.path().join("partial.inc");
6877        std::fs::write(&file, "<?php\nfunction render_partial() { return 1; }\n")
6878            .expect("write inc file");
6879
6880        let mut parser = FileParser::new();
6881        let symbols = parser
6882            .extract_symbols(&file)
6883            .expect("extract php inc symbols");
6884        let function = symbols
6885            .iter()
6886            .find(|symbol| symbol.name == "render_partial")
6887            .expect("find PHP function in .inc");
6888        assert_eq!(function.kind, SymbolKind::Function);
6889    }
6890
6891    #[test]
6892    fn scss_symbols_include_mixin_variable_function_and_rule() {
6893        let tmp = tempfile::tempdir().expect("create temp dir");
6894        let file = tmp.path().join("styles.scss");
6895        std::fs::write(
6896            &file,
6897            r#"$brand-color: #336699;
6898
6899@mixin button-base($padding) {
6900  padding: $padding;
6901}
6902
6903@function double($value) {
6904  @return $value * 2;
6905}
6906
6907.card, .panel {
6908  color: $brand-color;
6909}
6910"#,
6911        )
6912        .expect("write scss file");
6913
6914        let mut parser = FileParser::new();
6915        let symbols = parser.extract_symbols(&file).expect("extract scss symbols");
6916        let get = |name: &str| {
6917            symbols
6918                .iter()
6919                .find(|symbol| symbol.name == name)
6920                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
6921        };
6922
6923        assert_eq!(get("button-base").kind, SymbolKind::Function);
6924        assert_eq!(get("double").kind, SymbolKind::Function);
6925        assert_eq!(get("$brand-color").kind, SymbolKind::Variable);
6926        assert_eq!(get(".card, .panel").kind, SymbolKind::Class);
6927    }
6928
6929    #[test]
6930    fn symbol_cache_load_from_disk_round_trips_synthetic_entry() {
6931        let project = tempfile::tempdir().expect("create project dir");
6932        let storage = tempfile::tempdir().expect("create storage dir");
6933        let source = project.path().join("src/lib.rs");
6934        std::fs::create_dir_all(source.parent().expect("source parent"))
6935            .expect("create source dir");
6936        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
6937        let mtime = std::fs::metadata(&source)
6938            .expect("stat source")
6939            .modified()
6940            .expect("source mtime");
6941        let content = std::fs::read(&source).expect("read source");
6942        let size = content.len() as u64;
6943        let hash = crate::cache_freshness::hash_bytes(&content);
6944
6945        let mut cache = SymbolCache::new();
6946        cache.set_project_root(project.path().to_path_buf());
6947        cache.insert(
6948            source.clone(),
6949            mtime,
6950            size,
6951            hash,
6952            vec![test_symbol("cached")],
6953        );
6954        symbol_cache_disk::write_to_disk(&cache, storage.path(), "unit-project")
6955            .expect("write symbol cache");
6956
6957        let mut restored = SymbolCache::new();
6958        let loaded = restored.load_from_disk(storage.path(), "unit-project", project.path());
6959        let symbols = restored.get(&source, mtime).expect("restored symbols");
6960
6961        assert_eq!(loaded, 1);
6962        assert_eq!(symbols.len(), 1);
6963        assert_eq!(symbols[0].name, "cached");
6964    }
6965
6966    #[test]
6967    fn symbol_cache_load_from_disk_drops_stale_synthetic_entry() {
6968        let project = tempfile::tempdir().expect("create project dir");
6969        let storage = tempfile::tempdir().expect("create storage dir");
6970        let source = project.path().join("src/lib.rs");
6971        std::fs::create_dir_all(source.parent().expect("source parent"))
6972            .expect("create source dir");
6973        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
6974        let mtime = std::fs::metadata(&source)
6975            .expect("stat source")
6976            .modified()
6977            .expect("source mtime");
6978        let content = std::fs::read(&source).expect("read source");
6979        let size = content.len() as u64;
6980        let hash = crate::cache_freshness::hash_bytes(&content);
6981
6982        let mut cache = SymbolCache::new();
6983        cache.set_project_root(project.path().to_path_buf());
6984        cache.insert(
6985            source.clone(),
6986            mtime,
6987            size,
6988            hash,
6989            vec![test_symbol("cached")],
6990        );
6991        symbol_cache_disk::write_to_disk(&cache, storage.path(), "stale-unit-project")
6992            .expect("write symbol cache");
6993
6994        std::fs::write(&source, "pub fn cached() {}\npub fn fresh() {}\n").expect("change source");
6995
6996        let mut restored = SymbolCache::new();
6997        let loaded = restored.load_from_disk(storage.path(), "stale-unit-project", project.path());
6998
6999        assert_eq!(loaded, 0);
7000        assert_eq!(restored.len(), 0);
7001    }
7002
7003    #[test]
7004    fn stale_prewarm_generation_cannot_repopulate_symbol_cache_after_reset() {
7005        let project = tempfile::tempdir().expect("create project dir");
7006        let storage = tempfile::tempdir().expect("create storage dir");
7007        let source = project.path().join("src/lib.rs");
7008        std::fs::create_dir_all(source.parent().expect("source parent"))
7009            .expect("create source dir");
7010        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
7011        let mtime = std::fs::metadata(&source)
7012            .expect("stat source")
7013            .modified()
7014            .expect("source mtime");
7015        let content = std::fs::read(&source).expect("read source");
7016        let size = content.len() as u64;
7017        let hash = crate::cache_freshness::hash_bytes(&content);
7018
7019        let mut disk_cache = SymbolCache::new();
7020        disk_cache.set_project_root(project.path().to_path_buf());
7021        disk_cache.insert(
7022            source.clone(),
7023            mtime,
7024            size,
7025            hash,
7026            vec![test_symbol("cached")],
7027        );
7028        symbol_cache_disk::write_to_disk(&disk_cache, storage.path(), "prewarm-reset")
7029            .expect("write symbol cache");
7030
7031        let shared = Arc::new(RwLock::new(SymbolCache::new()));
7032        let stale_generation = shared.write().unwrap().reset();
7033        let active_generation = shared.write().unwrap().reset();
7034        assert_ne!(stale_generation, active_generation);
7035
7036        {
7037            let mut cache = shared.write().unwrap();
7038            assert!(!cache
7039                .set_project_root_for_generation(stale_generation, project.path().to_path_buf()));
7040            assert_eq!(
7041                cache.load_from_disk_for_generation(
7042                    stale_generation,
7043                    storage.path(),
7044                    "prewarm-reset",
7045                    project.path()
7046                ),
7047                0
7048            );
7049        }
7050
7051        let mut stale_parser =
7052            FileParser::with_symbol_cache_generation(Arc::clone(&shared), Some(stale_generation));
7053        stale_parser
7054            .extract_symbols(&source)
7055            .expect("stale prewarm parses source but must not write cache");
7056
7057        let cache = shared.read().unwrap();
7058        assert_eq!(cache.generation(), active_generation);
7059        assert_eq!(cache.len(), 0);
7060        assert!(cache.project_root().is_none());
7061        assert!(!cache.contains_key(&source));
7062    }
7063
7064    // --- Language detection ---
7065
7066    #[test]
7067    fn detect_ts() {
7068        assert_eq!(
7069            detect_language(Path::new("foo.ts")),
7070            Some(LangId::TypeScript)
7071        );
7072    }
7073
7074    #[test]
7075    fn detect_tsx() {
7076        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
7077    }
7078
7079    #[test]
7080    fn detect_js() {
7081        assert_eq!(
7082            detect_language(Path::new("foo.js")),
7083            Some(LangId::JavaScript)
7084        );
7085    }
7086
7087    #[test]
7088    fn detect_jsx() {
7089        assert_eq!(
7090            detect_language(Path::new("foo.jsx")),
7091            Some(LangId::JavaScript)
7092        );
7093    }
7094
7095    #[test]
7096    fn detect_py() {
7097        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
7098    }
7099
7100    #[test]
7101    fn detect_rs() {
7102        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
7103    }
7104
7105    #[test]
7106    fn detect_go() {
7107        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
7108    }
7109
7110    #[test]
7111    fn detect_c() {
7112        assert_eq!(detect_language(Path::new("foo.c")), Some(LangId::C));
7113    }
7114
7115    #[test]
7116    fn detect_h() {
7117        assert_eq!(detect_language(Path::new("foo.h")), Some(LangId::C));
7118    }
7119
7120    #[test]
7121    fn detect_cc() {
7122        assert_eq!(detect_language(Path::new("foo.cc")), Some(LangId::Cpp));
7123    }
7124
7125    #[test]
7126    fn detect_cpp() {
7127        assert_eq!(detect_language(Path::new("foo.cpp")), Some(LangId::Cpp));
7128    }
7129
7130    #[test]
7131    fn detect_cxx() {
7132        assert_eq!(detect_language(Path::new("foo.cxx")), Some(LangId::Cpp));
7133    }
7134
7135    #[test]
7136    fn detect_hpp() {
7137        assert_eq!(detect_language(Path::new("foo.hpp")), Some(LangId::Cpp));
7138    }
7139
7140    #[test]
7141    fn detect_hh() {
7142        assert_eq!(detect_language(Path::new("foo.hh")), Some(LangId::Cpp));
7143    }
7144
7145    #[test]
7146    fn detect_zig() {
7147        assert_eq!(detect_language(Path::new("foo.zig")), Some(LangId::Zig));
7148    }
7149
7150    #[test]
7151    fn detect_cs() {
7152        assert_eq!(detect_language(Path::new("foo.cs")), Some(LangId::CSharp));
7153    }
7154
7155    #[test]
7156    fn detect_unknown_returns_none() {
7157        assert_eq!(detect_language(Path::new("foo.txt")), None);
7158    }
7159
7160    // --- Unsupported extension error ---
7161
7162    #[test]
7163    fn unsupported_extension_returns_invalid_request() {
7164        // Use a file that exists but has an unsupported extension
7165        let path = fixture_path("sample.ts");
7166        let bad_path = path.with_extension("txt");
7167        // Create a dummy file so the error comes from language detection, not I/O
7168        std::fs::write(&bad_path, "hello").unwrap();
7169        let provider = TreeSitterProvider::new();
7170        let result = provider.list_symbols(&bad_path);
7171        std::fs::remove_file(&bad_path).ok();
7172        match result {
7173            Err(AftError::InvalidRequest { message }) => {
7174                assert!(
7175                    message.contains("unsupported file extension"),
7176                    "msg: {}",
7177                    message
7178                );
7179                assert!(message.contains("txt"), "msg: {}", message);
7180            }
7181            other => panic!("expected InvalidRequest, got {:?}", other),
7182        }
7183    }
7184
7185    // --- TypeScript extraction ---
7186
7187    #[test]
7188    fn ts_extracts_all_symbol_kinds() {
7189        let provider = TreeSitterProvider::new();
7190        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7191
7192        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7193        assert!(
7194            names.contains(&"greet"),
7195            "missing function greet: {:?}",
7196            names
7197        );
7198        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
7199        assert!(
7200            names.contains(&"UserService"),
7201            "missing class UserService: {:?}",
7202            names
7203        );
7204        assert!(
7205            names.contains(&"Config"),
7206            "missing interface Config: {:?}",
7207            names
7208        );
7209        assert!(
7210            names.contains(&"Status"),
7211            "missing enum Status: {:?}",
7212            names
7213        );
7214        assert!(
7215            names.contains(&"UserId"),
7216            "missing type alias UserId: {:?}",
7217            names
7218        );
7219        assert!(
7220            names.contains(&"internalHelper"),
7221            "missing non-exported fn: {:?}",
7222            names
7223        );
7224
7225        // At least 6 unique symbols as required
7226        assert!(
7227            symbols.len() >= 6,
7228            "expected ≥6 symbols, got {}: {:?}",
7229            symbols.len(),
7230            names
7231        );
7232    }
7233
7234    #[test]
7235    fn ts_symbol_kinds_correct() {
7236        let provider = TreeSitterProvider::new();
7237        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7238
7239        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7240
7241        assert_eq!(find("greet").kind, SymbolKind::Function);
7242        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
7243        assert_eq!(find("UserService").kind, SymbolKind::Class);
7244        assert_eq!(find("Config").kind, SymbolKind::Interface);
7245        assert_eq!(find("Status").kind, SymbolKind::Enum);
7246        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
7247    }
7248
7249    #[test]
7250    fn ts_export_detection() {
7251        let provider = TreeSitterProvider::new();
7252        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7253
7254        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7255
7256        assert!(find("greet").exported, "greet should be exported");
7257        assert!(find("add").exported, "add should be exported");
7258        assert!(
7259            find("UserService").exported,
7260            "UserService should be exported"
7261        );
7262        assert!(find("Config").exported, "Config should be exported");
7263        assert!(find("Status").exported, "Status should be exported");
7264        assert!(
7265            !find("internalHelper").exported,
7266            "internalHelper should not be exported"
7267        );
7268    }
7269
7270    #[test]
7271    fn ts_method_scope_chain() {
7272        let provider = TreeSitterProvider::new();
7273        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7274
7275        let methods: Vec<&Symbol> = symbols
7276            .iter()
7277            .filter(|s| s.kind == SymbolKind::Method)
7278            .collect();
7279        assert!(!methods.is_empty(), "should have at least one method");
7280
7281        for method in &methods {
7282            assert_eq!(
7283                method.scope_chain,
7284                vec!["UserService"],
7285                "method {} should have UserService in scope chain",
7286                method.name
7287            );
7288            assert_eq!(method.parent.as_deref(), Some("UserService"));
7289        }
7290    }
7291
7292    #[test]
7293    fn ts_signatures_present() {
7294        let provider = TreeSitterProvider::new();
7295        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7296
7297        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7298
7299        let greet_sig = find("greet").signature.as_ref().unwrap();
7300        assert!(
7301            greet_sig.contains("greet"),
7302            "signature should contain function name: {}",
7303            greet_sig
7304        );
7305    }
7306
7307    #[test]
7308    fn ts_ranges_valid() {
7309        let provider = TreeSitterProvider::new();
7310        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
7311
7312        for s in &symbols {
7313            assert!(
7314                s.range.end_line >= s.range.start_line,
7315                "symbol {} has invalid range: {:?}",
7316                s.name,
7317                s.range
7318            );
7319        }
7320    }
7321
7322    // --- JavaScript extraction ---
7323
7324    #[test]
7325    fn js_extracts_core_symbols() {
7326        let provider = TreeSitterProvider::new();
7327        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
7328
7329        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7330        assert!(
7331            names.contains(&"multiply"),
7332            "missing function multiply: {:?}",
7333            names
7334        );
7335        assert!(
7336            names.contains(&"divide"),
7337            "missing arrow fn divide: {:?}",
7338            names
7339        );
7340        assert!(
7341            names.contains(&"EventEmitter"),
7342            "missing class EventEmitter: {:?}",
7343            names
7344        );
7345        assert!(
7346            names.contains(&"main"),
7347            "missing default export fn main: {:?}",
7348            names
7349        );
7350
7351        assert!(
7352            symbols.len() >= 4,
7353            "expected ≥4 symbols, got {}: {:?}",
7354            symbols.len(),
7355            names
7356        );
7357    }
7358
7359    #[test]
7360    fn js_arrow_fn_correctly_named() {
7361        let provider = TreeSitterProvider::new();
7362        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
7363
7364        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
7365        assert_eq!(divide.kind, SymbolKind::Function);
7366        assert!(divide.exported, "divide should be exported");
7367
7368        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
7369        assert_eq!(internal.kind, SymbolKind::Function);
7370        assert!(!internal.exported, "internalUtil should not be exported");
7371    }
7372
7373    #[test]
7374    fn js_method_scope_chain() {
7375        let provider = TreeSitterProvider::new();
7376        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
7377
7378        let methods: Vec<&Symbol> = symbols
7379            .iter()
7380            .filter(|s| s.kind == SymbolKind::Method)
7381            .collect();
7382
7383        for method in &methods {
7384            assert_eq!(
7385                method.scope_chain,
7386                vec!["EventEmitter"],
7387                "method {} should have EventEmitter in scope chain",
7388                method.name
7389            );
7390        }
7391    }
7392
7393    // --- TSX extraction ---
7394
7395    #[test]
7396    fn tsx_extracts_react_component() {
7397        let provider = TreeSitterProvider::new();
7398        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
7399
7400        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7401        assert!(
7402            names.contains(&"Button"),
7403            "missing React component Button: {:?}",
7404            names
7405        );
7406        assert!(
7407            names.contains(&"Counter"),
7408            "missing class Counter: {:?}",
7409            names
7410        );
7411        assert!(
7412            names.contains(&"formatLabel"),
7413            "missing function formatLabel: {:?}",
7414            names
7415        );
7416
7417        assert!(
7418            symbols.len() >= 2,
7419            "expected ≥2 symbols, got {}: {:?}",
7420            symbols.len(),
7421            names
7422        );
7423    }
7424
7425    #[test]
7426    fn tsx_jsx_doesnt_break_parser() {
7427        // Main assertion: TSX grammar handles JSX without errors
7428        let provider = TreeSitterProvider::new();
7429        let result = provider.list_symbols(&fixture_path("sample.tsx"));
7430        assert!(
7431            result.is_ok(),
7432            "TSX parsing should succeed: {:?}",
7433            result.err()
7434        );
7435    }
7436
7437    // --- resolve_symbol ---
7438
7439    #[test]
7440    fn resolve_symbol_finds_match() {
7441        let provider = TreeSitterProvider::new();
7442        let matches = provider
7443            .resolve_symbol(&fixture_path("sample.ts"), "greet")
7444            .unwrap();
7445        assert_eq!(matches.len(), 1);
7446        assert_eq!(matches[0].symbol.name, "greet");
7447        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
7448    }
7449
7450    #[test]
7451    fn resolve_symbol_not_found() {
7452        let provider = TreeSitterProvider::new();
7453        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
7454        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
7455    }
7456
7457    #[test]
7458    fn resolve_symbol_follows_reexport_chains() {
7459        let dir = tempfile::tempdir().unwrap();
7460        let config = dir.path().join("config.ts");
7461        let barrel1 = dir.path().join("barrel1.ts");
7462        let barrel2 = dir.path().join("barrel2.ts");
7463        let barrel3 = dir.path().join("barrel3.ts");
7464        let index = dir.path().join("index.ts");
7465
7466        std::fs::write(
7467            &config,
7468            "export class Config {}\nexport default class DefaultConfig {}\n",
7469        )
7470        .unwrap();
7471        std::fs::write(
7472            &barrel1,
7473            "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
7474        )
7475        .unwrap();
7476        std::fs::write(
7477            &barrel2,
7478            "export { Config as RenamedConfig } from './barrel1';\n",
7479        )
7480        .unwrap();
7481        std::fs::write(
7482            &barrel3,
7483            "export * from './barrel2';\nexport * from './barrel1';\n",
7484        )
7485        .unwrap();
7486        std::fs::write(
7487            &index,
7488            "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
7489        )
7490        .unwrap();
7491
7492        let provider = TreeSitterProvider::new();
7493        let config_canon = std::fs::canonicalize(&config).unwrap();
7494
7495        let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
7496        assert_eq!(direct.len(), 1);
7497        assert_eq!(direct[0].symbol.name, "Config");
7498        assert_eq!(direct[0].file, config_canon.display().to_string());
7499
7500        let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
7501        assert_eq!(renamed.len(), 1);
7502        assert_eq!(renamed[0].symbol.name, "Config");
7503        assert_eq!(renamed[0].file, config_canon.display().to_string());
7504
7505        let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
7506        assert_eq!(wildcard_chain.len(), 1);
7507        assert_eq!(wildcard_chain[0].symbol.name, "Config");
7508        assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
7509
7510        let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
7511        assert_eq!(wildcard_default.len(), 1);
7512        assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
7513        assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
7514
7515        let local = provider.resolve_symbol(&index, "Config").unwrap();
7516        assert_eq!(local.len(), 1);
7517        assert_eq!(local[0].symbol.name, "Config");
7518        assert_eq!(local[0].file, index.display().to_string());
7519    }
7520
7521    // --- Parse tree caching ---
7522
7523    #[test]
7524    fn symbol_range_includes_rust_attributes() {
7525        let dir = tempfile::tempdir().unwrap();
7526        let path = dir.path().join("test_attrs.rs");
7527        std::fs::write(
7528            &path,
7529            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
7530        )
7531        .unwrap();
7532
7533        let provider = TreeSitterProvider::new();
7534        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
7535        assert_eq!(matches.len(), 1);
7536        assert_eq!(
7537            matches[0].symbol.range.start_line, 0,
7538            "symbol range should include preceding /// doc comment, got start_line={}",
7539            matches[0].symbol.range.start_line
7540        );
7541    }
7542
7543    #[test]
7544    fn symbol_range_includes_go_doc_comment() {
7545        let dir = tempfile::tempdir().unwrap();
7546        let path = dir.path().join("test_doc.go");
7547        std::fs::write(
7548            &path,
7549            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
7550        )
7551        .unwrap();
7552
7553        let provider = TreeSitterProvider::new();
7554        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
7555        assert_eq!(matches.len(), 1);
7556        assert_eq!(
7557            matches[0].symbol.range.start_line, 2,
7558            "symbol range should include preceding doc comments, got start_line={}",
7559            matches[0].symbol.range.start_line
7560        );
7561    }
7562
7563    #[test]
7564    fn symbol_range_skips_unrelated_comments() {
7565        let dir = tempfile::tempdir().unwrap();
7566        let path = dir.path().join("test_gap.go");
7567        std::fs::write(
7568            &path,
7569            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
7570        )
7571        .unwrap();
7572
7573        let provider = TreeSitterProvider::new();
7574        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
7575        assert_eq!(matches.len(), 1);
7576        assert_eq!(
7577            matches[0].symbol.range.start_line, 4,
7578            "symbol range should NOT include comment separated by blank line, got start_line={}",
7579            matches[0].symbol.range.start_line
7580        );
7581    }
7582
7583    #[test]
7584    fn parse_cache_returns_same_tree() {
7585        let mut parser = FileParser::new();
7586        let path = fixture_path("sample.ts");
7587
7588        let (tree1, _) = parser.parse(&path).unwrap();
7589        let tree1_root = tree1.root_node().byte_range();
7590
7591        let (tree2, _) = parser.parse(&path).unwrap();
7592        let tree2_root = tree2.root_node().byte_range();
7593
7594        // Same tree (cache hit) should return identical root node range
7595        assert_eq!(tree1_root, tree2_root);
7596    }
7597
7598    #[test]
7599    fn extract_symbols_from_tree_matches_list_symbols() {
7600        let path = fixture_path("sample.rs");
7601        let source = std::fs::read_to_string(&path).unwrap();
7602
7603        let provider = TreeSitterProvider::new();
7604        let listed = provider.list_symbols(&path).unwrap();
7605
7606        let mut parser = FileParser::new();
7607        let (tree, lang) = parser.parse(&path).unwrap();
7608        let extracted = extract_symbols_from_tree(&source, tree, lang).unwrap();
7609
7610        assert_eq!(symbols_as_debug(&extracted), symbols_as_debug(&listed));
7611    }
7612
7613    fn symbols_as_debug(symbols: &[Symbol]) -> Vec<String> {
7614        symbols
7615            .iter()
7616            .map(|symbol| {
7617                format!(
7618                    "{}|{:?}|{}:{}-{}:{}|{:?}|{:?}|{}|{:?}",
7619                    symbol.name,
7620                    symbol.kind,
7621                    symbol.range.start_line,
7622                    symbol.range.start_col,
7623                    symbol.range.end_line,
7624                    symbol.range.end_col,
7625                    symbol.signature,
7626                    symbol.scope_chain,
7627                    symbol.exported,
7628                    symbol.parent,
7629                )
7630            })
7631            .collect()
7632    }
7633
7634    // --- Python extraction ---
7635
7636    #[test]
7637    fn py_extracts_all_symbols() {
7638        let provider = TreeSitterProvider::new();
7639        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7640
7641        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7642        assert!(
7643            names.contains(&"top_level_function"),
7644            "missing top_level_function: {:?}",
7645            names
7646        );
7647        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
7648        assert!(
7649            names.contains(&"instance_method"),
7650            "missing method instance_method: {:?}",
7651            names
7652        );
7653        assert!(
7654            names.contains(&"decorated_function"),
7655            "missing decorated_function: {:?}",
7656            names
7657        );
7658
7659        // Plan requires ≥4 symbols
7660        assert!(
7661            symbols.len() >= 4,
7662            "expected ≥4 symbols, got {}: {:?}",
7663            symbols.len(),
7664            names
7665        );
7666    }
7667
7668    #[test]
7669    fn py_symbol_kinds_correct() {
7670        let provider = TreeSitterProvider::new();
7671        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7672
7673        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7674
7675        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
7676        assert_eq!(find("MyClass").kind, SymbolKind::Class);
7677        assert_eq!(find("instance_method").kind, SymbolKind::Method);
7678        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
7679        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
7680        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
7681        assert_eq!(find("inner_method").kind, SymbolKind::Method);
7682        assert_eq!(find("outer_method").kind, SymbolKind::Method);
7683    }
7684
7685    #[test]
7686    fn py_method_scope_chain() {
7687        let provider = TreeSitterProvider::new();
7688        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7689
7690        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7691
7692        // Method inside MyClass
7693        assert_eq!(
7694            find("instance_method").scope_chain,
7695            vec!["MyClass"],
7696            "instance_method should have MyClass in scope chain"
7697        );
7698        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
7699
7700        // Method inside OuterClass > InnerClass
7701        assert_eq!(
7702            find("inner_method").scope_chain,
7703            vec!["OuterClass", "InnerClass"],
7704            "inner_method should have nested scope chain"
7705        );
7706
7707        // InnerClass itself should have OuterClass in scope
7708        assert_eq!(
7709            find("InnerClass").scope_chain,
7710            vec!["OuterClass"],
7711            "InnerClass should have OuterClass in scope"
7712        );
7713
7714        // Top-level function has empty scope
7715        assert!(
7716            find("top_level_function").scope_chain.is_empty(),
7717            "top-level function should have empty scope chain"
7718        );
7719    }
7720
7721    #[test]
7722    fn py_decorated_function_signature() {
7723        let provider = TreeSitterProvider::new();
7724        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7725
7726        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7727
7728        let sig = find("decorated_function").signature.as_ref().unwrap();
7729        assert!(
7730            sig.contains("@staticmethod"),
7731            "decorated function signature should include decorator: {}",
7732            sig
7733        );
7734        assert!(
7735            sig.contains("def decorated_function"),
7736            "signature should include function def: {}",
7737            sig
7738        );
7739    }
7740
7741    #[test]
7742    fn py_ranges_valid() {
7743        let provider = TreeSitterProvider::new();
7744        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
7745
7746        for s in &symbols {
7747            assert!(
7748                s.range.end_line >= s.range.start_line,
7749                "symbol {} has invalid range: {:?}",
7750                s.name,
7751                s.range
7752            );
7753        }
7754    }
7755
7756    // --- Rust extraction ---
7757
7758    #[test]
7759    fn rs_extracts_all_symbols() {
7760        let provider = TreeSitterProvider::new();
7761        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7762
7763        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7764        assert!(
7765            names.contains(&"public_function"),
7766            "missing public_function: {:?}",
7767            names
7768        );
7769        assert!(
7770            names.contains(&"private_function"),
7771            "missing private_function: {:?}",
7772            names
7773        );
7774        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
7775        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
7776        assert!(
7777            names.contains(&"Drawable"),
7778            "missing trait Drawable: {:?}",
7779            names
7780        );
7781        // impl methods
7782        assert!(
7783            names.contains(&"new"),
7784            "missing impl method new: {:?}",
7785            names
7786        );
7787
7788        // Plan requires ≥6 symbols
7789        assert!(
7790            symbols.len() >= 6,
7791            "expected ≥6 symbols, got {}: {:?}",
7792            symbols.len(),
7793            names
7794        );
7795    }
7796
7797    #[test]
7798    fn rs_symbol_kinds_correct() {
7799        let provider = TreeSitterProvider::new();
7800        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7801
7802        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7803
7804        assert_eq!(find("public_function").kind, SymbolKind::Function);
7805        assert_eq!(find("private_function").kind, SymbolKind::Function);
7806        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
7807        assert_eq!(find("Color").kind, SymbolKind::Enum);
7808        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
7809        assert_eq!(find("new").kind, SymbolKind::Method);
7810    }
7811
7812    #[test]
7813    fn rs_pub_export_detection() {
7814        let provider = TreeSitterProvider::new();
7815        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7816
7817        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7818
7819        assert!(
7820            find("public_function").exported,
7821            "pub fn should be exported"
7822        );
7823        assert!(
7824            !find("private_function").exported,
7825            "non-pub fn should not be exported"
7826        );
7827        assert!(find("MyStruct").exported, "pub struct should be exported");
7828        assert!(find("Color").exported, "pub enum should be exported");
7829        assert!(find("Drawable").exported, "pub trait should be exported");
7830        assert!(
7831            find("new").exported,
7832            "pub fn inside impl should be exported"
7833        );
7834        assert!(
7835            !find("helper").exported,
7836            "non-pub fn inside impl should not be exported"
7837        );
7838    }
7839
7840    #[test]
7841    fn rs_impl_method_scope_chain() {
7842        let provider = TreeSitterProvider::new();
7843        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7844
7845        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7846
7847        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
7848        assert_eq!(
7849            find("new").scope_chain,
7850            vec!["MyStruct"],
7851            "impl method should have type in scope chain"
7852        );
7853        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
7854
7855        // Free function has empty scope chain
7856        assert!(
7857            find("public_function").scope_chain.is_empty(),
7858            "free function should have empty scope chain"
7859        );
7860    }
7861
7862    #[test]
7863    fn rs_trait_impl_scope_chain() {
7864        let provider = TreeSitterProvider::new();
7865        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7866
7867        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
7868        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
7869        assert_eq!(
7870            draw.scope_chain,
7871            vec!["Drawable for MyStruct"],
7872            "trait impl method should have 'Trait for Type' scope"
7873        );
7874        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
7875    }
7876
7877    #[test]
7878    fn rs_ranges_valid() {
7879        let provider = TreeSitterProvider::new();
7880        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
7881
7882        for s in &symbols {
7883            assert!(
7884                s.range.end_line >= s.range.start_line,
7885                "symbol {} has invalid range: {:?}",
7886                s.name,
7887                s.range
7888            );
7889        }
7890    }
7891
7892    // --- Go extraction ---
7893
7894    #[test]
7895    fn go_extracts_all_symbols() {
7896        let provider = TreeSitterProvider::new();
7897        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7898
7899        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
7900        assert!(
7901            names.contains(&"ExportedFunction"),
7902            "missing ExportedFunction: {:?}",
7903            names
7904        );
7905        assert!(
7906            names.contains(&"unexportedFunction"),
7907            "missing unexportedFunction: {:?}",
7908            names
7909        );
7910        assert!(
7911            names.contains(&"MyStruct"),
7912            "missing struct MyStruct: {:?}",
7913            names
7914        );
7915        assert!(
7916            names.contains(&"Reader"),
7917            "missing interface Reader: {:?}",
7918            names
7919        );
7920        // receiver method
7921        assert!(
7922            names.contains(&"String"),
7923            "missing receiver method String: {:?}",
7924            names
7925        );
7926
7927        // Plan requires ≥4 symbols
7928        assert!(
7929            symbols.len() >= 4,
7930            "expected ≥4 symbols, got {}: {:?}",
7931            symbols.len(),
7932            names
7933        );
7934    }
7935
7936    #[test]
7937    fn go_symbol_kinds_correct() {
7938        let provider = TreeSitterProvider::new();
7939        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7940
7941        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7942
7943        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
7944        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
7945        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
7946        assert_eq!(find("Reader").kind, SymbolKind::Interface);
7947        assert_eq!(find("String").kind, SymbolKind::Method);
7948        assert_eq!(find("helper").kind, SymbolKind::Method);
7949    }
7950
7951    #[test]
7952    fn go_uppercase_export_detection() {
7953        let provider = TreeSitterProvider::new();
7954        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7955
7956        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7957
7958        assert!(
7959            find("ExportedFunction").exported,
7960            "ExportedFunction (uppercase) should be exported"
7961        );
7962        assert!(
7963            !find("unexportedFunction").exported,
7964            "unexportedFunction (lowercase) should not be exported"
7965        );
7966        assert!(
7967            find("MyStruct").exported,
7968            "MyStruct (uppercase) should be exported"
7969        );
7970        assert!(
7971            find("Reader").exported,
7972            "Reader (uppercase) should be exported"
7973        );
7974        assert!(
7975            find("String").exported,
7976            "String method (uppercase) should be exported"
7977        );
7978        assert!(
7979            !find("helper").exported,
7980            "helper method (lowercase) should not be exported"
7981        );
7982    }
7983
7984    #[test]
7985    fn go_receiver_method_scope_chain() {
7986        let provider = TreeSitterProvider::new();
7987        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
7988
7989        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
7990
7991        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
7992        assert_eq!(
7993            find("String").scope_chain,
7994            vec!["MyStruct"],
7995            "receiver method should have type in scope chain"
7996        );
7997        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
7998
7999        // Regular function has empty scope chain
8000        assert!(
8001            find("ExportedFunction").scope_chain.is_empty(),
8002            "regular function should have empty scope chain"
8003        );
8004    }
8005
8006    #[test]
8007    fn go_ranges_valid() {
8008        let provider = TreeSitterProvider::new();
8009        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
8010
8011        for s in &symbols {
8012            assert!(
8013                s.range.end_line >= s.range.start_line,
8014                "symbol {} has invalid range: {:?}",
8015                s.name,
8016                s.range
8017            );
8018        }
8019    }
8020
8021    // --- Cross-language ---
8022
8023    #[test]
8024    fn cross_language_all_six_produce_symbols() {
8025        let provider = TreeSitterProvider::new();
8026
8027        let fixtures = [
8028            ("sample.ts", "TypeScript"),
8029            ("sample.tsx", "TSX"),
8030            ("sample.js", "JavaScript"),
8031            ("sample.py", "Python"),
8032            ("sample.rs", "Rust"),
8033            ("sample.go", "Go"),
8034        ];
8035
8036        for (fixture, lang) in &fixtures {
8037            let symbols = provider
8038                .list_symbols(&fixture_path(fixture))
8039                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
8040            assert!(
8041                symbols.len() >= 2,
8042                "{} should produce ≥2 symbols, got {}: {:?}",
8043                lang,
8044                symbols.len(),
8045                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
8046            );
8047        }
8048    }
8049
8050    // --- Symbol cache tests ---
8051
8052    #[test]
8053    fn symbol_cache_returns_cached_results_on_second_call() {
8054        let dir = tempfile::tempdir().unwrap();
8055        let file = dir.path().join("test.rs");
8056        std::fs::write(&file, "pub fn hello() {}\npub fn world() {}").unwrap();
8057
8058        let mut parser = FileParser::new();
8059
8060        let symbols1 = parser.extract_symbols(&file).unwrap();
8061        assert_eq!(symbols1.len(), 2);
8062
8063        // Second call should return cached result
8064        let symbols2 = parser.extract_symbols(&file).unwrap();
8065        assert_eq!(symbols2.len(), 2);
8066        assert_eq!(symbols1[0].name, symbols2[0].name);
8067
8068        // Verify cache is populated
8069        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
8070    }
8071
8072    #[test]
8073    fn symbol_cache_invalidates_on_file_change() {
8074        let dir = tempfile::tempdir().unwrap();
8075        let file = dir.path().join("test.rs");
8076        std::fs::write(&file, "pub fn hello() {}").unwrap();
8077
8078        let mut parser = FileParser::new();
8079
8080        let symbols1 = parser.extract_symbols(&file).unwrap();
8081        assert_eq!(symbols1.len(), 1);
8082        assert_eq!(symbols1[0].name, "hello");
8083
8084        // Wait to ensure mtime changes (filesystem resolution can be 1s on some OS)
8085        std::thread::sleep(std::time::Duration::from_millis(50));
8086
8087        // Modify file — add a second function
8088        std::fs::write(&file, "pub fn hello() {}\npub fn goodbye() {}").unwrap();
8089
8090        // Should detect mtime change and re-extract
8091        let symbols2 = parser.extract_symbols(&file).unwrap();
8092        assert_eq!(symbols2.len(), 2);
8093        assert!(symbols2.iter().any(|s| s.name == "goodbye"));
8094    }
8095
8096    #[test]
8097    fn symbol_cache_invalidate_method_clears_entry() {
8098        let dir = tempfile::tempdir().unwrap();
8099        let file = dir.path().join("test.rs");
8100        std::fs::write(&file, "pub fn hello() {}").unwrap();
8101
8102        let mut parser = FileParser::new();
8103        parser.extract_symbols(&file).unwrap();
8104        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
8105
8106        parser.invalidate_symbols(&file);
8107        assert!(!parser.symbol_cache.read().unwrap().contains_key(&file));
8108        // Parse tree cache should also be cleared
8109        assert!(!parser.cache.contains_key(&file));
8110    }
8111
8112    #[test]
8113    fn symbol_cache_works_for_multiple_languages() {
8114        let dir = tempfile::tempdir().unwrap();
8115        let rs_file = dir.path().join("lib.rs");
8116        let ts_file = dir.path().join("app.ts");
8117        let py_file = dir.path().join("main.py");
8118
8119        std::fs::write(&rs_file, "pub fn rust_fn() {}").unwrap();
8120        std::fs::write(&ts_file, "export function tsFn() {}").unwrap();
8121        std::fs::write(&py_file, "def py_fn():\n    pass").unwrap();
8122
8123        let mut parser = FileParser::new();
8124
8125        let rs_syms = parser.extract_symbols(&rs_file).unwrap();
8126        let ts_syms = parser.extract_symbols(&ts_file).unwrap();
8127        let py_syms = parser.extract_symbols(&py_file).unwrap();
8128
8129        assert!(rs_syms.iter().any(|s| s.name == "rust_fn"));
8130        assert!(ts_syms.iter().any(|s| s.name == "tsFn"));
8131        assert!(py_syms.iter().any(|s| s.name == "py_fn"));
8132
8133        // All should be cached now
8134        assert_eq!(parser.symbol_cache.read().unwrap().len(), 3);
8135
8136        // Re-extract should return same results from cache
8137        let rs_syms2 = parser.extract_symbols(&rs_file).unwrap();
8138        assert_eq!(rs_syms.len(), rs_syms2.len());
8139    }
8140
8141    #[test]
8142    fn extract_json_symbols_top_level_keys() {
8143        let dir = tempfile::tempdir().unwrap();
8144        let file = dir.path().join("package.json");
8145        std::fs::write(&file, r#"{"name": "x", "version": "1"}"#).unwrap();
8146
8147        let mut parser = FileParser::new();
8148        let symbols = parser.extract_symbols(&file).unwrap();
8149
8150        assert_eq!(symbols.len(), 2);
8151        assert!(symbols
8152            .iter()
8153            .any(|s| s.name == "name" && s.kind == SymbolKind::Variable));
8154        assert!(symbols
8155            .iter()
8156            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
8157    }
8158
8159    #[test]
8160    fn extract_json_symbols_root_array() {
8161        let dir = tempfile::tempdir().unwrap();
8162        let file = dir.path().join("array.json");
8163        std::fs::write(&file, "[1,2,3]").unwrap();
8164
8165        let mut parser = FileParser::new();
8166        let symbols = parser.extract_symbols(&file).unwrap();
8167
8168        assert_eq!(symbols.len(), 0);
8169    }
8170
8171    #[test]
8172    fn extract_json_symbols_no_recursion_into_nested() {
8173        let dir = tempfile::tempdir().unwrap();
8174        let file = dir.path().join("nested.json");
8175        std::fs::write(&file, r#"{"scripts": {"build": "tsc"}}"#).unwrap();
8176
8177        let mut parser = FileParser::new();
8178        let symbols = parser.extract_symbols(&file).unwrap();
8179
8180        assert_eq!(symbols.len(), 1);
8181        assert_eq!(symbols[0].name, "scripts");
8182        assert!(!symbols.iter().any(|s| s.name == "build"));
8183    }
8184
8185    #[test]
8186    fn extract_scala_symbols_object_and_method() {
8187        let dir = tempfile::tempdir().unwrap();
8188        let file = dir.path().join("Greeter.scala");
8189        std::fs::write(
8190            &file,
8191            "object Greeter {\n  def hello(name: String): String = s\"hi $name\"\n}",
8192        )
8193        .unwrap();
8194
8195        let mut parser = FileParser::new();
8196        let symbols = parser.extract_symbols(&file).unwrap();
8197
8198        assert!(symbols
8199            .iter()
8200            .any(|s| s.name == "Greeter" && s.kind == SymbolKind::Class));
8201        assert!(symbols.iter().any(|s| s.name == "hello"
8202            && s.kind == SymbolKind::Method
8203            && s.scope_chain == vec!["Greeter".to_string()]));
8204    }
8205
8206    #[test]
8207    fn extract_scala_symbols_class_and_trait() {
8208        let dir = tempfile::tempdir().unwrap();
8209        let file = dir.path().join("Types.scala");
8210        std::fs::write(&file, "class Foo\ntrait Bar").unwrap();
8211
8212        let mut parser = FileParser::new();
8213        let symbols = parser.extract_symbols(&file).unwrap();
8214
8215        assert!(symbols
8216            .iter()
8217            .any(|s| s.name == "Foo" && s.kind == SymbolKind::Class));
8218        assert!(symbols
8219            .iter()
8220            .any(|s| s.name == "Bar" && s.kind == SymbolKind::Interface));
8221    }
8222
8223    #[test]
8224    fn extract_yaml_symbols_k8s_resource() {
8225        let dir = tempfile::tempdir().unwrap();
8226        let file = dir.path().join("deployment.yaml");
8227        std::fs::write(
8228            &file,
8229            r#"apiVersion: apps/v1
8230kind: Deployment
8231metadata:
8232  name: nginx
8233  namespace: web
8234"#,
8235        )
8236        .unwrap();
8237
8238        let mut parser = FileParser::new();
8239        let symbols = parser.extract_symbols(&file).unwrap();
8240
8241        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Deployment");
8242        let sym = &symbols[0];
8243        assert_eq!(sym.name, "web/Deployment/nginx");
8244        assert_eq!(sym.kind, SymbolKind::Class);
8245        assert!(sym.exported);
8246    }
8247
8248    #[test]
8249    fn extract_yaml_symbols_k8s_no_namespace() {
8250        let dir = tempfile::tempdir().unwrap();
8251        let file = dir.path().join("service.yaml");
8252        std::fs::write(
8253            &file,
8254            r#"apiVersion: v1
8255kind: Service
8256metadata:
8257  name: nginx-svc
8258"#,
8259        )
8260        .unwrap();
8261
8262        let mut parser = FileParser::new();
8263        let symbols = parser.extract_symbols(&file).unwrap();
8264
8265        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Service");
8266        let sym = &symbols[0];
8267        assert_eq!(sym.name, "Service/nginx-svc");
8268        assert_eq!(sym.kind, SymbolKind::Class);
8269    }
8270
8271    #[test]
8272    fn extract_yaml_symbols_multidoc() {
8273        let dir = tempfile::tempdir().unwrap();
8274        let file = dir.path().join("multidoc.yaml");
8275        std::fs::write(
8276            &file,
8277            r#"apiVersion: apps/v1
8278kind: Deployment
8279metadata:
8280  name: a
8281---
8282apiVersion: v1
8283kind: Service
8284metadata:
8285  name: b
8286"#,
8287        )
8288        .unwrap();
8289
8290        let mut parser = FileParser::new();
8291        let symbols = parser.extract_symbols(&file).unwrap();
8292
8293        assert_eq!(symbols.len(), 2, "Expected 2 symbols for multi-doc YAML");
8294        assert!(symbols.iter().any(|s| s.name == "Deployment/a"));
8295        assert!(symbols.iter().any(|s| s.name == "Service/b"));
8296    }
8297
8298    #[test]
8299    fn extract_yaml_symbols_generic_fallback() {
8300        let dir = tempfile::tempdir().unwrap();
8301        let file = dir.path().join("compose.yaml");
8302        std::fs::write(
8303            &file,
8304            r#"version: "3"
8305services:
8306  web: {}
8307volumes:
8308  data: {}
8309"#,
8310        )
8311        .unwrap();
8312
8313        let mut parser = FileParser::new();
8314        let symbols = parser.extract_symbols(&file).unwrap();
8315
8316        // Should have top-level keys: version, services, volumes
8317        assert_eq!(symbols.len(), 3, "Expected 3 symbols for generic YAML");
8318        assert!(symbols
8319            .iter()
8320            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
8321        assert!(symbols
8322            .iter()
8323            .any(|s| s.name == "services" && s.kind == SymbolKind::Variable));
8324        assert!(symbols
8325            .iter()
8326            .any(|s| s.name == "volumes" && s.kind == SymbolKind::Variable));
8327    }
8328
8329    #[test]
8330    fn extract_yaml_symbols_empty() {
8331        let dir = tempfile::tempdir().unwrap();
8332        let file = dir.path().join("empty.yaml");
8333        std::fs::write(&file, "").unwrap();
8334
8335        let mut parser = FileParser::new();
8336        let symbols = parser.extract_symbols(&file).unwrap();
8337
8338        assert_eq!(symbols.len(), 0, "Expected 0 symbols for empty YAML");
8339    }
8340
8341    #[test]
8342    fn extract_yaml_symbols_resource_limits() {
8343        let dir = tempfile::tempdir().unwrap();
8344        let file = dir.path().join("deployment.yaml");
8345        std::fs::write(
8346            &file,
8347            r#"apiVersion: apps/v1
8348kind: Deployment
8349metadata:
8350  name: app
8351spec:
8352  template:
8353    spec:
8354      containers:
8355      - name: main
8356        image: myapp:1.0
8357        resources:
8358          limits:
8359            cpu: "2"
8360            memory: 1Gi
8361          requests:
8362            cpu: "1"
8363            memory: 512Mi
8364"#,
8365        )
8366        .unwrap();
8367
8368        let mut parser = FileParser::new();
8369        let symbols = parser.extract_symbols(&file).unwrap();
8370
8371        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
8372        let sym = &symbols[0];
8373        assert_eq!(sym.name, "Deployment/app");
8374        let sig = sym.signature.as_ref().unwrap();
8375        assert!(sig.contains("cpu="), "Signature should contain cpu= field");
8376        assert!(
8377            sig.contains("memory="),
8378            "Signature should contain memory= field"
8379        );
8380    }
8381
8382    #[test]
8383    fn extract_yaml_symbols_env_names() {
8384        let dir = tempfile::tempdir().unwrap();
8385        let file = dir.path().join("deployment.yaml");
8386        std::fs::write(
8387            &file,
8388            r#"apiVersion: apps/v1
8389kind: Deployment
8390metadata:
8391  name: app
8392spec:
8393  template:
8394    spec:
8395      containers:
8396      - name: main
8397        image: myapp:1.0
8398        env:
8399        - name: FOO
8400          value: "bar"
8401        - name: BAR
8402          value: "baz"
8403"#,
8404        )
8405        .unwrap();
8406
8407        let mut parser = FileParser::new();
8408        let symbols = parser.extract_symbols(&file).unwrap();
8409
8410        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
8411        let sym = &symbols[0];
8412        let sig = sym.signature.as_ref().unwrap();
8413        assert!(
8414            sig.contains("env=FOO,BAR"),
8415            "Signature should contain env=FOO,BAR"
8416        );
8417    }
8418
8419    #[test]
8420    fn extract_yaml_symbols_rbac_rules() {
8421        let dir = tempfile::tempdir().unwrap();
8422        let file = dir.path().join("role.yaml");
8423        std::fs::write(
8424            &file,
8425            r#"apiVersion: rbac.authorization.k8s.io/v1
8426kind: Role
8427metadata:
8428  name: reader
8429rules:
8430- apiGroups: [""]
8431  resources: [pods, services]
8432  verbs: [get, list, watch]
8433"#,
8434        )
8435        .unwrap();
8436
8437        let mut parser = FileParser::new();
8438        let symbols = parser.extract_symbols(&file).unwrap();
8439
8440        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Role");
8441        let sym = &symbols[0];
8442        let sig = sym.signature.as_ref().unwrap();
8443        assert!(
8444            sig.contains("verbs=get,list,watch"),
8445            "Signature should contain verbs=get,list,watch"
8446        );
8447        assert!(
8448            sig.contains("resources=pods,services"),
8449            "Signature should contain resources=pods,services"
8450        );
8451    }
8452
8453    #[test]
8454    fn extract_yaml_symbols_argo_workflow() {
8455        let dir = tempfile::tempdir().unwrap();
8456        let file = dir.path().join("workflow.yaml");
8457        std::fs::write(
8458            &file,
8459            r#"apiVersion: argoproj.io/v1alpha1
8460kind: Workflow
8461metadata:
8462  name: hello-world
8463spec:
8464  entrypoint: main
8465  templates:
8466  - name: main
8467    container:
8468      image: alpine:3.18
8469      command: [echo]
8470      args: ["hello"]
8471  - name: print
8472    container:
8473      image: alpine:3.18
8474      command: [echo]
8475      args: ["world"]
8476"#,
8477        )
8478        .unwrap();
8479
8480        let mut parser = FileParser::new();
8481        let symbols = parser.extract_symbols(&file).unwrap();
8482
8483        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
8484        let sym = &symbols[0];
8485        assert!(
8486            sym.name.contains("Workflow"),
8487            "Symbol name should contain Workflow"
8488        );
8489        let sig = sym.signature.as_ref().unwrap();
8490        assert!(
8491            sig.contains("entrypoint=main"),
8492            "Signature should contain entrypoint=main"
8493        );
8494        assert!(
8495            sig.contains("templates=main,print"),
8496            "Signature should contain templates=main,print"
8497        );
8498        assert!(
8499            sig.contains("image=alpine:3.18"),
8500            "Signature should contain image=alpine:3.18"
8501        );
8502        assert!(
8503            sig.contains("command=echo"),
8504            "Signature should contain command=echo"
8505        );
8506    }
8507
8508    #[test]
8509    fn extract_yaml_symbols_generatename_fallback() {
8510        let dir = tempfile::tempdir().unwrap();
8511        let file = dir.path().join("workflow.yaml");
8512        std::fs::write(
8513            &file,
8514            r#"apiVersion: argoproj.io/v1alpha1
8515kind: Workflow
8516metadata:
8517  generateName: hello-
8518spec:
8519  entrypoint: main
8520  templates:
8521  - name: main
8522    container:
8523      image: alpine:3.18
8524"#,
8525        )
8526        .unwrap();
8527
8528        let mut parser = FileParser::new();
8529        let symbols = parser.extract_symbols(&file).unwrap();
8530
8531        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
8532        let sym = &symbols[0];
8533        assert!(
8534            sym.name.contains("hello-"),
8535            "Symbol name should contain generateName fallback 'hello-'"
8536        );
8537    }
8538
8539    #[test]
8540    fn detect_r_extensions_are_case_sensitive() {
8541        assert_eq!(detect_language(Path::new("analysis.R")), Some(LangId::R));
8542        assert_eq!(detect_language(Path::new("script.r")), Some(LangId::R));
8543    }
8544
8545    #[test]
8546    fn extract_r_symbols_test() {
8547        let dir = tempfile::tempdir().unwrap();
8548        let file = dir.path().join("analysis.R");
8549        std::fs::write(
8550            &file,
8551            r#"
8552# summary function
8553summarise <- function(data, column) {
8554  total <- sum(data[[column]])
8555  total
8556}
8557
8558normalise = function(x) {
8559  x / max(x)
8560}
8561
8562function(y) {
8563  y + 1
8564} -> transform_values
8565
8566threshold <- 10
8567"done" -> status
8568
8569outer <- function(values) {
8570  local_value <- 1
8571  local_value
8572}
8573"#,
8574        )
8575        .unwrap();
8576
8577        let mut parser = FileParser::new();
8578        let symbols = parser.extract_symbols(&file).unwrap();
8579
8580        let get = |name: &str| {
8581            symbols
8582                .iter()
8583                .find(|symbol| symbol.name == name)
8584                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
8585        };
8586
8587        assert_eq!(get("summarise").kind, SymbolKind::Function);
8588        assert_eq!(get("normalise").kind, SymbolKind::Function);
8589        assert_eq!(get("transform_values").kind, SymbolKind::Function);
8590        assert!(
8591            symbols
8592                .iter()
8593                .all(|symbol| !symbol.name.starts_with("function(")),
8594            "rightward function assignments should use the RHS identifier: {symbols:?}"
8595        );
8596        assert_eq!(get("outer").kind, SymbolKind::Function);
8597        assert_eq!(get("threshold").kind, SymbolKind::Variable);
8598        assert_eq!(get("status").kind, SymbolKind::Variable);
8599        assert!(
8600            symbols.iter().all(|symbol| symbol.name != "local_value"),
8601            "nested assignments should not surface as top-level variables: {symbols:?}"
8602        );
8603    }
8604
8605    #[test]
8606    fn extract_pascal_symbols_test() {
8607        let dir = tempfile::tempdir().unwrap();
8608        let file = dir.path().join("MyUnit.pas");
8609        std::fs::write(
8610            &file,
8611            r#"
8612unit MyUnit;
8613
8614interface
8615
8616uses SysUtils;
8617
8618type
8619  TMyClass = class
8620  private
8621    FValue: Integer;
8622  public
8623    constructor Create;
8624    procedure DoSomething; virtual;
8625  end;
8626
8627  TMyRecord = record
8628    X, Y: Integer;
8629  end;
8630
8631  IMyInterface = interface
8632    procedure DoSomethingElse;
8633  end;
8634
8635  TMyEnum = (Red, Green, Blue);
8636
8637const
8638  UNIT_CONST = 42;
8639
8640var
8641  UnitVar: string;
8642
8643implementation
8644
8645constructor TMyClass.Create;
8646begin
8647  FValue := 0;
8648end;
8649
8650procedure TMyClass.DoSomething;
8651begin
8652end;
8653
8654procedure StandaloneProc;
8655begin
8656end;
8657
8658end.
8659"#,
8660        )
8661        .unwrap();
8662
8663        let mut parser = FileParser::new();
8664        let symbols = parser.extract_symbols(&file).unwrap();
8665
8666        let get = |name: &str| {
8667            symbols
8668                .iter()
8669                .find(|symbol| symbol.name == name)
8670                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
8671        };
8672
8673        assert_eq!(get("MyUnit").kind, SymbolKind::Class);
8674        assert_eq!(get("TMyClass").kind, SymbolKind::Class);
8675        assert_eq!(get("TMyRecord").kind, SymbolKind::Struct);
8676        assert_eq!(get("IMyInterface").kind, SymbolKind::Interface);
8677        assert_eq!(get("TMyEnum").kind, SymbolKind::Enum);
8678        assert_eq!(get("UNIT_CONST").kind, SymbolKind::Variable);
8679        assert_eq!(get("UnitVar").kind, SymbolKind::Variable);
8680        assert_eq!(get("StandaloneProc").kind, SymbolKind::Function);
8681
8682        // Methods inside TMyClass
8683        let create_methods: Vec<&Symbol> = symbols.iter().filter(|s| s.name == "Create").collect();
8684        assert_eq!(create_methods.len(), 2); // one in interface, one in implementation
8685        for m in create_methods {
8686            assert_eq!(m.kind, SymbolKind::Method);
8687            assert_eq!(m.scope_chain, vec!["TMyClass".to_string()]);
8688        }
8689
8690        let do_something_methods: Vec<&Symbol> =
8691            symbols.iter().filter(|s| s.name == "DoSomething").collect();
8692        assert_eq!(do_something_methods.len(), 2); // one in interface, one in implementation
8693        for m in do_something_methods {
8694            assert_eq!(m.kind, SymbolKind::Method);
8695            assert_eq!(m.scope_chain, vec!["TMyClass".to_string()]);
8696        }
8697    }
8698}