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