Skip to main content

aft/
parser.rs

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