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