Skip to main content

aft/
parser.rs

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