Skip to main content

aft/
parser.rs

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