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