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
5164fn extract_json_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
5165    let Some(value) = root.named_child(0) else {
5166        return Ok(Vec::new());
5167    };
5168
5169    if value.kind() != "object" {
5170        return Ok(Vec::new());
5171    }
5172
5173    let mut symbols = Vec::new();
5174    let mut cursor = value.walk();
5175    for child in value.named_children(&mut cursor) {
5176        if child.kind() != "pair" {
5177            continue;
5178        }
5179        let Some(key_node) = child.child_by_field_name("key") else {
5180            continue;
5181        };
5182        let name = node_text(source, &key_node).trim_matches('"').to_string();
5183        if name.is_empty() {
5184            continue;
5185        }
5186        symbols.push(Symbol {
5187            name,
5188            kind: SymbolKind::Variable,
5189            range: node_range_with_decorators(&child, source, LangId::Json),
5190            signature: None,
5191            scope_chain: vec![],
5192            exported: false,
5193            parent: None,
5194        });
5195    }
5196
5197    Ok(symbols)
5198}
5199
5200/// Read a YAML scalar/key node as trimmed text, stripping surrounding quotes.
5201fn yaml_scalar_text(source: &str, node: &Node) -> String {
5202    node_text(source, node)
5203        .trim()
5204        .trim_matches('"')
5205        .trim_matches('\'')
5206        .to_string()
5207}
5208
5209/// Find the mapping that is the direct payload of `node`, descending only
5210/// through wrapper nodes (`document`, `block_node`, `flow_node`) rather than
5211/// arbitrary nested mappings. This keeps detection anchored to the document's
5212/// top-level mapping instead of latching onto a mapping buried inside a
5213/// sequence or nested value.
5214fn yaml_find_mapping<'a>(node: &Node<'a>) -> Option<Node<'a>> {
5215    let mut current = *node;
5216    loop {
5217        match current.kind() {
5218            "block_mapping" | "flow_mapping" => return Some(current),
5219            "document" | "block_node" | "flow_node" => {
5220                let mut cursor = current.walk();
5221                let next = current
5222                    .named_children(&mut cursor)
5223                    .find(|child| !matches!(child.kind(), "tag" | "anchor"));
5224                match next {
5225                    Some(inner) => current = inner,
5226                    None => return None,
5227                }
5228            }
5229            _ => return None,
5230        }
5231    }
5232}
5233
5234/// Look up a top-level key in a YAML mapping and return its value node.
5235fn yaml_get<'a>(mapping: &Node<'a>, source: &str, key: &str) -> Option<Node<'a>> {
5236    let mut cursor = mapping.walk();
5237    for pair in mapping.named_children(&mut cursor) {
5238        if pair.kind() != "block_mapping_pair" && pair.kind() != "flow_pair" {
5239            continue;
5240        }
5241        let Some(key_node) = pair.child_by_field_name("key") else {
5242            continue;
5243        };
5244        if yaml_scalar_text(source, &key_node) == key {
5245            return pair.child_by_field_name("value");
5246        }
5247    }
5248    None
5249}
5250
5251/// Flatten a YAML value node to clean text for embed_text. Scalars return their
5252/// trimmed text; block/flow sequences are joined as comma-separated scalar items
5253/// (so `verbs: [get, list, watch]` becomes `get,list,watch` instead of raw
5254/// multi-line `- get\n- list` text). Nested mappings are ignored here.
5255fn yaml_unwrap<'a>(node: &Node<'a>) -> Node<'a> {
5256    let mut current = *node;
5257    while current.kind() == "block_node" || current.kind() == "flow_node" {
5258        let mut cursor = current.walk();
5259        let next = current
5260            .named_children(&mut cursor)
5261            .find(|child| !matches!(child.kind(), "tag" | "anchor"));
5262        match next {
5263            Some(inner) => current = inner,
5264            None => break,
5265        }
5266    }
5267    current
5268}
5269
5270fn yaml_flatten_value(source: &str, node: &Node) -> String {
5271    let node = &yaml_unwrap(node);
5272    match node.kind() {
5273        "block_sequence" | "flow_sequence" => {
5274            let mut items = Vec::new();
5275            let mut cursor = node.walk();
5276            for child in node.named_children(&mut cursor) {
5277                // block_sequence_item wraps a value; flow_sequence holds nodes directly.
5278                let value = if child.kind() == "block_sequence_item" {
5279                    child.named_child(0)
5280                } else {
5281                    Some(child)
5282                };
5283                if let Some(v) = value {
5284                    let v = yaml_unwrap(&v);
5285                    // Only flatten scalar leaves; skip mappings (handled elsewhere).
5286                    if v.kind() != "block_mapping" && v.kind() != "flow_mapping" {
5287                        let text = yaml_scalar_text(source, &v);
5288                        if !text.is_empty() {
5289                            items.push(text);
5290                        }
5291                    }
5292                }
5293            }
5294            items.join(",")
5295        }
5296        // Mapping values (e.g. container `resources:` block) are not flattened
5297        // here — their high-signal leaves (cpu/memory) are collected separately.
5298        // Returning empty avoids dumping raw multi-line YAML into embed_text.
5299        "block_mapping" | "flow_mapping" => String::new(),
5300        _ => yaml_scalar_text(source, node),
5301    }
5302}
5303
5304/// Recursively collect `key=value` pairs for the given keys (e.g. image, cpu,
5305/// memory, verbs) to enrich embed_text for semantic search. Sequence values are
5306/// flattened to comma-joined scalars. Capped to avoid runaway output.
5307fn yaml_collect_values(
5308    source: &str,
5309    node: &Node,
5310    keys: &[&str],
5311    out: &mut Vec<String>,
5312    cap: usize,
5313) {
5314    if out.len() >= cap {
5315        return;
5316    }
5317    let mut cursor = node.walk();
5318    for child in node.named_children(&mut cursor) {
5319        if child.kind() == "block_mapping_pair" || child.kind() == "flow_pair" {
5320            if let Some(key_node) = child.child_by_field_name("key") {
5321                let key_text = yaml_scalar_text(source, &key_node);
5322                if keys.contains(&key_text.as_str()) {
5323                    if let Some(value_node) = child.child_by_field_name("value") {
5324                        let value_text = yaml_flatten_value(source, &value_node);
5325                        if !value_text.is_empty() && out.len() < cap {
5326                            out.push(format!("{}={}", key_text, value_text));
5327                        }
5328                    }
5329                }
5330            }
5331        }
5332        yaml_collect_values(source, &child, keys, out, cap);
5333    }
5334}
5335
5336/// Collect the `name:` field from every item of a `<parent_key>:` sequence of
5337/// mappings. The bare `name` key is too generic to match globally (it collides
5338/// with metadata/container names), so these are gathered by parent key and
5339/// emitted as `<label>=A,B,...`. Handles k8s `env: [{name,value}]` and Argo
5340/// Workflow `templates: [{name, ...}]`. Capped.
5341fn yaml_collect_named_items(
5342    source: &str,
5343    node: &Node,
5344    parent_key: &str,
5345    label: &str,
5346    out: &mut Vec<String>,
5347    cap: usize,
5348) {
5349    let mut cursor = node.walk();
5350    for child in node.named_children(&mut cursor) {
5351        if (child.kind() == "block_mapping_pair" || child.kind() == "flow_pair")
5352            && child
5353                .child_by_field_name("key")
5354                .map(|k| yaml_scalar_text(source, &k))
5355                .as_deref()
5356                == Some(parent_key)
5357        {
5358            if let Some(seq) = child.child_by_field_name("value") {
5359                let seq = yaml_unwrap(&seq);
5360                let mut names = Vec::new();
5361                let mut seq_cursor = seq.walk();
5362                for item in seq.named_children(&mut seq_cursor) {
5363                    let value = if item.kind() == "block_sequence_item" {
5364                        item.named_child(0)
5365                    } else {
5366                        Some(item)
5367                    };
5368                    if let Some(v) = value {
5369                        if let Some(map) = yaml_find_mapping(&v) {
5370                            if let Some(name_node) = yaml_get(&map, source, "name") {
5371                                let n = yaml_scalar_text(source, &name_node);
5372                                if !n.is_empty() && names.len() < 16 {
5373                                    names.push(n);
5374                                }
5375                            }
5376                        }
5377                    }
5378                }
5379                if !names.is_empty() && out.len() < cap {
5380                    out.push(format!("{}={}", label, names.join(",")));
5381                }
5382            }
5383        }
5384        yaml_collect_named_items(source, &child, parent_key, label, out, cap);
5385    }
5386}
5387
5388/// Tier 1: if a document is a Kubernetes resource (has both apiVersion + kind),
5389/// emit one rich symbol named `<ns>/<Kind>/<name>` with enriched signature.
5390/// Generalizes to arbitrary CRDs since apiVersion+kind is the CRD contract.
5391fn yaml_k8s_resource_symbol(source: &str, doc: &Node, mapping: &Node) -> Option<Symbol> {
5392    let api_version =
5393        yaml_get(mapping, source, "apiVersion").map(|n| yaml_scalar_text(source, &n))?;
5394    let kind = yaml_get(mapping, source, "kind").map(|n| yaml_scalar_text(source, &n))?;
5395    if api_version.is_empty() || kind.is_empty() {
5396        return None;
5397    }
5398
5399    let (name, namespace) = match yaml_get(mapping, source, "metadata") {
5400        Some(meta) => match yaml_find_mapping(&meta) {
5401            Some(meta_map) => {
5402                // Prefer `name`; fall back to `generateName` (common in Argo
5403                // Workflows submitted without a fixed name) so the symbol still
5404                // carries an identifier instead of collapsing to bare <Kind>.
5405                let name = yaml_get(&meta_map, source, "name")
5406                    .map(|n| yaml_scalar_text(source, &n))
5407                    .filter(|s| !s.is_empty())
5408                    .or_else(|| {
5409                        yaml_get(&meta_map, source, "generateName")
5410                            .map(|n| yaml_scalar_text(source, &n))
5411                            .filter(|s| !s.is_empty())
5412                    });
5413                (
5414                    name,
5415                    yaml_get(&meta_map, source, "namespace").map(|n| yaml_scalar_text(source, &n)),
5416                )
5417            }
5418            None => (None, None),
5419        },
5420        None => (None, None),
5421    };
5422
5423    let res_name = name.clone().filter(|s| !s.is_empty());
5424    let sym_name = match (&namespace, &res_name) {
5425        (Some(ns), Some(n)) if !ns.is_empty() => format!("{}/{}/{}", ns, kind, n),
5426        (_, Some(n)) => format!("{}/{}", kind, n),
5427        _ => kind.clone(),
5428    };
5429
5430    let mut sig = format!("apiVersion={} kind={}", api_version, kind);
5431    if let Some(ns) = namespace.as_ref().filter(|s| !s.is_empty()) {
5432        sig.push_str(&format!(" namespace={}", ns));
5433    }
5434    if let Some(n) = res_name.as_ref() {
5435        sig.push_str(&format!(" name={}", n));
5436    }
5437    // Enrich with high-signal spec fields so intent queries match. Covers
5438    // containers (image/ports), resource limits/requests (cpu/memory), RBAC
5439    // rules (verbs/resources/apiGroups), and storage (volumeMounts mountPath).
5440    let mut extras = Vec::new();
5441    yaml_collect_values(
5442        source,
5443        mapping,
5444        &[
5445            "image",
5446            "containerPort",
5447            "port",
5448            "targetPort",
5449            "cpu",
5450            "memory",
5451            "storage",
5452            "verbs",
5453            "resources",
5454            "apiGroups",
5455            "mountPath",
5456            "replicas",
5457            // Argo Workflow high-signal scalars.
5458            "entrypoint",
5459            "command",
5460            "args",
5461            "schedule",
5462        ],
5463        &mut extras,
5464        24,
5465    );
5466    // Sequence-of-mappings whose `name` key is ambiguous: collect by parent key.
5467    // k8s env vars and Argo Workflow templates both use the `{name: ...}` shape.
5468    yaml_collect_named_items(source, mapping, "env", "env", &mut extras, 24);
5469    yaml_collect_named_items(source, mapping, "templates", "templates", &mut extras, 24);
5470    if !extras.is_empty() {
5471        sig.push(' ');
5472        sig.push_str(&extras.join(" "));
5473    }
5474
5475    Some(Symbol {
5476        name: sym_name,
5477        kind: SymbolKind::Class,
5478        range: node_range(doc),
5479        signature: Some(sig),
5480        scope_chain: vec![],
5481        exported: true,
5482        parent: None,
5483    })
5484}
5485
5486/// Tier 2: generic YAML — emit top-level mapping keys as Variable symbols
5487/// (docker-compose services, CI jobs, Helm values.yaml, etc.).
5488fn yaml_generic_keys(source: &str, mapping: &Node, symbols: &mut Vec<Symbol>) {
5489    let mut cursor = mapping.walk();
5490    for pair in mapping.named_children(&mut cursor) {
5491        if pair.kind() != "block_mapping_pair" && pair.kind() != "flow_pair" {
5492            continue;
5493        }
5494        let Some(key_node) = pair.child_by_field_name("key") else {
5495            continue;
5496        };
5497        let name = yaml_scalar_text(source, &key_node);
5498        if name.is_empty() {
5499            continue;
5500        }
5501        symbols.push(Symbol {
5502            name,
5503            kind: SymbolKind::Variable,
5504            range: node_range(&pair),
5505            signature: None,
5506            scope_chain: vec![],
5507            exported: false,
5508            parent: None,
5509        });
5510    }
5511}
5512
5513/// Extract symbols from a YAML stream. Handles multi-document (`---`) streams:
5514/// each document becomes a Kubernetes resource symbol (Tier 1) when it carries
5515/// apiVersion+kind, otherwise its top-level keys are emitted (Tier 2). Helm/Go
5516/// templated YAML that yields no parseable mapping degrades gracefully (the
5517/// document is skipped rather than failing the whole file).
5518fn extract_yaml_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
5519    let mut symbols = Vec::new();
5520    let mut cursor = root.walk();
5521    for doc in root.named_children(&mut cursor) {
5522        if doc.kind() != "document" {
5523            continue;
5524        }
5525        let Some(mapping) = yaml_find_mapping(&doc) else {
5526            continue;
5527        };
5528        if let Some(symbol) = yaml_k8s_resource_symbol(source, &doc, &mapping) {
5529            symbols.push(symbol);
5530        } else {
5531            yaml_generic_keys(source, &mapping, &mut symbols);
5532        }
5533    }
5534    Ok(symbols)
5535}
5536
5537fn scala_scope_chain(node: &Node, source: &str) -> Vec<String> {
5538    let mut chain = Vec::new();
5539    let mut current = node.parent();
5540
5541    while let Some(parent) = current {
5542        match parent.kind() {
5543            "class_definition" | "object_definition" | "enum_definition" | "trait_definition" => {
5544                if let Some(name_node) = parent.child_by_field_name("name") {
5545                    chain.push(node_text(source, &name_node).to_string());
5546                }
5547            }
5548            _ => {}
5549        }
5550        current = parent.parent();
5551    }
5552
5553    chain.reverse();
5554    chain
5555}
5556
5557fn extract_scala_symbols(
5558    source: &str,
5559    root: &Node,
5560    query: &Query,
5561) -> Result<Vec<Symbol>, AftError> {
5562    let lang = LangId::Scala;
5563    let capture_names = query.capture_names();
5564
5565    let mut symbols = Vec::new();
5566    let mut cursor = QueryCursor::new();
5567    let mut matches = cursor.matches(query, *root, source.as_bytes());
5568
5569    while let Some(m) = {
5570        matches.advance();
5571        matches.get()
5572    } {
5573        let mut class_name_node = None;
5574        let mut class_def_node = None;
5575        let mut object_name_node = None;
5576        let mut object_def_node = None;
5577        let mut enum_name_node = None;
5578        let mut enum_def_node = None;
5579        let mut trait_name_node = None;
5580        let mut trait_def_node = None;
5581        let mut fn_name_node = None;
5582        let mut fn_def_node = None;
5583        let mut val_name_node = None;
5584        let mut val_def_node = None;
5585        let mut var_name_node = None;
5586        let mut var_def_node = None;
5587        let mut type_name_node = None;
5588        let mut type_def_node = None;
5589
5590        for cap in m.captures {
5591            let Some(&name) = capture_names.get(cap.index as usize) else {
5592                continue;
5593            };
5594            match name {
5595                "class.name" => class_name_node = Some(cap.node),
5596                "class.def" => class_def_node = Some(cap.node),
5597                "object.name" => object_name_node = Some(cap.node),
5598                "object.def" => object_def_node = Some(cap.node),
5599                "enum.name" => enum_name_node = Some(cap.node),
5600                "enum.def" => enum_def_node = Some(cap.node),
5601                "trait.name" => trait_name_node = Some(cap.node),
5602                "trait.def" => trait_def_node = Some(cap.node),
5603                "fn.name" => fn_name_node = Some(cap.node),
5604                "fn.def" => fn_def_node = Some(cap.node),
5605                "val.name" => val_name_node = Some(cap.node),
5606                "val.def" => val_def_node = Some(cap.node),
5607                "var.name" => var_name_node = Some(cap.node),
5608                "var.def" => var_def_node = Some(cap.node),
5609                "given.name" => val_name_node = Some(cap.node),
5610                "given.def" => val_def_node = Some(cap.node),
5611                "type.name" => type_name_node = Some(cap.node),
5612                "type.def" => type_def_node = Some(cap.node),
5613                _ => {}
5614            }
5615        }
5616
5617        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
5618            symbols.push(Symbol {
5619                name: node_text(source, &name_node).to_string(),
5620                kind: SymbolKind::Class,
5621                range: node_range_with_decorators(&def_node, source, lang),
5622                signature: Some(extract_signature(source, &def_node)),
5623                scope_chain: scala_scope_chain(&def_node, source),
5624                exported: true,
5625                parent: scala_scope_chain(&def_node, source).last().cloned(),
5626            });
5627        }
5628
5629        if let (Some(name_node), Some(def_node)) = (object_name_node, object_def_node) {
5630            symbols.push(Symbol {
5631                name: node_text(source, &name_node).to_string(),
5632                kind: SymbolKind::Class,
5633                range: node_range_with_decorators(&def_node, source, lang),
5634                signature: Some(extract_signature(source, &def_node)),
5635                scope_chain: scala_scope_chain(&def_node, source),
5636                exported: true,
5637                parent: scala_scope_chain(&def_node, source).last().cloned(),
5638            });
5639        }
5640
5641        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
5642            symbols.push(Symbol {
5643                name: node_text(source, &name_node).to_string(),
5644                kind: SymbolKind::Enum,
5645                range: node_range_with_decorators(&def_node, source, lang),
5646                signature: Some(extract_signature(source, &def_node)),
5647                scope_chain: scala_scope_chain(&def_node, source),
5648                exported: true,
5649                parent: scala_scope_chain(&def_node, source).last().cloned(),
5650            });
5651        }
5652
5653        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
5654            symbols.push(Symbol {
5655                name: node_text(source, &name_node).to_string(),
5656                kind: SymbolKind::Interface,
5657                range: node_range_with_decorators(&def_node, source, lang),
5658                signature: Some(extract_signature(source, &def_node)),
5659                scope_chain: scala_scope_chain(&def_node, source),
5660                exported: true,
5661                parent: scala_scope_chain(&def_node, source).last().cloned(),
5662            });
5663        }
5664
5665        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
5666            let scope_chain = scala_scope_chain(&def_node, source);
5667            let kind = if scope_chain.is_empty() {
5668                SymbolKind::Function
5669            } else {
5670                SymbolKind::Method
5671            };
5672            symbols.push(Symbol {
5673                name: node_text(source, &name_node).to_string(),
5674                kind,
5675                range: node_range_with_decorators(&def_node, source, lang),
5676                signature: Some(extract_signature(source, &def_node)),
5677                parent: scope_chain.last().cloned(),
5678                scope_chain,
5679                exported: true,
5680            });
5681        }
5682
5683        if let (Some(name_node), Some(def_node)) = (val_name_node, val_def_node) {
5684            let scope_chain = scala_scope_chain(&def_node, source);
5685            symbols.push(Symbol {
5686                name: node_text(source, &name_node).to_string(),
5687                kind: SymbolKind::Variable,
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)) = (var_name_node, var_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)) = (type_name_node, type_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::TypeAlias,
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
5723    dedup_symbols(&mut symbols);
5724    Ok(symbols)
5725}
5726
5727fn child_text_by_field_or_kind(
5728    node: &Node,
5729    source: &str,
5730    field_name: &str,
5731    kinds: &[&str],
5732) -> Option<String> {
5733    if let Some(name_node) = node.child_by_field_name(field_name) {
5734        return Some(node_text(source, &name_node).to_string());
5735    }
5736
5737    let mut cursor = node.walk();
5738    if !cursor.goto_first_child() {
5739        return None;
5740    }
5741
5742    loop {
5743        let child = cursor.node();
5744        if kinds.contains(&child.kind()) {
5745            return Some(node_text(source, &child).to_string());
5746        }
5747        if !cursor.goto_next_sibling() {
5748            break;
5749        }
5750    }
5751
5752    None
5753}
5754
5755fn push_captured_symbol(
5756    symbols: &mut Vec<Symbol>,
5757    source: &str,
5758    lang: LangId,
5759    name_node: Node,
5760    def_node: Node,
5761    kind: SymbolKind,
5762    scope_chain: Vec<String>,
5763    exported: bool,
5764) {
5765    symbols.push(Symbol {
5766        name: node_text(source, &name_node).to_string(),
5767        kind,
5768        range: node_range_with_decorators(&def_node, source, lang),
5769        signature: Some(extract_signature(source, &def_node)),
5770        parent: scope_chain.last().cloned(),
5771        scope_chain,
5772        exported,
5773    });
5774}
5775
5776fn range_spanning_nodes(start_node: &Node, end_node: &Node) -> Range {
5777    let start = start_node.start_position();
5778    let end = end_node.end_position();
5779    Range {
5780        start_line: start.row as u32,
5781        start_col: start.column as u32,
5782        end_line: end.row as u32,
5783        end_col: end.column as u32,
5784    }
5785}
5786
5787fn extract_signature_between(source: &str, start_node: &Node, end_node: &Node) -> String {
5788    let start = start_node.start_byte();
5789    let end = end_node.end_byte();
5790    let text = source.get(start..end).unwrap_or_default();
5791    let first_line = text.lines().next().unwrap_or(text);
5792    let trimmed = first_line.trim_end();
5793    let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
5794    trimmed.to_string()
5795}
5796
5797fn groovy_scope_chain(node: &Node, source: &str) -> Vec<String> {
5798    let mut chain = Vec::new();
5799    let mut current = node.parent();
5800
5801    while let Some(parent) = current {
5802        if matches!(
5803            parent.kind(),
5804            "class_declaration"
5805                | "interface_declaration"
5806                | "trait_declaration"
5807                | "enum_declaration"
5808                | "record_declaration"
5809                | "annotation_type_declaration"
5810        ) {
5811            if let Some(name_node) = parent.child_by_field_name("name") {
5812                chain.push(node_text(source, &name_node).to_string());
5813            }
5814        }
5815        current = parent.parent();
5816    }
5817
5818    chain.reverse();
5819    chain
5820}
5821
5822// Gradle's `task foo { ... }` syntax parses as an expression statement for the
5823// `task` keyword followed by a separate command-chain node for the task name and
5824// body. Pair those adjacent siblings so outline shows the task as one symbol.
5825fn groovy_keyword_task_symbol(
5826    source: &str,
5827    keyword_stmt: Node,
5828    declaration: Node,
5829) -> Option<Symbol> {
5830    if keyword_stmt.kind() != "expression_statement" || declaration.kind() != "command_chain" {
5831        return None;
5832    }
5833
5834    let keyword = keyword_stmt.named_child(0)?;
5835    if keyword.kind() != "identifier" || node_text(source, &keyword) != "task" {
5836        return None;
5837    }
5838
5839    let name_node = declaration.child_by_field_name("receiver")?;
5840    if name_node.kind() != "identifier" {
5841        return None;
5842    }
5843
5844    let body_node = declaration.child_by_field_name("argument")?;
5845    if body_node.kind() != "closure" {
5846        return None;
5847    }
5848
5849    Some(Symbol {
5850        name: node_text(source, &name_node).to_string(),
5851        kind: SymbolKind::Function,
5852        range: range_spanning_nodes(&keyword_stmt, &declaration),
5853        signature: Some(extract_signature_between(
5854            source,
5855            &keyword_stmt,
5856            &declaration,
5857        )),
5858        scope_chain: vec![],
5859        exported: true,
5860        parent: None,
5861    })
5862}
5863
5864// `tasks.register("foo") { ... }` likewise arrives as two adjacent siblings:
5865// the registration call and a trailing closure expression. Pair them into one
5866// task symbol without scanning beyond the immediate statement pair.
5867fn groovy_registered_task_symbol(
5868    source: &str,
5869    invocation_stmt: Node,
5870    closure_stmt: Node,
5871) -> Option<Symbol> {
5872    if invocation_stmt.kind() != "expression_statement"
5873        || closure_stmt.kind() != "expression_statement"
5874    {
5875        return None;
5876    }
5877
5878    let invocation = invocation_stmt.named_child(0)?;
5879    if invocation.kind() != "method_invocation" {
5880        return None;
5881    }
5882
5883    let function = invocation.child_by_field_name("function")?;
5884    if function.kind() != "field_access" {
5885        return None;
5886    }
5887
5888    let object = function.child_by_field_name("object")?;
5889    let field = function.child_by_field_name("field")?;
5890    if node_text(source, &object) != "tasks" || node_text(source, &field) != "register" {
5891        return None;
5892    }
5893
5894    let arguments = invocation.child_by_field_name("arguments")?;
5895    let name_node = arguments.named_child(0)?;
5896    if name_node.kind() != "string_literal" {
5897        return None;
5898    }
5899
5900    let closure = closure_stmt.named_child(0)?;
5901    if closure.kind() != "closure" {
5902        return None;
5903    }
5904
5905    Some(Symbol {
5906        name: string_content(source, &name_node)?,
5907        kind: SymbolKind::Function,
5908        range: range_spanning_nodes(&invocation_stmt, &closure_stmt),
5909        signature: Some(extract_signature_between(
5910            source,
5911            &invocation_stmt,
5912            &closure_stmt,
5913        )),
5914        scope_chain: vec![],
5915        exported: true,
5916        parent: None,
5917    })
5918}
5919
5920fn collect_groovy_task_symbols(source: &str, root: &Node, symbols: &mut Vec<Symbol>) {
5921    let children: Vec<Node> = root.named_children(&mut root.walk()).collect();
5922    let mut index = 0;
5923    while index + 1 < children.len() {
5924        if let Some(symbol) =
5925            groovy_keyword_task_symbol(source, children[index], children[index + 1])
5926        {
5927            symbols.push(symbol);
5928            index += 2;
5929            continue;
5930        }
5931
5932        if let Some(symbol) =
5933            groovy_registered_task_symbol(source, children[index], children[index + 1])
5934        {
5935            symbols.push(symbol);
5936            index += 2;
5937            continue;
5938        }
5939
5940        index += 1;
5941    }
5942}
5943
5944fn java_scope_chain(node: &Node, source: &str) -> Vec<String> {
5945    let mut chain = Vec::new();
5946    let mut current = node.parent();
5947
5948    while let Some(parent) = current {
5949        if matches!(
5950            parent.kind(),
5951            "class_declaration"
5952                | "interface_declaration"
5953                | "annotation_type_declaration"
5954                | "enum_declaration"
5955                | "record_declaration"
5956        ) {
5957            if let Some(name_node) = parent.child_by_field_name("name") {
5958                chain.push(node_text(source, &name_node).to_string());
5959            }
5960        }
5961        current = parent.parent();
5962    }
5963
5964    chain.reverse();
5965    chain
5966}
5967
5968fn extract_java_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
5969    let lang = LangId::Java;
5970    let capture_names = query.capture_names();
5971    let mut symbols = Vec::new();
5972    let mut cursor = QueryCursor::new();
5973    let mut matches = cursor.matches(query, *root, source.as_bytes());
5974
5975    while let Some(m) = {
5976        matches.advance();
5977        matches.get()
5978    } {
5979        let mut class_name_node = None;
5980        let mut class_def_node = None;
5981        let mut interface_name_node = None;
5982        let mut interface_def_node = None;
5983        let mut enum_name_node = None;
5984        let mut enum_def_node = None;
5985        let mut struct_name_node = None;
5986        let mut struct_def_node = None;
5987        let mut fn_name_node = None;
5988        let mut fn_def_node = None;
5989        let mut var_name_node = None;
5990        let mut var_def_node = None;
5991
5992        for cap in m.captures {
5993            let Some(&name) = capture_names.get(cap.index as usize) else {
5994                continue;
5995            };
5996            match name {
5997                "class.name" => class_name_node = Some(cap.node),
5998                "class.def" => class_def_node = Some(cap.node),
5999                "interface.name" => interface_name_node = Some(cap.node),
6000                "interface.def" => interface_def_node = Some(cap.node),
6001                "enum.name" => enum_name_node = Some(cap.node),
6002                "enum.def" => enum_def_node = Some(cap.node),
6003                "struct.name" => struct_name_node = Some(cap.node),
6004                "struct.def" => struct_def_node = Some(cap.node),
6005                "fn.name" => fn_name_node = Some(cap.node),
6006                "fn.def" => fn_def_node = Some(cap.node),
6007                "var.name" => var_name_node = Some(cap.node),
6008                "var.def" => var_def_node = Some(cap.node),
6009                _ => {}
6010            }
6011        }
6012
6013        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
6014            push_captured_symbol(
6015                &mut symbols,
6016                source,
6017                lang,
6018                name_node,
6019                def_node,
6020                SymbolKind::Class,
6021                java_scope_chain(&def_node, source),
6022                true,
6023            );
6024        }
6025        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
6026            push_captured_symbol(
6027                &mut symbols,
6028                source,
6029                lang,
6030                name_node,
6031                def_node,
6032                SymbolKind::Interface,
6033                java_scope_chain(&def_node, source),
6034                true,
6035            );
6036        }
6037        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
6038            push_captured_symbol(
6039                &mut symbols,
6040                source,
6041                lang,
6042                name_node,
6043                def_node,
6044                SymbolKind::Enum,
6045                java_scope_chain(&def_node, source),
6046                true,
6047            );
6048        }
6049        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
6050            push_captured_symbol(
6051                &mut symbols,
6052                source,
6053                lang,
6054                name_node,
6055                def_node,
6056                SymbolKind::Struct,
6057                java_scope_chain(&def_node, source),
6058                true,
6059            );
6060        }
6061        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6062            let scope_chain = java_scope_chain(&def_node, source);
6063            let kind = if scope_chain.is_empty() {
6064                SymbolKind::Function
6065            } else {
6066                SymbolKind::Method
6067            };
6068            push_captured_symbol(
6069                &mut symbols,
6070                source,
6071                lang,
6072                name_node,
6073                def_node,
6074                kind,
6075                scope_chain,
6076                true,
6077            );
6078        }
6079        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6080            push_captured_symbol(
6081                &mut symbols,
6082                source,
6083                lang,
6084                name_node,
6085                def_node,
6086                SymbolKind::Variable,
6087                java_scope_chain(&def_node, source),
6088                true,
6089            );
6090        }
6091    }
6092
6093    dedup_symbols(&mut symbols);
6094    Ok(symbols)
6095}
6096
6097fn ruby_scope_chain(node: &Node, source: &str) -> Vec<String> {
6098    let mut chain = Vec::new();
6099    let mut current = node.parent();
6100
6101    while let Some(parent) = current {
6102        if matches!(parent.kind(), "class" | "module") {
6103            if let Some(name_node) = parent.child_by_field_name("name") {
6104                chain.push(node_text(source, &name_node).to_string());
6105            }
6106        }
6107        current = parent.parent();
6108    }
6109
6110    chain.reverse();
6111    chain
6112}
6113
6114fn extract_ruby_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
6115    let lang = LangId::Ruby;
6116    let capture_names = query.capture_names();
6117    let mut symbols = Vec::new();
6118    let mut cursor = QueryCursor::new();
6119    let mut matches = cursor.matches(query, *root, source.as_bytes());
6120
6121    while let Some(m) = {
6122        matches.advance();
6123        matches.get()
6124    } {
6125        let mut module_name_node = None;
6126        let mut module_def_node = None;
6127        let mut class_name_node = None;
6128        let mut class_def_node = None;
6129        let mut fn_name_node = None;
6130        let mut fn_def_node = None;
6131        let mut var_name_node = None;
6132        let mut var_def_node = None;
6133
6134        for cap in m.captures {
6135            let Some(&name) = capture_names.get(cap.index as usize) else {
6136                continue;
6137            };
6138            match name {
6139                "module.name" => module_name_node = Some(cap.node),
6140                "module.def" => module_def_node = Some(cap.node),
6141                "class.name" => class_name_node = Some(cap.node),
6142                "class.def" => class_def_node = Some(cap.node),
6143                "fn.name" => fn_name_node = Some(cap.node),
6144                "fn.def" => fn_def_node = Some(cap.node),
6145                "var.name" => var_name_node = Some(cap.node),
6146                "var.def" => var_def_node = Some(cap.node),
6147                _ => {}
6148            }
6149        }
6150
6151        if let (Some(name_node), Some(def_node)) = (module_name_node, module_def_node) {
6152            push_captured_symbol(
6153                &mut symbols,
6154                source,
6155                lang,
6156                name_node,
6157                def_node,
6158                SymbolKind::Class,
6159                ruby_scope_chain(&def_node, source),
6160                true,
6161            );
6162        }
6163        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
6164            push_captured_symbol(
6165                &mut symbols,
6166                source,
6167                lang,
6168                name_node,
6169                def_node,
6170                SymbolKind::Class,
6171                ruby_scope_chain(&def_node, source),
6172                true,
6173            );
6174        }
6175        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6176            let scope_chain = ruby_scope_chain(&def_node, source);
6177            let kind = if scope_chain.is_empty() {
6178                SymbolKind::Function
6179            } else {
6180                SymbolKind::Method
6181            };
6182            push_captured_symbol(
6183                &mut symbols,
6184                source,
6185                lang,
6186                name_node,
6187                def_node,
6188                kind,
6189                scope_chain,
6190                true,
6191            );
6192        }
6193        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6194            push_captured_symbol(
6195                &mut symbols,
6196                source,
6197                lang,
6198                name_node,
6199                def_node,
6200                SymbolKind::Variable,
6201                ruby_scope_chain(&def_node, source),
6202                true,
6203            );
6204        }
6205    }
6206
6207    dedup_symbols(&mut symbols);
6208    Ok(symbols)
6209}
6210
6211fn kotlin_scope_chain(node: &Node, source: &str) -> Vec<String> {
6212    let mut chain = Vec::new();
6213    let mut current = node.parent();
6214
6215    while let Some(parent) = current {
6216        if matches!(parent.kind(), "class_declaration" | "object_declaration") {
6217            if let Some(name) =
6218                child_text_by_field_or_kind(&parent, source, "name", &["type_identifier"])
6219            {
6220                chain.push(name);
6221            }
6222        }
6223        current = parent.parent();
6224    }
6225
6226    chain.reverse();
6227    chain
6228}
6229
6230fn extract_kotlin_symbols(
6231    source: &str,
6232    root: &Node,
6233    query: &Query,
6234) -> Result<Vec<Symbol>, AftError> {
6235    let lang = LangId::Kotlin;
6236    let capture_names = query.capture_names();
6237    let mut symbols = Vec::new();
6238    let mut cursor = QueryCursor::new();
6239    let mut matches = cursor.matches(query, *root, source.as_bytes());
6240
6241    while let Some(m) = {
6242        matches.advance();
6243        matches.get()
6244    } {
6245        let mut class_name_node = None;
6246        let mut class_def_node = None;
6247        let mut object_name_node = None;
6248        let mut object_def_node = None;
6249        let mut fn_name_node = None;
6250        let mut fn_def_node = None;
6251        let mut var_name_node = None;
6252        let mut var_def_node = None;
6253        let mut type_name_node = None;
6254        let mut type_def_node = None;
6255
6256        for cap in m.captures {
6257            let Some(&name) = capture_names.get(cap.index as usize) else {
6258                continue;
6259            };
6260            match name {
6261                "class.name" => class_name_node = Some(cap.node),
6262                "class.def" => class_def_node = Some(cap.node),
6263                "object.name" => object_name_node = Some(cap.node),
6264                "object.def" => object_def_node = Some(cap.node),
6265                "fn.name" => fn_name_node = Some(cap.node),
6266                "fn.def" => fn_def_node = Some(cap.node),
6267                "var.name" => var_name_node = Some(cap.node),
6268                "var.def" => var_def_node = Some(cap.node),
6269                "type.name" => type_name_node = Some(cap.node),
6270                "type.def" => type_def_node = Some(cap.node),
6271                _ => {}
6272            }
6273        }
6274
6275        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
6276            push_captured_symbol(
6277                &mut symbols,
6278                source,
6279                lang,
6280                name_node,
6281                def_node,
6282                SymbolKind::Class,
6283                kotlin_scope_chain(&def_node, source),
6284                true,
6285            );
6286        }
6287        if let (Some(name_node), Some(def_node)) = (object_name_node, object_def_node) {
6288            push_captured_symbol(
6289                &mut symbols,
6290                source,
6291                lang,
6292                name_node,
6293                def_node,
6294                SymbolKind::Class,
6295                kotlin_scope_chain(&def_node, source),
6296                true,
6297            );
6298        }
6299        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6300            let scope_chain = kotlin_scope_chain(&def_node, source);
6301            let kind = if scope_chain.is_empty() {
6302                SymbolKind::Function
6303            } else {
6304                SymbolKind::Method
6305            };
6306            push_captured_symbol(
6307                &mut symbols,
6308                source,
6309                lang,
6310                name_node,
6311                def_node,
6312                kind,
6313                scope_chain,
6314                true,
6315            );
6316        }
6317        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6318            push_captured_symbol(
6319                &mut symbols,
6320                source,
6321                lang,
6322                name_node,
6323                def_node,
6324                SymbolKind::Variable,
6325                kotlin_scope_chain(&def_node, source),
6326                true,
6327            );
6328        }
6329        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
6330            push_captured_symbol(
6331                &mut symbols,
6332                source,
6333                lang,
6334                name_node,
6335                def_node,
6336                SymbolKind::TypeAlias,
6337                kotlin_scope_chain(&def_node, source),
6338                true,
6339            );
6340        }
6341    }
6342
6343    dedup_symbols(&mut symbols);
6344    Ok(symbols)
6345}
6346
6347fn swift_scope_chain(node: &Node, source: &str) -> Vec<String> {
6348    let mut chain = Vec::new();
6349    let mut current = node.parent();
6350
6351    while let Some(parent) = current {
6352        if matches!(parent.kind(), "class_declaration" | "protocol_declaration") {
6353            if let Some(name_node) = parent.child_by_field_name("name") {
6354                chain.push(node_text(source, &name_node).to_string());
6355            }
6356        }
6357        current = parent.parent();
6358    }
6359
6360    chain.reverse();
6361    chain
6362}
6363
6364fn swift_type_kind(source: &str, node: &Node) -> SymbolKind {
6365    match node
6366        .child_by_field_name("declaration_kind")
6367        .map(|kind_node| node_text(source, &kind_node))
6368    {
6369        Some("struct") => SymbolKind::Struct,
6370        Some("enum") => SymbolKind::Enum,
6371        _ => SymbolKind::Class,
6372    }
6373}
6374
6375fn extract_swift_symbols(
6376    source: &str,
6377    root: &Node,
6378    query: &Query,
6379) -> Result<Vec<Symbol>, AftError> {
6380    let lang = LangId::Swift;
6381    let capture_names = query.capture_names();
6382    let mut symbols = Vec::new();
6383    let mut cursor = QueryCursor::new();
6384    let mut matches = cursor.matches(query, *root, source.as_bytes());
6385
6386    while let Some(m) = {
6387        matches.advance();
6388        matches.get()
6389    } {
6390        let mut class_name_node = None;
6391        let mut class_def_node = None;
6392        let mut interface_name_node = None;
6393        let mut interface_def_node = None;
6394        let mut fn_name_node = None;
6395        let mut fn_def_node = None;
6396        let mut var_name_node = None;
6397        let mut var_def_node = None;
6398        let mut type_name_node = None;
6399        let mut type_def_node = None;
6400
6401        for cap in m.captures {
6402            let Some(&name) = capture_names.get(cap.index as usize) else {
6403                continue;
6404            };
6405            match name {
6406                "class.name" => class_name_node = Some(cap.node),
6407                "class.def" => class_def_node = Some(cap.node),
6408                "interface.name" => interface_name_node = Some(cap.node),
6409                "interface.def" => interface_def_node = Some(cap.node),
6410                "fn.name" => fn_name_node = Some(cap.node),
6411                "fn.def" => fn_def_node = Some(cap.node),
6412                "var.name" => var_name_node = Some(cap.node),
6413                "var.def" => var_def_node = Some(cap.node),
6414                "type.name" => type_name_node = Some(cap.node),
6415                "type.def" => type_def_node = Some(cap.node),
6416                _ => {}
6417            }
6418        }
6419
6420        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
6421            let kind = swift_type_kind(source, &def_node);
6422            push_captured_symbol(
6423                &mut symbols,
6424                source,
6425                lang,
6426                name_node,
6427                def_node,
6428                kind,
6429                swift_scope_chain(&def_node, source),
6430                true,
6431            );
6432        }
6433        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
6434            push_captured_symbol(
6435                &mut symbols,
6436                source,
6437                lang,
6438                name_node,
6439                def_node,
6440                SymbolKind::Interface,
6441                swift_scope_chain(&def_node, source),
6442                true,
6443            );
6444        }
6445        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6446            let scope_chain = swift_scope_chain(&def_node, source);
6447            let kind = if scope_chain.is_empty() {
6448                SymbolKind::Function
6449            } else {
6450                SymbolKind::Method
6451            };
6452            push_captured_symbol(
6453                &mut symbols,
6454                source,
6455                lang,
6456                name_node,
6457                def_node,
6458                kind,
6459                scope_chain,
6460                true,
6461            );
6462        }
6463        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6464            push_captured_symbol(
6465                &mut symbols,
6466                source,
6467                lang,
6468                name_node,
6469                def_node,
6470                SymbolKind::Variable,
6471                swift_scope_chain(&def_node, source),
6472                true,
6473            );
6474        }
6475        if let (Some(name_node), Some(def_node)) = (type_name_node, type_def_node) {
6476            push_captured_symbol(
6477                &mut symbols,
6478                source,
6479                lang,
6480                name_node,
6481                def_node,
6482                SymbolKind::TypeAlias,
6483                swift_scope_chain(&def_node, source),
6484                true,
6485            );
6486        }
6487    }
6488
6489    dedup_symbols(&mut symbols);
6490    Ok(symbols)
6491}
6492
6493fn php_scope_chain(node: &Node, source: &str) -> Vec<String> {
6494    let mut chain = Vec::new();
6495    let mut current = node.parent();
6496
6497    while let Some(parent) = current {
6498        match parent.kind() {
6499            "namespace_definition"
6500            | "class_declaration"
6501            | "interface_declaration"
6502            | "trait_declaration"
6503            | "enum_declaration" => {
6504                if let Some(name_node) = parent.child_by_field_name("name") {
6505                    chain.push(node_text(source, &name_node).to_string());
6506                }
6507            }
6508            _ => {}
6509        }
6510        current = parent.parent();
6511    }
6512
6513    chain.reverse();
6514    chain
6515}
6516
6517fn extract_scss_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
6518    let lang = LangId::Scss;
6519    let capture_names = query.capture_names();
6520    let mut symbols = Vec::new();
6521    let mut cursor = QueryCursor::new();
6522    let mut matches = cursor.matches(query, *root, source.as_bytes());
6523
6524    while let Some(m) = {
6525        matches.advance();
6526        matches.get()
6527    } {
6528        let mut mixin_name_node = None;
6529        let mut mixin_def_node = None;
6530        let mut fn_name_node = None;
6531        let mut fn_def_node = None;
6532        let mut var_name_node = None;
6533        let mut var_def_node = None;
6534        let mut selector_name_node = None;
6535        let mut selector_def_node = None;
6536
6537        for cap in m.captures {
6538            let Some(&name) = capture_names.get(cap.index as usize) else {
6539                continue;
6540            };
6541            match name {
6542                "mixin.name" => mixin_name_node = Some(cap.node),
6543                "mixin.def" => mixin_def_node = Some(cap.node),
6544                "fn.name" => fn_name_node = Some(cap.node),
6545                "fn.def" => fn_def_node = Some(cap.node),
6546                "var.name" => var_name_node = Some(cap.node),
6547                "var.def" => var_def_node = Some(cap.node),
6548                "selector.name" => selector_name_node = Some(cap.node),
6549                "selector.def" => selector_def_node = Some(cap.node),
6550                _ => {}
6551            }
6552        }
6553
6554        if let (Some(name_node), Some(def_node)) = (mixin_name_node, mixin_def_node) {
6555            push_captured_symbol(
6556                &mut symbols,
6557                source,
6558                lang,
6559                name_node,
6560                def_node,
6561                SymbolKind::Function,
6562                vec![],
6563                true,
6564            );
6565        }
6566        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6567            push_captured_symbol(
6568                &mut symbols,
6569                source,
6570                lang,
6571                name_node,
6572                def_node,
6573                SymbolKind::Function,
6574                vec![],
6575                true,
6576            );
6577        }
6578        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6579            if !node_text(source, &name_node).starts_with('$') {
6580                continue;
6581            }
6582            push_captured_symbol(
6583                &mut symbols,
6584                source,
6585                lang,
6586                name_node,
6587                def_node,
6588                SymbolKind::Variable,
6589                vec![],
6590                true,
6591            );
6592        }
6593        if let (Some(name_node), Some(def_node)) = (selector_name_node, selector_def_node) {
6594            push_captured_symbol(
6595                &mut symbols,
6596                source,
6597                lang,
6598                name_node,
6599                def_node,
6600                SymbolKind::Class,
6601                vec![],
6602                true,
6603            );
6604        }
6605    }
6606
6607    Ok(symbols)
6608}
6609
6610fn extract_php_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
6611    let lang = LangId::Php;
6612    let capture_names = query.capture_names();
6613    let mut symbols = Vec::new();
6614    let mut cursor = QueryCursor::new();
6615    let mut matches = cursor.matches(query, *root, source.as_bytes());
6616
6617    while let Some(m) = {
6618        matches.advance();
6619        matches.get()
6620    } {
6621        let mut namespace_name_node = None;
6622        let mut namespace_def_node = None;
6623        let mut class_name_node = None;
6624        let mut class_def_node = None;
6625        let mut interface_name_node = None;
6626        let mut interface_def_node = None;
6627        let mut trait_name_node = None;
6628        let mut trait_def_node = None;
6629        let mut enum_name_node = None;
6630        let mut enum_def_node = None;
6631        let mut fn_name_node = None;
6632        let mut fn_def_node = None;
6633        let mut var_name_node = None;
6634        let mut var_def_node = None;
6635
6636        for cap in m.captures {
6637            let Some(&name) = capture_names.get(cap.index as usize) else {
6638                continue;
6639            };
6640            match name {
6641                "namespace.name" => namespace_name_node = Some(cap.node),
6642                "namespace.def" => namespace_def_node = Some(cap.node),
6643                "class.name" => class_name_node = Some(cap.node),
6644                "class.def" => class_def_node = Some(cap.node),
6645                "interface.name" => interface_name_node = Some(cap.node),
6646                "interface.def" => interface_def_node = Some(cap.node),
6647                "trait.name" => trait_name_node = Some(cap.node),
6648                "trait.def" => trait_def_node = Some(cap.node),
6649                "enum.name" => enum_name_node = Some(cap.node),
6650                "enum.def" => enum_def_node = Some(cap.node),
6651                "fn.name" => fn_name_node = Some(cap.node),
6652                "fn.def" => fn_def_node = Some(cap.node),
6653                "var.name" => var_name_node = Some(cap.node),
6654                "var.def" => var_def_node = Some(cap.node),
6655                _ => {}
6656            }
6657        }
6658
6659        if let (Some(name_node), Some(def_node)) = (namespace_name_node, namespace_def_node) {
6660            push_captured_symbol(
6661                &mut symbols,
6662                source,
6663                lang,
6664                name_node,
6665                def_node,
6666                SymbolKind::Class,
6667                php_scope_chain(&def_node, source),
6668                true,
6669            );
6670        }
6671        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
6672            push_captured_symbol(
6673                &mut symbols,
6674                source,
6675                lang,
6676                name_node,
6677                def_node,
6678                SymbolKind::Class,
6679                php_scope_chain(&def_node, source),
6680                true,
6681            );
6682        }
6683        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
6684            push_captured_symbol(
6685                &mut symbols,
6686                source,
6687                lang,
6688                name_node,
6689                def_node,
6690                SymbolKind::Interface,
6691                php_scope_chain(&def_node, source),
6692                true,
6693            );
6694        }
6695        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
6696            push_captured_symbol(
6697                &mut symbols,
6698                source,
6699                lang,
6700                name_node,
6701                def_node,
6702                SymbolKind::Interface,
6703                php_scope_chain(&def_node, source),
6704                true,
6705            );
6706        }
6707        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
6708            push_captured_symbol(
6709                &mut symbols,
6710                source,
6711                lang,
6712                name_node,
6713                def_node,
6714                SymbolKind::Enum,
6715                php_scope_chain(&def_node, source),
6716                true,
6717            );
6718        }
6719        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6720            let scope_chain = php_scope_chain(&def_node, source);
6721            let kind = if scope_chain.is_empty() || def_node.kind() == "function_definition" {
6722                SymbolKind::Function
6723            } else {
6724                SymbolKind::Method
6725            };
6726            push_captured_symbol(
6727                &mut symbols,
6728                source,
6729                lang,
6730                name_node,
6731                def_node,
6732                kind,
6733                scope_chain,
6734                true,
6735            );
6736        }
6737        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6738            push_captured_symbol(
6739                &mut symbols,
6740                source,
6741                lang,
6742                name_node,
6743                def_node,
6744                SymbolKind::Variable,
6745                php_scope_chain(&def_node, source),
6746                true,
6747            );
6748        }
6749    }
6750
6751    dedup_symbols(&mut symbols);
6752    Ok(symbols)
6753}
6754
6755fn lua_scope_chain(node: &Node, source: &str) -> Vec<String> {
6756    let mut chain = Vec::new();
6757
6758    if node.kind() == "function_declaration" {
6759        if let Some(name_node) = node.child_by_field_name("name") {
6760            match name_node.kind() {
6761                "dot_index_expression" | "method_index_expression" => {
6762                    if let Some(table_node) = name_node.child_by_field_name("table") {
6763                        chain.push(node_text(source, &table_node).to_string());
6764                    }
6765                }
6766                _ => {}
6767            }
6768        }
6769    }
6770
6771    chain
6772}
6773
6774fn extract_lua_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
6775    let lang = LangId::Lua;
6776    let capture_names = query.capture_names();
6777    let mut symbols = Vec::new();
6778    let mut cursor = QueryCursor::new();
6779    let mut matches = cursor.matches(query, *root, source.as_bytes());
6780
6781    while let Some(m) = {
6782        matches.advance();
6783        matches.get()
6784    } {
6785        let mut fn_name_node = None;
6786        let mut fn_def_node = None;
6787        let mut var_name_node = None;
6788        let mut var_def_node = None;
6789
6790        for cap in m.captures {
6791            let Some(&name) = capture_names.get(cap.index as usize) else {
6792                continue;
6793            };
6794            match name {
6795                "fn.name" => fn_name_node = Some(cap.node),
6796                "fn.def" => fn_def_node = Some(cap.node),
6797                "var.name" => var_name_node = Some(cap.node),
6798                "var.def" => var_def_node = Some(cap.node),
6799                _ => {}
6800            }
6801        }
6802
6803        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6804            let scope_chain = lua_scope_chain(&def_node, source);
6805            let kind = if scope_chain.is_empty() {
6806                SymbolKind::Function
6807            } else {
6808                SymbolKind::Method
6809            };
6810            push_captured_symbol(
6811                &mut symbols,
6812                source,
6813                lang,
6814                name_node,
6815                def_node,
6816                kind,
6817                scope_chain,
6818                true,
6819            );
6820        }
6821        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6822            push_captured_symbol(
6823                &mut symbols,
6824                source,
6825                lang,
6826                name_node,
6827                def_node,
6828                SymbolKind::Variable,
6829                vec![],
6830                true,
6831            );
6832        }
6833    }
6834
6835    dedup_symbols(&mut symbols);
6836    Ok(symbols)
6837}
6838
6839fn perl_package_name(source: &str, node: &Node) -> Option<String> {
6840    let mut cursor = node.walk();
6841    if !cursor.goto_first_child() {
6842        return None;
6843    }
6844
6845    loop {
6846        let child = cursor.node();
6847        if child.kind() == "package" {
6848            return Some(node_text(source, &child).to_string());
6849        }
6850        if !cursor.goto_next_sibling() {
6851            break;
6852        }
6853    }
6854
6855    None
6856}
6857
6858fn perl_scope_chain(node: &Node, source: &str) -> Vec<String> {
6859    let mut chain = Vec::new();
6860    let mut current = node.parent();
6861
6862    while let Some(parent) = current {
6863        if parent.kind() == "package_statement" {
6864            if let Some(name) = perl_package_name(source, &parent) {
6865                chain.push(name);
6866            }
6867        }
6868        current = parent.parent();
6869    }
6870
6871    if chain.is_empty() && node.kind() != "package_statement" {
6872        let mut sibling = node.prev_sibling();
6873        while let Some(prev) = sibling {
6874            if prev.kind() == "package_statement" {
6875                if let Some(name) = perl_package_name(source, &prev) {
6876                    chain.push(name);
6877                }
6878                break;
6879            }
6880            sibling = prev.prev_sibling();
6881        }
6882    }
6883
6884    chain.reverse();
6885    chain
6886}
6887
6888fn extract_perl_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
6889    let lang = LangId::Perl;
6890    let capture_names = query.capture_names();
6891    let mut symbols = Vec::new();
6892    let mut cursor = QueryCursor::new();
6893    let mut matches = cursor.matches(query, *root, source.as_bytes());
6894
6895    while let Some(m) = {
6896        matches.advance();
6897        matches.get()
6898    } {
6899        let mut package_name_node = None;
6900        let mut package_def_node = None;
6901        let mut fn_name_node = None;
6902        let mut fn_def_node = None;
6903        let mut var_name_node = None;
6904        let mut var_def_node = None;
6905        let mut const_pragma_node = None;
6906        let mut const_name_node = None;
6907        let mut const_def_node = None;
6908
6909        for cap in m.captures {
6910            let Some(&name) = capture_names.get(cap.index as usize) else {
6911                continue;
6912            };
6913            match name {
6914                "package.name" => package_name_node = Some(cap.node),
6915                "package.def" => package_def_node = Some(cap.node),
6916                "fn.name" => fn_name_node = Some(cap.node),
6917                "fn.def" => fn_def_node = Some(cap.node),
6918                "var.name" => var_name_node = Some(cap.node),
6919                "var.def" => var_def_node = Some(cap.node),
6920                "const.pragma" => const_pragma_node = Some(cap.node),
6921                "const.name" => const_name_node = Some(cap.node),
6922                "const.def" => const_def_node = Some(cap.node),
6923                _ => {}
6924            }
6925        }
6926
6927        // `use constant NAME => ...;` defines a constant. Our grammar parses every
6928        // `use`/`no` pragma as a generic `use_statement`, so gate on the pragma
6929        // module text to avoid treating e.g. `use parent -norequire, ...` as a
6930        // constant definition.
6931        if let (Some(pragma_node), Some(name_node), Some(def_node)) =
6932            (const_pragma_node, const_name_node, const_def_node)
6933        {
6934            if node_text(source, &pragma_node) == "constant" {
6935                var_name_node = Some(name_node);
6936                var_def_node = Some(def_node);
6937            }
6938        }
6939
6940        if let (Some(name_node), Some(def_node)) = (package_name_node, package_def_node) {
6941            push_captured_symbol(
6942                &mut symbols,
6943                source,
6944                lang,
6945                name_node,
6946                def_node,
6947                SymbolKind::Class,
6948                vec![],
6949                true,
6950            );
6951        }
6952        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
6953            let scope_chain = perl_scope_chain(&def_node, source);
6954            let kind = if scope_chain.is_empty() {
6955                SymbolKind::Function
6956            } else {
6957                SymbolKind::Method
6958            };
6959            push_captured_symbol(
6960                &mut symbols,
6961                source,
6962                lang,
6963                name_node,
6964                def_node,
6965                kind,
6966                scope_chain,
6967                true,
6968            );
6969        }
6970        if let (Some(name_node), Some(def_node)) = (var_name_node, var_def_node) {
6971            push_captured_symbol(
6972                &mut symbols,
6973                source,
6974                lang,
6975                name_node,
6976                def_node,
6977                SymbolKind::Variable,
6978                perl_scope_chain(&def_node, source),
6979                true,
6980            );
6981        }
6982    }
6983
6984    dedup_symbols(&mut symbols);
6985    Ok(symbols)
6986}
6987
6988fn extract_vue_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
6989    let mut symbols = Vec::new();
6990    collect_vue_sections(source, root, &mut symbols, true);
6991    dedup_symbols(&mut symbols);
6992    Ok(symbols)
6993}
6994
6995fn collect_vue_sections(
6996    source: &str,
6997    node: &Node,
6998    symbols: &mut Vec<Symbol>,
6999    allow_sections: bool,
7000) {
7001    let mut cursor = node.walk();
7002    if !cursor.goto_first_child() {
7003        return;
7004    }
7005
7006    loop {
7007        let child = cursor.node();
7008        if let Some(section_name) = vue_section_name(&child) {
7009            if allow_sections {
7010                symbols.push(Symbol {
7011                    name: section_name.to_string(),
7012                    kind: SymbolKind::Heading,
7013                    range: node_range(&child),
7014                    signature: vue_opening_tag_signature(source, &child),
7015                    scope_chain: vec![],
7016                    exported: false,
7017                    parent: None,
7018                });
7019            }
7020        } else {
7021            collect_vue_sections(
7022                source,
7023                &child,
7024                symbols,
7025                allow_sections && child.kind() == "document",
7026            );
7027        }
7028
7029        if !cursor.goto_next_sibling() {
7030            break;
7031        }
7032    }
7033}
7034
7035fn vue_section_name(node: &Node) -> Option<&'static str> {
7036    match node.kind() {
7037        "template_element" => Some("template"),
7038        "script_element" => Some("script"),
7039        "style_element" => Some("style"),
7040        _ => None,
7041    }
7042}
7043
7044fn vue_opening_tag_signature(source: &str, node: &Node) -> Option<String> {
7045    find_child_by_kind(*node, "start_tag")
7046        .or_else(|| find_child_by_kind(*node, "script_start_tag"))
7047        .or_else(|| find_child_by_kind(*node, "style_start_tag"))
7048        .or_else(|| find_child_by_kind(*node, "template_start_tag"))
7049        .map(|tag| node_text(source, &tag).trim().to_string())
7050}
7051
7052fn source_line_end_col(source: &str, line: u32) -> u32 {
7053    source
7054        .lines()
7055        .nth(line as usize)
7056        .map(|line| line.len() as u32)
7057        .unwrap_or(0)
7058}
7059
7060fn extract_html_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
7061    let mut headings = Vec::new();
7062    collect_html_headings(source, root, &mut headings);
7063
7064    let total_lines = source.lines().count() as u32;
7065
7066    // Extend each heading's end_line to just before the next heading at the
7067    // same or shallower level (or EOF). This makes aft_zoom return the full
7068    // section content rather than just the heading element's single line.
7069    for i in 0..headings.len() {
7070        let level = headings[i].level;
7071        let section_end = headings[i + 1..]
7072            .iter()
7073            .find(|heading| heading.level <= level)
7074            .map(|heading| heading.symbol.range.start_line.saturating_sub(1))
7075            .unwrap_or_else(|| total_lines.saturating_sub(1));
7076        headings[i].symbol.range.end_line = section_end;
7077        if section_end != headings[i].symbol.range.start_line {
7078            headings[i].symbol.range.end_col = source_line_end_col(source, section_end);
7079        }
7080    }
7081
7082    // Build hierarchy: assign scope_chain and parent based on heading level
7083    let mut scope_stack: Vec<(u8, String)> = Vec::new(); // (level, name)
7084    for heading in &mut headings {
7085        // Pop scope entries that are at the same level or deeper
7086        while scope_stack
7087            .last()
7088            .is_some_and(|(level, _)| *level >= heading.level)
7089        {
7090            scope_stack.pop();
7091        }
7092        heading.symbol.scope_chain = scope_stack.iter().map(|(_, name)| name.clone()).collect();
7093        heading.symbol.parent = scope_stack.last().map(|(_, name)| name.clone());
7094        scope_stack.push((heading.level, heading.symbol.name.clone()));
7095    }
7096
7097    Ok(headings.into_iter().map(|heading| heading.symbol).collect())
7098}
7099
7100/// A parsed HTML heading and its explicit `id` or legacy `name` URL anchor.
7101struct HtmlHeading {
7102    level: u8,
7103    symbol: Symbol,
7104    anchor_id: Option<String>,
7105}
7106
7107/// Recursively collect h1-h6 elements from the HTML tree.
7108fn collect_html_headings(source: &str, node: &Node, headings: &mut Vec<HtmlHeading>) {
7109    let mut cursor = node.walk();
7110    if !cursor.goto_first_child() {
7111        return;
7112    }
7113
7114    loop {
7115        let child = cursor.node();
7116        if child.kind() == "element" {
7117            // Check if this element's start tag is h1-h6
7118            if let Some(start_tag) = child
7119                .child_by_field_name("start_tag")
7120                .or_else(|| child.child(0).filter(|c| c.kind() == "start_tag"))
7121            {
7122                if let Some(tag_name_node) = start_tag
7123                    .child_by_field_name("tag_name")
7124                    .or_else(|| start_tag.child(1).filter(|c| c.kind() == "tag_name"))
7125                {
7126                    let tag_name = node_text(source, &tag_name_node).to_lowercase();
7127                    if let Some(level) = match tag_name.as_str() {
7128                        "h1" => Some(1u8),
7129                        "h2" => Some(2),
7130                        "h3" => Some(3),
7131                        "h4" => Some(4),
7132                        "h5" => Some(5),
7133                        "h6" => Some(6),
7134                        _ => None,
7135                    } {
7136                        // Extract text content from the element
7137                        let text = extract_element_text(source, &child).trim().to_string();
7138                        if !text.is_empty() {
7139                            let range = node_range(&child);
7140                            let signature = format!("<h{}> {}", level, text);
7141                            headings.push(HtmlHeading {
7142                                level,
7143                                anchor_id: html_heading_anchor_id(source, &start_tag),
7144                                symbol: Symbol {
7145                                    name: text,
7146                                    kind: SymbolKind::Heading,
7147                                    range,
7148                                    signature: Some(signature),
7149                                    scope_chain: vec![], // filled later
7150                                    exported: false,
7151                                    parent: None, // filled later
7152                                },
7153                            });
7154                        }
7155                    }
7156                }
7157            }
7158            // Recurse into element children (nested headings)
7159            collect_html_headings(source, &child, headings);
7160        } else {
7161            // Recurse into other node types (document, body, etc.)
7162            collect_html_headings(source, &child, headings);
7163        }
7164
7165        if !cursor.goto_next_sibling() {
7166            break;
7167        }
7168    }
7169}
7170
7171fn extract_html_heading_anchors(source: &str, root: &Node) -> Vec<crate::language::HeadingAnchor> {
7172    let mut headings = Vec::new();
7173    collect_html_headings(source, root, &mut headings);
7174    headings
7175        .into_iter()
7176        .filter_map(|heading| {
7177            heading.anchor_id.map(|id| crate::language::HeadingAnchor {
7178                start_line: heading.symbol.range.start_line,
7179                start_col: heading.symbol.range.start_col,
7180                id,
7181            })
7182        })
7183        .collect()
7184}
7185
7186fn html_heading_anchor_id(source: &str, start_tag: &Node) -> Option<String> {
7187    for expected_name in ["id", "name"] {
7188        let mut cursor = start_tag.walk();
7189        for attribute in start_tag.named_children(&mut cursor) {
7190            if attribute.kind() != "attribute" {
7191                continue;
7192            }
7193            let Some(name_node) = attribute
7194                .child_by_field_name("name")
7195                .or_else(|| find_child_by_kind(attribute, "attribute_name"))
7196            else {
7197                continue;
7198            };
7199            if !node_text(source, &name_node).eq_ignore_ascii_case(expected_name) {
7200                continue;
7201            }
7202            let Some(value_node) = attribute
7203                .child_by_field_name("value")
7204                .or_else(|| find_child_by_kind(attribute, "quoted_attribute_value"))
7205                .or_else(|| find_child_by_kind(attribute, "attribute_value"))
7206            else {
7207                continue;
7208            };
7209            let raw_value = node_text(source, &value_node).trim();
7210            let value = raw_value
7211                .strip_prefix('"')
7212                .and_then(|value| value.strip_suffix('"'))
7213                .or_else(|| {
7214                    raw_value
7215                        .strip_prefix('\'')
7216                        .and_then(|value| value.strip_suffix('\''))
7217                })
7218                .unwrap_or(raw_value)
7219                .trim();
7220            if !value.is_empty() {
7221                return Some(value.to_string());
7222            }
7223        }
7224    }
7225    None
7226}
7227
7228/// Extract text content from an HTML element, stripping tags.
7229fn extract_element_text(source: &str, node: &Node) -> String {
7230    let mut text = String::new();
7231    let mut cursor = node.walk();
7232    if !cursor.goto_first_child() {
7233        return text;
7234    }
7235    loop {
7236        let child = cursor.node();
7237        match child.kind() {
7238            "text" => {
7239                text.push_str(node_text(source, &child));
7240            }
7241            "element" => {
7242                // Recurse into nested elements (e.g., <strong>, <em>, <a>)
7243                text.push_str(&extract_element_text(source, &child));
7244            }
7245            _ => {}
7246        }
7247        if !cursor.goto_next_sibling() {
7248            break;
7249        }
7250    }
7251    text
7252}
7253
7254struct RawHeading {
7255    name: String,
7256    level: u8,
7257    range: Range,
7258}
7259
7260fn parse_atx_heading(source: &str, node: &Node) -> Option<(String, u8)> {
7261    let mut heading_level = 1;
7262    let mut heading_name = String::new();
7263    let mut cursor = node.walk();
7264    if cursor.goto_first_child() {
7265        loop {
7266            let child = cursor.node();
7267            let kind = child.kind();
7268            if kind.starts_with("atx_h") && kind.ends_with("_marker") {
7269                heading_level = kind
7270                    .strip_prefix("atx_h")
7271                    .and_then(|s| s.strip_suffix("_marker"))
7272                    .and_then(|s| s.parse::<u8>().ok())
7273                    .unwrap_or(1);
7274            } else if kind == "inline" {
7275                heading_name = node_text(source, &child).trim().to_string();
7276            }
7277            if !cursor.goto_next_sibling() {
7278                break;
7279            }
7280        }
7281    }
7282    if heading_name.is_empty() {
7283        None
7284    } else {
7285        Some((heading_name, heading_level))
7286    }
7287}
7288
7289fn parse_setext_heading(source: &str, node: &Node) -> Option<(String, u8)> {
7290    let mut heading_level = 1;
7291    let mut heading_name = String::new();
7292    let mut cursor = node.walk();
7293    if cursor.goto_first_child() {
7294        loop {
7295            let child = cursor.node();
7296            let kind = child.kind();
7297            if kind.starts_with("setext_h") && kind.ends_with("_underline") {
7298                heading_level = kind
7299                    .strip_prefix("setext_h")
7300                    .and_then(|s| s.strip_suffix("_underline"))
7301                    .and_then(|s| s.parse::<u8>().ok())
7302                    .unwrap_or(1);
7303            } else if kind == "paragraph" {
7304                heading_name = node_text(source, &child).trim().to_string();
7305            }
7306            if !cursor.goto_next_sibling() {
7307                break;
7308            }
7309        }
7310    }
7311    if heading_name.is_empty() {
7312        None
7313    } else {
7314        Some((heading_name, heading_level))
7315    }
7316}
7317
7318fn collect_headings(source: &str, node: &Node, headings: &mut Vec<RawHeading>) {
7319    let kind = node.kind();
7320    if kind == "atx_heading" {
7321        if let Some((name, level)) = parse_atx_heading(source, node) {
7322            headings.push(RawHeading {
7323                name,
7324                level,
7325                range: node_range(node),
7326            });
7327        }
7328    } else if kind == "setext_heading" {
7329        if let Some((name, level)) = parse_setext_heading(source, node) {
7330            headings.push(RawHeading {
7331                name,
7332                level,
7333                range: node_range(node),
7334            });
7335        }
7336    } else {
7337        let mut cursor = node.walk();
7338        if cursor.goto_first_child() {
7339            loop {
7340                collect_headings(source, &cursor.node(), headings);
7341                if !cursor.goto_next_sibling() {
7342                    break;
7343                }
7344            }
7345        }
7346    }
7347}
7348
7349/// Extract markdown headings as symbols.
7350/// Each heading becomes a symbol with kind `Heading`, and its range covers the entire
7351/// section (from the heading to the next heading at the same or higher level, or EOF).
7352fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
7353    let mut raw_headings = Vec::new();
7354    collect_headings(source, root, &mut raw_headings);
7355
7356    let mut symbols = Vec::new();
7357    let total_lines = source.lines().count() as u32;
7358
7359    let mut active_headings: Vec<Option<String>> = vec![None; 7];
7360
7361    for i in 0..raw_headings.len() {
7362        let h = &raw_headings[i];
7363        let level = h.level as usize;
7364
7365        // Build scope chain from active headings of lower levels
7366        let mut scope_chain = Vec::new();
7367        for l in 1..level {
7368            if let Some(ref name) = active_headings[l] {
7369                scope_chain.push(name.clone());
7370            }
7371        }
7372
7373        // Update active headings
7374        active_headings[level] = Some(h.name.clone());
7375        for l in (level + 1)..7 {
7376            active_headings[l] = None;
7377        }
7378
7379        // Determine the section range
7380        let start_line = h.range.start_line;
7381        let start_col = h.range.start_col;
7382
7383        // Find the next heading of the same or higher level (level <= h.level)
7384        let mut end_line = total_lines.saturating_sub(1);
7385        let mut end_col = if total_lines > 0 {
7386            source_line_end_col(source, end_line)
7387        } else {
7388            0
7389        };
7390
7391        for j in (i + 1)..raw_headings.len() {
7392            if raw_headings[j].level <= h.level {
7393                // The section ends before this heading starts
7394                let next_start_line = raw_headings[j].range.start_line;
7395                end_line = next_start_line.saturating_sub(1).max(start_line);
7396                end_col = source_line_end_col(source, end_line);
7397                break;
7398            }
7399        }
7400
7401        let signature = format!("{} {}", "#".repeat((h.level as usize).min(6)), h.name);
7402
7403        symbols.push(Symbol {
7404            name: h.name.clone(),
7405            kind: SymbolKind::Heading,
7406            range: Range {
7407                start_line,
7408                start_col,
7409                end_line,
7410                end_col,
7411            },
7412            signature: Some(signature),
7413            scope_chain: scope_chain.clone(),
7414            exported: false,
7415            parent: scope_chain.last().cloned(),
7416        });
7417    }
7418
7419    Ok(symbols)
7420}
7421
7422/// Remove duplicate symbols based on (name, kind, start_line).
7423/// Class declarations can match both "class" and "method" patterns,
7424/// producing duplicates.
7425fn dedup_symbols(symbols: &mut Vec<Symbol>) {
7426    let mut seen = std::collections::HashSet::new();
7427    symbols.retain(|s| {
7428        let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
7429        seen.insert(key)
7430    });
7431}
7432
7433/// Provider that uses tree-sitter for real symbol extraction.
7434/// Implements the `LanguageProvider` trait from `language.rs`.
7435pub struct TreeSitterProvider {
7436    symbol_cache: SharedSymbolCache,
7437}
7438
7439#[derive(Debug, Clone)]
7440struct ReExportTarget {
7441    file: PathBuf,
7442    symbol_name: String,
7443}
7444
7445impl TreeSitterProvider {
7446    /// Create a new `TreeSitterProvider` backed by a fresh shared symbol cache.
7447    pub fn new() -> Self {
7448        Self::with_symbol_cache(Arc::new(RwLock::new(SymbolCache::new())))
7449    }
7450
7451    /// Create a new `TreeSitterProvider` backed by a shared symbol cache.
7452    pub fn with_symbol_cache(symbol_cache: SharedSymbolCache) -> Self {
7453        Self { symbol_cache }
7454    }
7455
7456    /// Return shared symbol cache entries for status reporting.
7457    pub fn symbol_cache_len(&self) -> usize {
7458        self.symbol_cache
7459            .read()
7460            .map(|cache| cache.len())
7461            .unwrap_or(0)
7462    }
7463
7464    /// Shared symbol cache backing this provider.
7465    pub fn symbol_cache(&self) -> SharedSymbolCache {
7466        Arc::clone(&self.symbol_cache)
7467    }
7468
7469    fn resolve_symbol_inner(
7470        &self,
7471        parser: &mut FileParser,
7472        file: &Path,
7473        name: &str,
7474        depth: usize,
7475        visited: &mut HashSet<(PathBuf, String)>,
7476    ) -> Result<Vec<SymbolMatch>, AftError> {
7477        if depth > MAX_REEXPORT_DEPTH {
7478            return Ok(Vec::new());
7479        }
7480
7481        let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
7482        if !visited.insert((canonical_file, name.to_string())) {
7483            return Ok(Vec::new());
7484        }
7485
7486        let symbols = parser.extract_symbols(file)?;
7487        let local_matches = symbol_matches_in_file(file, &symbols, name);
7488        if !local_matches.is_empty() {
7489            return Ok(local_matches);
7490        }
7491
7492        if name == "default" {
7493            let default_matches = self.resolve_local_default_export(parser, file, &symbols)?;
7494            if !default_matches.is_empty() {
7495                return Ok(default_matches);
7496            }
7497        }
7498
7499        let reexport_targets = self.collect_reexport_targets(parser, file, name)?;
7500        let mut matches = Vec::new();
7501        let mut seen = HashSet::new();
7502        for target in reexport_targets {
7503            for resolved in self.resolve_symbol_inner(
7504                parser,
7505                &target.file,
7506                &target.symbol_name,
7507                depth + 1,
7508                visited,
7509            )? {
7510                let key = format!(
7511                    "{}:{}:{}:{}:{}:{}",
7512                    resolved.file,
7513                    resolved.symbol.name,
7514                    resolved.symbol.range.start_line,
7515                    resolved.symbol.range.start_col,
7516                    resolved.symbol.range.end_line,
7517                    resolved.symbol.range.end_col
7518                );
7519                if seen.insert(key) {
7520                    matches.push(resolved);
7521                }
7522            }
7523        }
7524
7525        Ok(matches)
7526    }
7527
7528    fn collect_reexport_targets(
7529        &self,
7530        parser: &mut FileParser,
7531        file: &Path,
7532        requested_name: &str,
7533    ) -> Result<Vec<ReExportTarget>, AftError> {
7534        let (source, tree, lang) = self.read_parsed_file(parser, file)?;
7535        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
7536            return Ok(Vec::new());
7537        }
7538
7539        let mut targets = Vec::new();
7540        let root = tree.root_node();
7541        let from_dir = file.parent().unwrap_or_else(|| Path::new("."));
7542
7543        let mut cursor = root.walk();
7544        if !cursor.goto_first_child() {
7545            return Ok(targets);
7546        }
7547
7548        loop {
7549            let node = cursor.node();
7550            if node.kind() == "export_statement" {
7551                let Some(source_node) = node.child_by_field_name("source") else {
7552                    if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
7553                        if let Some(symbol_name) =
7554                            resolve_export_clause_name(&source, &export_clause, requested_name)
7555                        {
7556                            targets.push(ReExportTarget {
7557                                file: file.to_path_buf(),
7558                                symbol_name,
7559                            });
7560                        }
7561                    }
7562                    if !cursor.goto_next_sibling() {
7563                        break;
7564                    }
7565                    continue;
7566                };
7567
7568                let Some(module_path) = string_content(&source, &source_node) else {
7569                    if !cursor.goto_next_sibling() {
7570                        break;
7571                    }
7572                    continue;
7573                };
7574
7575                let Some(target_file) = resolve_module_path(from_dir, &module_path) else {
7576                    if !cursor.goto_next_sibling() {
7577                        break;
7578                    }
7579                    continue;
7580                };
7581
7582                if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
7583                    if let Some(symbol_name) =
7584                        resolve_export_clause_name(&source, &export_clause, requested_name)
7585                    {
7586                        targets.push(ReExportTarget {
7587                            file: target_file,
7588                            symbol_name,
7589                        });
7590                    }
7591                } else if export_statement_has_wildcard(&source, &node) {
7592                    targets.push(ReExportTarget {
7593                        file: target_file,
7594                        symbol_name: requested_name.to_string(),
7595                    });
7596                }
7597            }
7598
7599            if !cursor.goto_next_sibling() {
7600                break;
7601            }
7602        }
7603
7604        Ok(targets)
7605    }
7606
7607    fn resolve_local_default_export(
7608        &self,
7609        parser: &mut FileParser,
7610        file: &Path,
7611        symbols: &[Symbol],
7612    ) -> Result<Vec<SymbolMatch>, AftError> {
7613        let (source, tree, lang) = self.read_parsed_file(parser, file)?;
7614        if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
7615            return Ok(Vec::new());
7616        }
7617
7618        let root = tree.root_node();
7619        let mut matches = Vec::new();
7620        let mut seen = HashSet::new();
7621
7622        let mut cursor = root.walk();
7623        if !cursor.goto_first_child() {
7624            return Ok(matches);
7625        }
7626
7627        loop {
7628            let node = cursor.node();
7629            if node.kind() == "export_statement"
7630                && node.child_by_field_name("source").is_none()
7631                && node_contains_token(&source, &node, "default")
7632            {
7633                if let Some(target_name) = default_export_target_name(&source, &node) {
7634                    for symbol_match in symbol_matches_in_file(file, symbols, &target_name) {
7635                        let key = format!(
7636                            "{}:{}:{}:{}:{}:{}",
7637                            symbol_match.file,
7638                            symbol_match.symbol.name,
7639                            symbol_match.symbol.range.start_line,
7640                            symbol_match.symbol.range.start_col,
7641                            symbol_match.symbol.range.end_line,
7642                            symbol_match.symbol.range.end_col
7643                        );
7644                        if seen.insert(key) {
7645                            matches.push(symbol_match);
7646                        }
7647                    }
7648                }
7649            }
7650
7651            if !cursor.goto_next_sibling() {
7652                break;
7653            }
7654        }
7655
7656        Ok(matches)
7657    }
7658
7659    fn read_parsed_file(
7660        &self,
7661        parser: &mut FileParser,
7662        file: &Path,
7663    ) -> Result<(String, Tree, LangId), AftError> {
7664        let current_mtime = std::fs::metadata(file)
7665            .and_then(|m| m.modified())
7666            .map_err(|e| AftError::FileNotFound {
7667                path: format!("{}: {}", file.display(), e),
7668            })?;
7669        let source = std::fs::read_to_string(file).map_err(|e| AftError::FileNotFound {
7670            path: format!("{}: {}", file.display(), e),
7671        })?;
7672        let size = source.len() as u64;
7673        let content_hash = content_hash_for_source(&source);
7674        let (tree, lang) =
7675            parser.parse_with_source(file, &source, current_mtime, size, content_hash)?;
7676        Ok((source, tree.clone(), lang))
7677    }
7678}
7679
7680fn symbol_matches_in_file(file: &Path, symbols: &[Symbol], name: &str) -> Vec<SymbolMatch> {
7681    symbols
7682        .iter()
7683        .filter(|symbol| symbol_query_matches(symbol, name))
7684        .cloned()
7685        .map(|symbol| SymbolMatch {
7686            file: file.display().to_string(),
7687            symbol,
7688        })
7689        .collect()
7690}
7691
7692fn symbol_query_matches(symbol: &Symbol, query: &str) -> bool {
7693    if symbol.name == query {
7694        return true;
7695    }
7696
7697    if !query.contains('.') && !query.contains("::") {
7698        return false;
7699    }
7700
7701    let mut parts = symbol
7702        .scope_chain
7703        .iter()
7704        .filter(|part| !part.is_empty())
7705        .map(String::as_str)
7706        .collect::<Vec<_>>();
7707    if parts.is_empty() {
7708        return false;
7709    }
7710    parts.push(symbol.name.as_str());
7711
7712    parts.join(".") == query || parts.join("::") == query
7713}
7714
7715fn string_content(source: &str, node: &Node) -> Option<String> {
7716    let text = node_text(source, node);
7717    if text.len() < 2 {
7718        return None;
7719    }
7720
7721    Some(
7722        text.trim_start_matches(|c| c == '\'' || c == '"')
7723            .trim_end_matches(|c| c == '\'' || c == '"')
7724            .to_string(),
7725    )
7726}
7727
7728fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
7729    let mut cursor = node.walk();
7730    if !cursor.goto_first_child() {
7731        return None;
7732    }
7733
7734    loop {
7735        let child = cursor.node();
7736        if child.kind() == kind {
7737            return Some(child);
7738        }
7739        if !cursor.goto_next_sibling() {
7740            break;
7741        }
7742    }
7743
7744    None
7745}
7746
7747fn resolve_export_clause_name(
7748    source: &str,
7749    export_clause: &Node,
7750    requested_name: &str,
7751) -> Option<String> {
7752    let mut cursor = export_clause.walk();
7753    if !cursor.goto_first_child() {
7754        return None;
7755    }
7756
7757    loop {
7758        let child = cursor.node();
7759        if child.kind() == "export_specifier" {
7760            let (source_name, exported_name) = export_specifier_names(source, &child)?;
7761            if exported_name == requested_name {
7762                return Some(source_name);
7763            }
7764        }
7765
7766        if !cursor.goto_next_sibling() {
7767            break;
7768        }
7769    }
7770
7771    None
7772}
7773
7774fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
7775    let source_name = specifier
7776        .child_by_field_name("name")
7777        .map(|node| node_text(source, &node).to_string());
7778    let alias_name = specifier
7779        .child_by_field_name("alias")
7780        .map(|node| node_text(source, &node).to_string());
7781
7782    if let Some(source_name) = source_name {
7783        let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
7784        return Some((source_name, exported_name));
7785    }
7786
7787    let mut names = Vec::new();
7788    let mut cursor = specifier.walk();
7789    if cursor.goto_first_child() {
7790        loop {
7791            let child = cursor.node();
7792            let child_text = node_text(source, &child).trim();
7793            if matches!(
7794                child.kind(),
7795                "identifier" | "type_identifier" | "property_identifier"
7796            ) || child_text == "default"
7797            {
7798                names.push(child_text.to_string());
7799            }
7800            if !cursor.goto_next_sibling() {
7801                break;
7802            }
7803        }
7804    }
7805
7806    match names.as_slice() {
7807        [name] => Some((name.clone(), name.clone())),
7808        [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
7809        _ => None,
7810    }
7811}
7812
7813fn export_statement_has_wildcard(source: &str, node: &Node) -> bool {
7814    let mut cursor = node.walk();
7815    if !cursor.goto_first_child() {
7816        return false;
7817    }
7818
7819    loop {
7820        if node_text(source, &cursor.node()).trim() == "*" {
7821            return true;
7822        }
7823        if !cursor.goto_next_sibling() {
7824            break;
7825        }
7826    }
7827
7828    false
7829}
7830
7831fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
7832    let mut cursor = node.walk();
7833    if !cursor.goto_first_child() {
7834        return false;
7835    }
7836
7837    loop {
7838        if node_text(source, &cursor.node()).trim() == token {
7839            return true;
7840        }
7841        if !cursor.goto_next_sibling() {
7842            break;
7843        }
7844    }
7845
7846    false
7847}
7848
7849fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
7850    if let Some(value_node) = export_stmt.child_by_field_name("value") {
7851        if let Some(name) = default_export_node_name(source, &value_node) {
7852            return Some(name);
7853        }
7854    }
7855
7856    if let Some(declaration_node) = export_stmt.child_by_field_name("declaration") {
7857        if let Some(name) = default_export_node_name(source, &declaration_node) {
7858            return Some(name);
7859        }
7860    }
7861
7862    let mut cursor = export_stmt.walk();
7863    if !cursor.goto_first_child() {
7864        return None;
7865    }
7866
7867    loop {
7868        let child = cursor.node();
7869        if let Some(name) = default_export_node_name(source, &child) {
7870            return Some(name);
7871        }
7872
7873        if !cursor.goto_next_sibling() {
7874            break;
7875        }
7876    }
7877
7878    None
7879}
7880
7881fn default_export_node_name(source: &str, node: &Node) -> Option<String> {
7882    match node.kind() {
7883        "function_declaration"
7884        | "generator_function_declaration"
7885        | "function_expression"
7886        | "generator_function"
7887        | "class_declaration"
7888        | "class" => node
7889            .child_by_field_name("name")
7890            .map(|name_node| node_text(source, &name_node).to_string())
7891            .or_else(|| Some("default".to_string())),
7892        "interface_declaration" | "enum_declaration" | "type_alias_declaration" => node
7893            .child_by_field_name("name")
7894            .map(|name_node| node_text(source, &name_node).to_string()),
7895        "lexical_declaration" => lexical_declaration_name(source, node),
7896        "identifier" | "type_identifier" => {
7897            let text = node_text(source, node);
7898            (text != "export" && text != "default").then(|| text.to_string())
7899        }
7900        _ => None,
7901    }
7902}
7903
7904fn lexical_declaration_name(source: &str, node: &Node) -> Option<String> {
7905    let mut cursor = node.walk();
7906    if !cursor.goto_first_child() {
7907        return None;
7908    }
7909
7910    loop {
7911        let child = cursor.node();
7912        if child.kind() == "variable_declarator" {
7913            if let Some(name_node) = child.child_by_field_name("name") {
7914                return Some(node_text(source, &name_node).to_string());
7915            }
7916        }
7917        if !cursor.goto_next_sibling() {
7918            break;
7919        }
7920    }
7921
7922    None
7923}
7924
7925impl crate::language::LanguageProvider for TreeSitterProvider {
7926    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
7927        let mut parser = FileParser::with_symbol_cache(self.symbol_cache());
7928        let matches = self.resolve_symbol_inner(&mut parser, file, name, 0, &mut HashSet::new())?;
7929
7930        if matches.is_empty() {
7931            Err(AftError::SymbolNotFound {
7932                name: name.to_string(),
7933                file: file.display().to_string(),
7934            })
7935        } else {
7936            Ok(matches)
7937        }
7938    }
7939
7940    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
7941        let mut parser = FileParser::with_symbol_cache(self.symbol_cache());
7942        parser.extract_symbols(file)
7943    }
7944
7945    fn heading_anchors(
7946        &self,
7947        file: &Path,
7948    ) -> Result<Vec<crate::language::HeadingAnchor>, AftError> {
7949        if detect_language(file) != Some(LangId::Html) {
7950            return Ok(Vec::new());
7951        }
7952        let mut parser = FileParser::with_symbol_cache(self.symbol_cache());
7953        let (source, tree, _) = self.read_parsed_file(&mut parser, file)?;
7954        Ok(extract_html_heading_anchors(&source, &tree.root_node()))
7955    }
7956
7957    fn as_any(&self) -> &dyn std::any::Any {
7958        self
7959    }
7960}
7961
7962#[cfg(test)]
7963mod tests {
7964    use super::*;
7965    use crate::language::LanguageProvider;
7966    use crate::symbol_cache_disk;
7967    use std::path::{Path, PathBuf};
7968
7969    fn fixture_path(name: &str) -> PathBuf {
7970        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7971            .join("tests")
7972            .join("fixtures")
7973            .join(name)
7974    }
7975
7976    #[test]
7977    #[ignore = "manual single-file parser/query phase benchmark"]
7978    fn profile_rust_parser_and_query_phases() {
7979        let source = "pub fn tiny() -> bool {\n    true\n}\n";
7980
7981        let grammar_started = std::time::Instant::now();
7982        let grammar = grammar_for(LangId::Rust);
7983        let grammar_elapsed = grammar_started.elapsed();
7984
7985        let parser_init_started = std::time::Instant::now();
7986        let mut parser = Parser::new();
7987        parser.set_language(&grammar).unwrap();
7988        let parser_init_elapsed = parser_init_started.elapsed();
7989
7990        let parse_started = std::time::Instant::now();
7991        let tree = parser.parse(source, None).unwrap();
7992        let parse_elapsed = parse_started.elapsed();
7993
7994        let query_compile_started = std::time::Instant::now();
7995        let query = Query::new(&grammar, RS_QUERY).unwrap();
7996        let query_compile_elapsed = query_compile_started.elapsed();
7997
7998        let query_execute_started = std::time::Instant::now();
7999        let mut cursor = QueryCursor::new();
8000        let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
8001        let mut query_matches = 0;
8002        while matches.next().is_some() {
8003            query_matches += 1;
8004        }
8005        let query_execute_elapsed = query_execute_started.elapsed();
8006
8007        let direct_extract_started = std::time::Instant::now();
8008        let symbols = extract_rs_symbols(source, &tree.root_node()).unwrap();
8009        let direct_extract_elapsed = direct_extract_started.elapsed();
8010
8011        eprintln!(
8012            "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={}",
8013            symbols.len()
8014        );
8015        assert!(query_matches > 0);
8016        assert_eq!(symbols.len(), 1);
8017    }
8018
8019    fn extract_rs_symbols_with_query_oracle(source: &str, root: Node<'_>) -> Vec<Symbol> {
8020        let grammar = grammar_for(LangId::Rust);
8021        let query = Query::new(&grammar, RS_QUERY).expect("compile Rust parity query");
8022        let capture_names = query.capture_names();
8023        let is_pub = |node: &Node<'_>| {
8024            let mut cursor = node.walk();
8025            if cursor.goto_first_child() {
8026                loop {
8027                    if cursor.node().kind() == "visibility_modifier" {
8028                        return true;
8029                    }
8030                    if !cursor.goto_next_sibling() {
8031                        break;
8032                    }
8033                }
8034            }
8035            false
8036        };
8037
8038        let mut symbols = Vec::new();
8039        let mut cursor = QueryCursor::new();
8040        let mut matches = cursor.matches(&query, root, source.as_bytes());
8041        while let Some(query_match) = {
8042            matches.advance();
8043            matches.get()
8044        } {
8045            let mut function_name = None;
8046            let mut function_definition = None;
8047            let mut item_name = None;
8048            let mut item_definition = None;
8049            let mut item_kind = None;
8050            let mut impl_node = None;
8051            for capture in query_match.captures {
8052                let Some(name) = capture_names.get(capture.index as usize) else {
8053                    continue;
8054                };
8055                match *name {
8056                    "fn.name" => function_name = Some(capture.node),
8057                    "fn.def" => function_definition = Some(capture.node),
8058                    "struct.name" => {
8059                        item_name = Some(capture.node);
8060                        item_kind = Some(SymbolKind::Struct);
8061                    }
8062                    "struct.def" => item_definition = Some(capture.node),
8063                    "enum.name" => {
8064                        item_name = Some(capture.node);
8065                        item_kind = Some(SymbolKind::Enum);
8066                    }
8067                    "enum.def" => item_definition = Some(capture.node),
8068                    "trait.name" => {
8069                        item_name = Some(capture.node);
8070                        item_kind = Some(SymbolKind::Interface);
8071                    }
8072                    "trait.def" => item_definition = Some(capture.node),
8073                    "impl.def" => impl_node = Some(capture.node),
8074                    _ => {}
8075                }
8076            }
8077
8078            if let (Some(name_node), Some(definition)) = (function_name, function_definition) {
8079                let owner = rust_function_declaration_list_owner(&definition);
8080                if owner
8081                    .as_ref()
8082                    .is_none_or(|owner| owner.kind() == "mod_item")
8083                {
8084                    let scope_chain = rust_mod_scope_chain(&definition, source);
8085                    symbols.push(Symbol {
8086                        name: node_text(source, &name_node).to_string(),
8087                        kind: SymbolKind::Function,
8088                        range: node_range_with_decorators(&definition, source, LangId::Rust),
8089                        signature: Some(extract_signature(source, &definition)),
8090                        scope_chain: scope_chain.clone(),
8091                        exported: is_pub(&definition),
8092                        parent: scope_chain.last().cloned(),
8093                    });
8094                }
8095            }
8096
8097            if let (Some(name_node), Some(definition), Some(kind)) =
8098                (item_name, item_definition, item_kind)
8099            {
8100                symbols.push(Symbol {
8101                    name: node_text(source, &name_node).to_string(),
8102                    kind,
8103                    range: node_range_with_decorators(&definition, source, LangId::Rust),
8104                    signature: Some(extract_signature(source, &definition)),
8105                    scope_chain: Vec::new(),
8106                    exported: is_pub(&definition),
8107                    parent: None,
8108                });
8109            }
8110
8111            if let Some(impl_node) = impl_node {
8112                let scope_name = rust_impl_scope_name(&impl_node, source);
8113                let parent_name = scope_name
8114                    .rsplit(" for ")
8115                    .next()
8116                    .unwrap_or_default()
8117                    .to_string();
8118                let mut impl_cursor = impl_node.walk();
8119                if impl_cursor.goto_first_child() {
8120                    loop {
8121                        let child = impl_cursor.node();
8122                        if child.kind() == "declaration_list" {
8123                            let mut method_cursor = child.walk();
8124                            if method_cursor.goto_first_child() {
8125                                loop {
8126                                    let method = method_cursor.node();
8127                                    if method.kind() == "function_item" {
8128                                        if let Some(name_node) = method.child_by_field_name("name")
8129                                        {
8130                                            symbols.push(Symbol {
8131                                                name: node_text(source, &name_node).to_string(),
8132                                                kind: SymbolKind::Method,
8133                                                range: node_range_with_decorators(
8134                                                    &method,
8135                                                    source,
8136                                                    LangId::Rust,
8137                                                ),
8138                                                signature: Some(extract_signature(source, &method)),
8139                                                scope_chain: (!scope_name.is_empty())
8140                                                    .then(|| vec![scope_name.clone()])
8141                                                    .unwrap_or_default(),
8142                                                exported: is_pub(&method),
8143                                                parent: (!parent_name.is_empty())
8144                                                    .then(|| parent_name.clone()),
8145                                            });
8146                                        }
8147                                    }
8148                                    if !method_cursor.goto_next_sibling() {
8149                                        break;
8150                                    }
8151                                }
8152                            }
8153                        }
8154                        if !impl_cursor.goto_next_sibling() {
8155                            break;
8156                        }
8157                    }
8158                }
8159            }
8160        }
8161
8162        dedup_symbols(&mut symbols);
8163        symbols
8164    }
8165
8166    fn rust_symbol_fingerprint(symbols: &[Symbol]) -> Vec<String> {
8167        symbols
8168            .iter()
8169            .map(|symbol| {
8170                format!(
8171                    "{}|{:?}|{:?}|{:?}|{:?}|{}|{:?}",
8172                    symbol.name,
8173                    symbol.kind,
8174                    symbol.range,
8175                    symbol.signature,
8176                    symbol.scope_chain,
8177                    symbol.exported,
8178                    symbol.parent
8179                )
8180            })
8181            .collect()
8182    }
8183
8184    #[test]
8185    fn rust_walk_matches_query_oracle_for_adversarial_shapes() {
8186        let source = r#"
8187#[cfg(any())]
8188pub(crate) mod outer {
8189    pub mod inner {
8190        #[doc = "keeps decorator range"]
8191        pub(crate) fn nested<T>() {}
8192
8193        pub(crate) struct Generic<T>(T);
8194        pub enum State { Ready }
8195        pub trait Service<T> {
8196            fn declaration(&self);
8197        }
8198
8199        impl<T> Service<T> for Generic<T> {
8200            #[inline]
8201            pub(crate) fn attributed_method(&self) {}
8202            fn private_method(&self) {}
8203        }
8204    }
8205}
8206"#;
8207        let grammar = grammar_for(LangId::Rust);
8208        let mut parser = Parser::new();
8209        parser.set_language(&grammar).unwrap();
8210        let tree = parser.parse(source, None).unwrap();
8211
8212        let query_symbols = extract_rs_symbols_with_query_oracle(source, tree.root_node());
8213        let walk_symbols = extract_rs_symbols(source, &tree.root_node()).unwrap();
8214        assert_eq!(
8215            rust_symbol_fingerprint(&walk_symbols),
8216            rust_symbol_fingerprint(&query_symbols)
8217        );
8218        assert!(walk_symbols.iter().any(|symbol| {
8219            symbol.name == "nested" && symbol.exported && symbol.scope_chain == ["outer", "inner"]
8220        }));
8221        assert!(walk_symbols.iter().any(|symbol| {
8222            symbol.name == "attributed_method"
8223                && symbol.kind == SymbolKind::Method
8224                && symbol.scope_chain == ["Service<T> for Generic<T>"]
8225                && symbol.exported
8226        }));
8227        assert!(
8228            walk_symbols
8229                .iter()
8230                .all(|symbol| symbol.name != "declaration"),
8231            "trait declarations are not implementation methods"
8232        );
8233    }
8234
8235    #[test]
8236    fn source_parser_is_reused_on_the_current_worker_thread() {
8237        let first = "pub fn first() {}\n";
8238        let second = "pub struct Second;\n";
8239        let path = Path::new("worker.rs");
8240
8241        let before = REUSABLE_PARSERS.with(|parsers| parsers.borrow().len());
8242        let first_tree =
8243            parse_source_with_cached_parser(path, first, LangId::Rust).expect("parse first source");
8244        let after_first = REUSABLE_PARSERS.with(|parsers| parsers.borrow().len());
8245        let second_tree = parse_source_with_cached_parser(path, second, LangId::Rust)
8246            .expect("parse second source");
8247        let after_second = REUSABLE_PARSERS.with(|parsers| parsers.borrow().len());
8248
8249        assert!(!first_tree.root_node().has_error());
8250        assert!(!second_tree.root_node().has_error());
8251        assert!(after_first == before || after_first == before + 1);
8252        assert_eq!(after_second, after_first);
8253    }
8254
8255    fn test_symbol(name: &str) -> Symbol {
8256        Symbol {
8257            name: name.to_string(),
8258            kind: SymbolKind::Function,
8259            range: Range {
8260                start_line: 0,
8261                start_col: 0,
8262                end_line: 0,
8263                end_col: 10,
8264            },
8265            signature: Some(format!("fn {name}()")),
8266            scope_chain: Vec::new(),
8267            exported: true,
8268            parent: None,
8269        }
8270    }
8271
8272    #[test]
8273    fn symbol_memory_estimate_is_zero_when_empty_and_nonzero_when_populated() {
8274        let mut cache = SymbolCache::new();
8275        assert_eq!(cache.estimated_memory().estimated_bytes, Some(0));
8276        cache.insert(
8277            PathBuf::from("src/lib.rs"),
8278            SystemTime::UNIX_EPOCH,
8279            10,
8280            cache_freshness::zero_hash(),
8281            vec![test_symbol("memory_estimate")],
8282        );
8283        let estimate = cache.estimated_memory();
8284        assert!(estimate.estimated_bytes.unwrap() > 0);
8285        assert_eq!(estimate.counts["entries"], 1);
8286        assert_eq!(estimate.counts["symbols"], 1);
8287    }
8288
8289    #[test]
8290    fn inc_and_scss_extensions_are_detected() {
8291        assert_eq!(
8292            detect_language(Path::new("template.inc")),
8293            Some(LangId::Php)
8294        );
8295        assert_eq!(
8296            detect_language(Path::new("styles.scss")),
8297            Some(LangId::Scss)
8298        );
8299        assert_eq!(detect_language(Path::new("main.pas")), Some(LangId::Pascal));
8300        assert_eq!(detect_language(Path::new("main.pp")), Some(LangId::Pascal));
8301        assert_eq!(detect_language(Path::new("main.dpr")), Some(LangId::Pascal));
8302        assert_eq!(detect_language(Path::new("main.dpk")), Some(LangId::Pascal));
8303        assert_eq!(detect_language(Path::new("main.lpr")), Some(LangId::Pascal));
8304        assert_eq!(detect_language(Path::new("template.tpl")), None);
8305    }
8306
8307    #[test]
8308    fn inc_files_parse_with_php_grammar() {
8309        let tmp = tempfile::tempdir().expect("create temp dir");
8310        let file = tmp.path().join("partial.inc");
8311        std::fs::write(&file, "<?php\nfunction render_partial() { return 1; }\n")
8312            .expect("write inc file");
8313
8314        let mut parser = FileParser::new();
8315        let symbols = parser
8316            .extract_symbols(&file)
8317            .expect("extract php inc symbols");
8318        let function = symbols
8319            .iter()
8320            .find(|symbol| symbol.name == "render_partial")
8321            .expect("find PHP function in .inc");
8322        assert_eq!(function.kind, SymbolKind::Function);
8323    }
8324
8325    #[test]
8326    fn scss_symbols_include_mixin_variable_function_and_rule() {
8327        let tmp = tempfile::tempdir().expect("create temp dir");
8328        let file = tmp.path().join("styles.scss");
8329        std::fs::write(
8330            &file,
8331            r#"$brand-color: #336699;
8332
8333@mixin button-base($padding) {
8334  padding: $padding;
8335}
8336
8337@function double($value) {
8338  @return $value * 2;
8339}
8340
8341.card, .panel {
8342  color: $brand-color;
8343}
8344"#,
8345        )
8346        .expect("write scss file");
8347
8348        let mut parser = FileParser::new();
8349        let symbols = parser.extract_symbols(&file).expect("extract scss symbols");
8350        let get = |name: &str| {
8351            symbols
8352                .iter()
8353                .find(|symbol| symbol.name == name)
8354                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
8355        };
8356
8357        assert_eq!(get("button-base").kind, SymbolKind::Function);
8358        assert_eq!(get("double").kind, SymbolKind::Function);
8359        assert_eq!(get("$brand-color").kind, SymbolKind::Variable);
8360        assert_eq!(get(".card, .panel").kind, SymbolKind::Class);
8361    }
8362
8363    #[test]
8364    fn symbol_cache_load_from_disk_round_trips_synthetic_entry() {
8365        let project = tempfile::tempdir().expect("create project dir");
8366        let storage = tempfile::tempdir().expect("create storage dir");
8367        let source = project.path().join("src/lib.rs");
8368        std::fs::create_dir_all(source.parent().expect("source parent"))
8369            .expect("create source dir");
8370        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
8371        let mtime = std::fs::metadata(&source)
8372            .expect("stat source")
8373            .modified()
8374            .expect("source mtime");
8375        let content = std::fs::read(&source).expect("read source");
8376        let size = content.len() as u64;
8377        let hash = crate::cache_freshness::hash_bytes(&content);
8378
8379        let mut cache = SymbolCache::new();
8380        cache.set_project_root(project.path().to_path_buf());
8381        cache.insert(
8382            source.clone(),
8383            mtime,
8384            size,
8385            hash,
8386            vec![test_symbol("cached")],
8387        );
8388        symbol_cache_disk::write_to_disk(&cache, storage.path(), "unit-project")
8389            .expect("write symbol cache");
8390
8391        let mut restored = SymbolCache::new();
8392        let outcome =
8393            restored.load_from_disk_with_outcome(storage.path(), "unit-project", project.path());
8394        let symbols = restored.get(&source, mtime).expect("restored symbols");
8395
8396        assert_eq!(outcome.loaded, 1);
8397        assert!(!outcome.needs_persistence);
8398        assert_eq!(symbols.len(), 1);
8399        assert_eq!(symbols[0].name, "cached");
8400    }
8401
8402    #[test]
8403    fn symbol_cache_load_from_disk_marks_mtime_refresh_for_persistence() {
8404        let project = tempfile::tempdir().expect("create project dir");
8405        let storage = tempfile::tempdir().expect("create storage dir");
8406        let source = project.path().join("src/lib.rs");
8407        std::fs::create_dir_all(source.parent().expect("source parent"))
8408            .expect("create source dir");
8409        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
8410        let metadata = std::fs::metadata(&source).expect("stat source");
8411        let mtime = metadata.modified().expect("source mtime");
8412        let content = std::fs::read(&source).expect("read source");
8413
8414        let mut cache = SymbolCache::new();
8415        cache.set_project_root(project.path().to_path_buf());
8416        cache.insert(
8417            source.clone(),
8418            mtime,
8419            content.len() as u64,
8420            crate::cache_freshness::hash_bytes(&content),
8421            vec![test_symbol("cached")],
8422        );
8423        symbol_cache_disk::write_to_disk(&cache, storage.path(), "mtime-unit-project")
8424            .expect("write symbol cache");
8425
8426        let advanced_mtime = mtime
8427            .checked_add(std::time::Duration::from_secs(2))
8428            .expect("advance source mtime");
8429        filetime::set_file_mtime(
8430            &source,
8431            filetime::FileTime::from_system_time(advanced_mtime),
8432        )
8433        .expect("touch source");
8434
8435        let mut restored = SymbolCache::new();
8436        let outcome = restored.load_from_disk_with_outcome(
8437            storage.path(),
8438            "mtime-unit-project",
8439            project.path(),
8440        );
8441
8442        assert_eq!(outcome.loaded, 1);
8443        assert!(outcome.needs_persistence);
8444        assert!(restored.get(&source, advanced_mtime).is_some());
8445    }
8446
8447    #[test]
8448    fn symbol_cache_load_from_disk_drops_stale_synthetic_entry() {
8449        let project = tempfile::tempdir().expect("create project dir");
8450        let storage = tempfile::tempdir().expect("create storage dir");
8451        let source = project.path().join("src/lib.rs");
8452        std::fs::create_dir_all(source.parent().expect("source parent"))
8453            .expect("create source dir");
8454        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
8455        let mtime = std::fs::metadata(&source)
8456            .expect("stat source")
8457            .modified()
8458            .expect("source mtime");
8459        let content = std::fs::read(&source).expect("read source");
8460        let size = content.len() as u64;
8461        let hash = crate::cache_freshness::hash_bytes(&content);
8462
8463        let mut cache = SymbolCache::new();
8464        cache.set_project_root(project.path().to_path_buf());
8465        cache.insert(
8466            source.clone(),
8467            mtime,
8468            size,
8469            hash,
8470            vec![test_symbol("cached")],
8471        );
8472        symbol_cache_disk::write_to_disk(&cache, storage.path(), "stale-unit-project")
8473            .expect("write symbol cache");
8474
8475        std::fs::write(&source, "pub fn cached() {}\npub fn fresh() {}\n").expect("change source");
8476
8477        let mut restored = SymbolCache::new();
8478        let outcome = restored.load_from_disk_with_outcome(
8479            storage.path(),
8480            "stale-unit-project",
8481            project.path(),
8482        );
8483
8484        assert_eq!(outcome.loaded, 0);
8485        assert!(outcome.needs_persistence);
8486        assert_eq!(restored.len(), 0);
8487    }
8488
8489    #[test]
8490    fn stale_prewarm_generation_cannot_repopulate_symbol_cache_after_reset() {
8491        let project = tempfile::tempdir().expect("create project dir");
8492        let storage = tempfile::tempdir().expect("create storage dir");
8493        let source = project.path().join("src/lib.rs");
8494        std::fs::create_dir_all(source.parent().expect("source parent"))
8495            .expect("create source dir");
8496        std::fs::write(&source, "pub fn cached() {}\n").expect("write source");
8497        let mtime = std::fs::metadata(&source)
8498            .expect("stat source")
8499            .modified()
8500            .expect("source mtime");
8501        let content = std::fs::read(&source).expect("read source");
8502        let size = content.len() as u64;
8503        let hash = crate::cache_freshness::hash_bytes(&content);
8504
8505        let mut disk_cache = SymbolCache::new();
8506        disk_cache.set_project_root(project.path().to_path_buf());
8507        disk_cache.insert(
8508            source.clone(),
8509            mtime,
8510            size,
8511            hash,
8512            vec![test_symbol("cached")],
8513        );
8514        symbol_cache_disk::write_to_disk(&disk_cache, storage.path(), "prewarm-reset")
8515            .expect("write symbol cache");
8516
8517        let shared = Arc::new(RwLock::new(SymbolCache::new()));
8518        let stale_generation = shared.write().unwrap().reset();
8519        let active_generation = shared.write().unwrap().reset();
8520        assert_ne!(stale_generation, active_generation);
8521
8522        {
8523            let mut cache = shared.write().unwrap();
8524            assert!(!cache
8525                .set_project_root_for_generation(stale_generation, project.path().to_path_buf()));
8526            assert_eq!(
8527                cache.load_from_disk_for_generation(
8528                    stale_generation,
8529                    storage.path(),
8530                    "prewarm-reset",
8531                    project.path()
8532                ),
8533                0
8534            );
8535        }
8536
8537        let mut stale_parser =
8538            FileParser::with_symbol_cache_generation(Arc::clone(&shared), Some(stale_generation));
8539        stale_parser
8540            .extract_symbols(&source)
8541            .expect("stale prewarm parses source but must not write cache");
8542
8543        let cache = shared.read().unwrap();
8544        assert_eq!(cache.generation(), active_generation);
8545        assert_eq!(cache.len(), 0);
8546        assert!(cache.project_root().is_none());
8547        assert!(!cache.contains_key(&source));
8548    }
8549
8550    // --- Language detection ---
8551
8552    #[test]
8553    fn detect_ts() {
8554        assert_eq!(
8555            detect_language(Path::new("foo.ts")),
8556            Some(LangId::TypeScript)
8557        );
8558    }
8559
8560    #[test]
8561    fn detect_tsx() {
8562        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
8563    }
8564
8565    #[test]
8566    fn detect_js() {
8567        assert_eq!(
8568            detect_language(Path::new("foo.js")),
8569            Some(LangId::JavaScript)
8570        );
8571    }
8572
8573    #[test]
8574    fn detect_jsx() {
8575        assert_eq!(
8576            detect_language(Path::new("foo.jsx")),
8577            Some(LangId::JavaScript)
8578        );
8579    }
8580
8581    #[test]
8582    fn detect_py() {
8583        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
8584    }
8585
8586    #[test]
8587    fn detect_rs() {
8588        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
8589    }
8590
8591    #[test]
8592    fn detect_go() {
8593        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
8594    }
8595
8596    #[test]
8597    fn detect_c() {
8598        assert_eq!(detect_language(Path::new("foo.c")), Some(LangId::C));
8599    }
8600
8601    #[test]
8602    fn detect_h() {
8603        assert_eq!(detect_language(Path::new("foo.h")), Some(LangId::C));
8604    }
8605
8606    #[test]
8607    fn detect_cc() {
8608        assert_eq!(detect_language(Path::new("foo.cc")), Some(LangId::Cpp));
8609    }
8610
8611    #[test]
8612    fn detect_cpp() {
8613        assert_eq!(detect_language(Path::new("foo.cpp")), Some(LangId::Cpp));
8614    }
8615
8616    #[test]
8617    fn detect_cxx() {
8618        assert_eq!(detect_language(Path::new("foo.cxx")), Some(LangId::Cpp));
8619    }
8620
8621    #[test]
8622    fn detect_hpp() {
8623        assert_eq!(detect_language(Path::new("foo.hpp")), Some(LangId::Cpp));
8624    }
8625
8626    #[test]
8627    fn detect_hh() {
8628        assert_eq!(detect_language(Path::new("foo.hh")), Some(LangId::Cpp));
8629    }
8630
8631    #[test]
8632    fn detect_zig() {
8633        assert_eq!(detect_language(Path::new("foo.zig")), Some(LangId::Zig));
8634    }
8635
8636    #[test]
8637    fn detect_cs() {
8638        assert_eq!(detect_language(Path::new("foo.cs")), Some(LangId::CSharp));
8639    }
8640
8641    #[test]
8642    fn detect_unknown_returns_none() {
8643        assert_eq!(detect_language(Path::new("foo.txt")), None);
8644    }
8645
8646    #[test]
8647    fn groovy_language_constant_loads_under_workspace_tree_sitter() {
8648        let mut parser = Parser::new();
8649        let language: Language = dekobon_tree_sitter_groovy::LANGUAGE.into();
8650        parser
8651            .set_language(&language)
8652            .expect("load Groovy grammar under workspace tree-sitter");
8653        let tree = parser
8654            .parse("class Greeter { def greet() { 'hi' } }", None)
8655            .expect("parse Groovy sample");
8656        assert!(
8657            !tree.root_node().has_error(),
8658            "Groovy sample should parse cleanly"
8659        );
8660    }
8661
8662    #[test]
8663    fn detect_groovy_extensions_and_jenkinsfile() {
8664        assert_eq!(
8665            detect_language(Path::new("script.groovy")),
8666            Some(LangId::Groovy)
8667        );
8668        assert_eq!(
8669            detect_language(Path::new("script.gvy")),
8670            Some(LangId::Groovy)
8671        );
8672        assert_eq!(
8673            detect_language(Path::new("script.gy")),
8674            Some(LangId::Groovy)
8675        );
8676        assert_eq!(
8677            detect_language(Path::new("shell.gsh")),
8678            Some(LangId::Groovy)
8679        );
8680        assert_eq!(
8681            detect_language(Path::new("build.gradle")),
8682            Some(LangId::Groovy)
8683        );
8684        assert_eq!(
8685            detect_language(Path::new("build.gradle.kts")),
8686            Some(LangId::Kotlin)
8687        );
8688        assert_eq!(
8689            detect_language(Path::new("Jenkinsfile")),
8690            Some(LangId::Groovy)
8691        );
8692    }
8693
8694    // --- Unsupported extension error ---
8695
8696    #[test]
8697    fn unsupported_extension_returns_invalid_request() {
8698        // Use a file that exists but has an unsupported extension
8699        let path = fixture_path("sample.ts");
8700        let bad_path = path.with_extension("txt");
8701        // Create a dummy file so the error comes from language detection, not I/O
8702        std::fs::write(&bad_path, "hello").unwrap();
8703        let provider = TreeSitterProvider::new();
8704        let result = provider.list_symbols(&bad_path);
8705        std::fs::remove_file(&bad_path).ok();
8706        match result {
8707            Err(AftError::InvalidRequest { message }) => {
8708                assert!(
8709                    message.contains("unsupported file extension"),
8710                    "msg: {}",
8711                    message
8712                );
8713                assert!(message.contains("txt"), "msg: {}", message);
8714            }
8715            other => panic!("expected InvalidRequest, got {:?}", other),
8716        }
8717    }
8718
8719    // --- TypeScript extraction ---
8720
8721    #[test]
8722    fn ts_extracts_all_symbol_kinds() {
8723        let provider = TreeSitterProvider::new();
8724        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8725
8726        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
8727        assert!(
8728            names.contains(&"greet"),
8729            "missing function greet: {:?}",
8730            names
8731        );
8732        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
8733        assert!(
8734            names.contains(&"UserService"),
8735            "missing class UserService: {:?}",
8736            names
8737        );
8738        assert!(
8739            names.contains(&"Config"),
8740            "missing interface Config: {:?}",
8741            names
8742        );
8743        assert!(
8744            names.contains(&"Status"),
8745            "missing enum Status: {:?}",
8746            names
8747        );
8748        assert!(
8749            names.contains(&"UserId"),
8750            "missing type alias UserId: {:?}",
8751            names
8752        );
8753        assert!(
8754            names.contains(&"internalHelper"),
8755            "missing non-exported fn: {:?}",
8756            names
8757        );
8758
8759        // At least 6 unique symbols as required
8760        assert!(
8761            symbols.len() >= 6,
8762            "expected ≥6 symbols, got {}: {:?}",
8763            symbols.len(),
8764            names
8765        );
8766    }
8767
8768    #[test]
8769    fn ts_symbol_kinds_correct() {
8770        let provider = TreeSitterProvider::new();
8771        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8772
8773        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
8774
8775        assert_eq!(find("greet").kind, SymbolKind::Function);
8776        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
8777        assert_eq!(find("UserService").kind, SymbolKind::Class);
8778        assert_eq!(find("Config").kind, SymbolKind::Interface);
8779        assert_eq!(find("Status").kind, SymbolKind::Enum);
8780        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
8781    }
8782
8783    #[test]
8784    fn ts_export_detection() {
8785        let provider = TreeSitterProvider::new();
8786        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8787
8788        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
8789
8790        assert!(find("greet").exported, "greet should be exported");
8791        assert!(find("add").exported, "add should be exported");
8792        assert!(
8793            find("UserService").exported,
8794            "UserService should be exported"
8795        );
8796        assert!(find("Config").exported, "Config should be exported");
8797        assert!(find("Status").exported, "Status should be exported");
8798        assert!(
8799            !find("internalHelper").exported,
8800            "internalHelper should not be exported"
8801        );
8802    }
8803
8804    #[test]
8805    fn ts_method_scope_chain() {
8806        let provider = TreeSitterProvider::new();
8807        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8808
8809        let methods: Vec<&Symbol> = symbols
8810            .iter()
8811            .filter(|s| s.kind == SymbolKind::Method)
8812            .collect();
8813        assert!(!methods.is_empty(), "should have at least one method");
8814
8815        for method in &methods {
8816            assert_eq!(
8817                method.scope_chain,
8818                vec!["UserService"],
8819                "method {} should have UserService in scope chain",
8820                method.name
8821            );
8822            assert_eq!(method.parent.as_deref(), Some("UserService"));
8823        }
8824    }
8825
8826    #[test]
8827    fn ts_signatures_present() {
8828        let provider = TreeSitterProvider::new();
8829        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8830
8831        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
8832
8833        let greet_sig = find("greet").signature.as_ref().unwrap();
8834        assert!(
8835            greet_sig.contains("greet"),
8836            "signature should contain function name: {}",
8837            greet_sig
8838        );
8839    }
8840
8841    #[test]
8842    fn ts_ranges_valid() {
8843        let provider = TreeSitterProvider::new();
8844        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
8845
8846        for s in &symbols {
8847            assert!(
8848                s.range.end_line >= s.range.start_line,
8849                "symbol {} has invalid range: {:?}",
8850                s.name,
8851                s.range
8852            );
8853        }
8854    }
8855
8856    // --- JavaScript extraction ---
8857
8858    #[test]
8859    fn js_extracts_core_symbols() {
8860        let provider = TreeSitterProvider::new();
8861        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
8862
8863        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
8864        assert!(
8865            names.contains(&"multiply"),
8866            "missing function multiply: {:?}",
8867            names
8868        );
8869        assert!(
8870            names.contains(&"divide"),
8871            "missing arrow fn divide: {:?}",
8872            names
8873        );
8874        assert!(
8875            names.contains(&"EventEmitter"),
8876            "missing class EventEmitter: {:?}",
8877            names
8878        );
8879        assert!(
8880            names.contains(&"main"),
8881            "missing default export fn main: {:?}",
8882            names
8883        );
8884
8885        assert!(
8886            symbols.len() >= 4,
8887            "expected ≥4 symbols, got {}: {:?}",
8888            symbols.len(),
8889            names
8890        );
8891    }
8892
8893    #[test]
8894    fn js_arrow_fn_correctly_named() {
8895        let provider = TreeSitterProvider::new();
8896        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
8897
8898        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
8899        assert_eq!(divide.kind, SymbolKind::Function);
8900        assert!(divide.exported, "divide should be exported");
8901
8902        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
8903        assert_eq!(internal.kind, SymbolKind::Function);
8904        assert!(!internal.exported, "internalUtil should not be exported");
8905    }
8906
8907    #[test]
8908    fn js_method_scope_chain() {
8909        let provider = TreeSitterProvider::new();
8910        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
8911
8912        let methods: Vec<&Symbol> = symbols
8913            .iter()
8914            .filter(|s| s.kind == SymbolKind::Method)
8915            .collect();
8916
8917        for method in &methods {
8918            assert_eq!(
8919                method.scope_chain,
8920                vec!["EventEmitter"],
8921                "method {} should have EventEmitter in scope chain",
8922                method.name
8923            );
8924        }
8925    }
8926
8927    // --- TSX extraction ---
8928
8929    #[test]
8930    fn tsx_extracts_react_component() {
8931        let provider = TreeSitterProvider::new();
8932        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
8933
8934        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
8935        assert!(
8936            names.contains(&"Button"),
8937            "missing React component Button: {:?}",
8938            names
8939        );
8940        assert!(
8941            names.contains(&"Counter"),
8942            "missing class Counter: {:?}",
8943            names
8944        );
8945        assert!(
8946            names.contains(&"formatLabel"),
8947            "missing function formatLabel: {:?}",
8948            names
8949        );
8950
8951        assert!(
8952            symbols.len() >= 2,
8953            "expected ≥2 symbols, got {}: {:?}",
8954            symbols.len(),
8955            names
8956        );
8957    }
8958
8959    #[test]
8960    fn tsx_jsx_doesnt_break_parser() {
8961        // Main assertion: TSX grammar handles JSX without errors
8962        let provider = TreeSitterProvider::new();
8963        let result = provider.list_symbols(&fixture_path("sample.tsx"));
8964        assert!(
8965            result.is_ok(),
8966            "TSX parsing should succeed: {:?}",
8967            result.err()
8968        );
8969    }
8970
8971    // --- resolve_symbol ---
8972
8973    #[test]
8974    fn resolve_symbol_finds_match() {
8975        let provider = TreeSitterProvider::new();
8976        let matches = provider
8977            .resolve_symbol(&fixture_path("sample.ts"), "greet")
8978            .unwrap();
8979        assert_eq!(matches.len(), 1);
8980        assert_eq!(matches[0].symbol.name, "greet");
8981        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
8982    }
8983
8984    #[test]
8985    fn resolve_symbol_not_found() {
8986        let provider = TreeSitterProvider::new();
8987        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
8988        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
8989    }
8990
8991    #[test]
8992    fn resolve_symbol_follows_reexport_chains() {
8993        let dir = tempfile::tempdir().unwrap();
8994        let config = dir.path().join("config.ts");
8995        let barrel1 = dir.path().join("barrel1.ts");
8996        let barrel2 = dir.path().join("barrel2.ts");
8997        let barrel3 = dir.path().join("barrel3.ts");
8998        let index = dir.path().join("index.ts");
8999
9000        std::fs::write(
9001            &config,
9002            "export class Config {}\nexport default class DefaultConfig {}\n",
9003        )
9004        .unwrap();
9005        std::fs::write(
9006            &barrel1,
9007            "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
9008        )
9009        .unwrap();
9010        std::fs::write(
9011            &barrel2,
9012            "export { Config as RenamedConfig } from './barrel1';\n",
9013        )
9014        .unwrap();
9015        std::fs::write(
9016            &barrel3,
9017            "export * from './barrel2';\nexport * from './barrel1';\n",
9018        )
9019        .unwrap();
9020        std::fs::write(
9021            &index,
9022            "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
9023        )
9024        .unwrap();
9025
9026        let provider = TreeSitterProvider::new();
9027        let config_canon = std::fs::canonicalize(&config).unwrap();
9028
9029        let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
9030        assert_eq!(direct.len(), 1);
9031        assert_eq!(direct[0].symbol.name, "Config");
9032        assert_eq!(direct[0].file, config_canon.display().to_string());
9033
9034        let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
9035        assert_eq!(renamed.len(), 1);
9036        assert_eq!(renamed[0].symbol.name, "Config");
9037        assert_eq!(renamed[0].file, config_canon.display().to_string());
9038
9039        let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
9040        assert_eq!(wildcard_chain.len(), 1);
9041        assert_eq!(wildcard_chain[0].symbol.name, "Config");
9042        assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
9043
9044        let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
9045        assert_eq!(wildcard_default.len(), 1);
9046        assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
9047        assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
9048
9049        let local = provider.resolve_symbol(&index, "Config").unwrap();
9050        assert_eq!(local.len(), 1);
9051        assert_eq!(local[0].symbol.name, "Config");
9052        assert_eq!(local[0].file, index.display().to_string());
9053    }
9054
9055    // --- Parse tree caching ---
9056
9057    #[test]
9058    fn symbol_range_includes_rust_attributes() {
9059        let dir = tempfile::tempdir().unwrap();
9060        let path = dir.path().join("test_attrs.rs");
9061        std::fs::write(
9062            &path,
9063            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
9064        )
9065        .unwrap();
9066
9067        let provider = TreeSitterProvider::new();
9068        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
9069        assert_eq!(matches.len(), 1);
9070        assert_eq!(
9071            matches[0].symbol.range.start_line, 0,
9072            "symbol range should include preceding /// doc comment, got start_line={}",
9073            matches[0].symbol.range.start_line
9074        );
9075    }
9076
9077    #[test]
9078    fn symbol_range_includes_go_doc_comment() {
9079        let dir = tempfile::tempdir().unwrap();
9080        let path = dir.path().join("test_doc.go");
9081        std::fs::write(
9082            &path,
9083            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
9084        )
9085        .unwrap();
9086
9087        let provider = TreeSitterProvider::new();
9088        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
9089        assert_eq!(matches.len(), 1);
9090        assert_eq!(
9091            matches[0].symbol.range.start_line, 2,
9092            "symbol range should include preceding doc comments, got start_line={}",
9093            matches[0].symbol.range.start_line
9094        );
9095    }
9096
9097    #[test]
9098    fn symbol_range_skips_unrelated_comments() {
9099        let dir = tempfile::tempdir().unwrap();
9100        let path = dir.path().join("test_gap.go");
9101        std::fs::write(
9102            &path,
9103            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
9104        )
9105        .unwrap();
9106
9107        let provider = TreeSitterProvider::new();
9108        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
9109        assert_eq!(matches.len(), 1);
9110        assert_eq!(
9111            matches[0].symbol.range.start_line, 4,
9112            "symbol range should NOT include comment separated by blank line, got start_line={}",
9113            matches[0].symbol.range.start_line
9114        );
9115    }
9116
9117    #[test]
9118    fn parse_cache_returns_same_tree() {
9119        let mut parser = FileParser::new();
9120        let path = fixture_path("sample.ts");
9121
9122        let (tree1, _) = parser.parse(&path).unwrap();
9123        let tree1_root = tree1.root_node().byte_range();
9124
9125        let (tree2, _) = parser.parse(&path).unwrap();
9126        let tree2_root = tree2.root_node().byte_range();
9127
9128        // Same tree (cache hit) should return identical root node range
9129        assert_eq!(tree1_root, tree2_root);
9130    }
9131
9132    #[test]
9133    fn extract_symbols_from_tree_matches_list_symbols() {
9134        let path = fixture_path("sample.rs");
9135        let source = std::fs::read_to_string(&path).unwrap();
9136
9137        let provider = TreeSitterProvider::new();
9138        let listed = provider.list_symbols(&path).unwrap();
9139
9140        let mut parser = FileParser::new();
9141        let (tree, lang) = parser.parse(&path).unwrap();
9142        let extracted = extract_symbols_from_tree(&source, tree, lang).unwrap();
9143
9144        assert_eq!(symbols_as_debug(&extracted), symbols_as_debug(&listed));
9145    }
9146
9147    fn symbols_as_debug(symbols: &[Symbol]) -> Vec<String> {
9148        symbols
9149            .iter()
9150            .map(|symbol| {
9151                format!(
9152                    "{}|{:?}|{}:{}-{}:{}|{:?}|{:?}|{}|{:?}",
9153                    symbol.name,
9154                    symbol.kind,
9155                    symbol.range.start_line,
9156                    symbol.range.start_col,
9157                    symbol.range.end_line,
9158                    symbol.range.end_col,
9159                    symbol.signature,
9160                    symbol.scope_chain,
9161                    symbol.exported,
9162                    symbol.parent,
9163                )
9164            })
9165            .collect()
9166    }
9167
9168    // --- Python extraction ---
9169
9170    #[test]
9171    fn py_extracts_all_symbols() {
9172        let provider = TreeSitterProvider::new();
9173        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
9174
9175        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
9176        assert!(
9177            names.contains(&"top_level_function"),
9178            "missing top_level_function: {:?}",
9179            names
9180        );
9181        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
9182        assert!(
9183            names.contains(&"instance_method"),
9184            "missing method instance_method: {:?}",
9185            names
9186        );
9187        assert!(
9188            names.contains(&"decorated_function"),
9189            "missing decorated_function: {:?}",
9190            names
9191        );
9192
9193        // Plan requires ≥4 symbols
9194        assert!(
9195            symbols.len() >= 4,
9196            "expected ≥4 symbols, got {}: {:?}",
9197            symbols.len(),
9198            names
9199        );
9200    }
9201
9202    #[test]
9203    fn py_symbol_kinds_correct() {
9204        let provider = TreeSitterProvider::new();
9205        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
9206
9207        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9208
9209        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
9210        assert_eq!(find("MyClass").kind, SymbolKind::Class);
9211        assert_eq!(find("instance_method").kind, SymbolKind::Method);
9212        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
9213        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
9214        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
9215        assert_eq!(find("inner_method").kind, SymbolKind::Method);
9216        assert_eq!(find("outer_method").kind, SymbolKind::Method);
9217    }
9218
9219    #[test]
9220    fn py_method_scope_chain() {
9221        let provider = TreeSitterProvider::new();
9222        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
9223
9224        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9225
9226        // Method inside MyClass
9227        assert_eq!(
9228            find("instance_method").scope_chain,
9229            vec!["MyClass"],
9230            "instance_method should have MyClass in scope chain"
9231        );
9232        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
9233
9234        // Method inside OuterClass > InnerClass
9235        assert_eq!(
9236            find("inner_method").scope_chain,
9237            vec!["OuterClass", "InnerClass"],
9238            "inner_method should have nested scope chain"
9239        );
9240
9241        // InnerClass itself should have OuterClass in scope
9242        assert_eq!(
9243            find("InnerClass").scope_chain,
9244            vec!["OuterClass"],
9245            "InnerClass should have OuterClass in scope"
9246        );
9247
9248        // Top-level function has empty scope
9249        assert!(
9250            find("top_level_function").scope_chain.is_empty(),
9251            "top-level function should have empty scope chain"
9252        );
9253    }
9254
9255    #[test]
9256    fn py_decorated_function_signature() {
9257        let provider = TreeSitterProvider::new();
9258        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
9259
9260        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9261
9262        let sig = find("decorated_function").signature.as_ref().unwrap();
9263        assert!(
9264            sig.contains("@staticmethod"),
9265            "decorated function signature should include decorator: {}",
9266            sig
9267        );
9268        assert!(
9269            sig.contains("def decorated_function"),
9270            "signature should include function def: {}",
9271            sig
9272        );
9273    }
9274
9275    #[test]
9276    fn py_ranges_valid() {
9277        let provider = TreeSitterProvider::new();
9278        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
9279
9280        for s in &symbols {
9281            assert!(
9282                s.range.end_line >= s.range.start_line,
9283                "symbol {} has invalid range: {:?}",
9284                s.name,
9285                s.range
9286            );
9287        }
9288    }
9289
9290    // --- Rust extraction ---
9291
9292    #[test]
9293    fn rs_extracts_all_symbols() {
9294        let provider = TreeSitterProvider::new();
9295        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9296
9297        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
9298        assert!(
9299            names.contains(&"public_function"),
9300            "missing public_function: {:?}",
9301            names
9302        );
9303        assert!(
9304            names.contains(&"private_function"),
9305            "missing private_function: {:?}",
9306            names
9307        );
9308        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
9309        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
9310        assert!(
9311            names.contains(&"Drawable"),
9312            "missing trait Drawable: {:?}",
9313            names
9314        );
9315        // impl methods
9316        assert!(
9317            names.contains(&"new"),
9318            "missing impl method new: {:?}",
9319            names
9320        );
9321
9322        // Plan requires ≥6 symbols
9323        assert!(
9324            symbols.len() >= 6,
9325            "expected ≥6 symbols, got {}: {:?}",
9326            symbols.len(),
9327            names
9328        );
9329    }
9330
9331    #[test]
9332    fn rs_symbol_kinds_correct() {
9333        let provider = TreeSitterProvider::new();
9334        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9335
9336        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9337
9338        assert_eq!(find("public_function").kind, SymbolKind::Function);
9339        assert_eq!(find("private_function").kind, SymbolKind::Function);
9340        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
9341        assert_eq!(find("Color").kind, SymbolKind::Enum);
9342        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
9343        assert_eq!(find("new").kind, SymbolKind::Method);
9344    }
9345
9346    #[test]
9347    fn rs_pub_export_detection() {
9348        let provider = TreeSitterProvider::new();
9349        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9350
9351        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9352
9353        assert!(
9354            find("public_function").exported,
9355            "pub fn should be exported"
9356        );
9357        assert!(
9358            !find("private_function").exported,
9359            "non-pub fn should not be exported"
9360        );
9361        assert!(find("MyStruct").exported, "pub struct should be exported");
9362        assert!(find("Color").exported, "pub enum should be exported");
9363        assert!(find("Drawable").exported, "pub trait should be exported");
9364        assert!(
9365            find("new").exported,
9366            "pub fn inside impl should be exported"
9367        );
9368        assert!(
9369            !find("helper").exported,
9370            "non-pub fn inside impl should not be exported"
9371        );
9372    }
9373
9374    #[test]
9375    fn rs_impl_method_scope_chain() {
9376        let provider = TreeSitterProvider::new();
9377        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9378
9379        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9380
9381        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
9382        assert_eq!(
9383            find("new").scope_chain,
9384            vec!["MyStruct"],
9385            "impl method should have type in scope chain"
9386        );
9387        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
9388
9389        // Free function has empty scope chain
9390        assert!(
9391            find("public_function").scope_chain.is_empty(),
9392            "free function should have empty scope chain"
9393        );
9394    }
9395
9396    #[test]
9397    fn rs_trait_impl_scope_chain() {
9398        let provider = TreeSitterProvider::new();
9399        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9400
9401        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
9402        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
9403        assert_eq!(
9404            draw.scope_chain,
9405            vec!["Drawable for MyStruct"],
9406            "trait impl method should have 'Trait for Type' scope"
9407        );
9408        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
9409    }
9410
9411    #[test]
9412    fn rs_ranges_valid() {
9413        let provider = TreeSitterProvider::new();
9414        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
9415
9416        for s in &symbols {
9417            assert!(
9418                s.range.end_line >= s.range.start_line,
9419                "symbol {} has invalid range: {:?}",
9420                s.name,
9421                s.range
9422            );
9423        }
9424    }
9425
9426    // --- Go extraction ---
9427
9428    #[test]
9429    fn go_extracts_all_symbols() {
9430        let provider = TreeSitterProvider::new();
9431        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
9432
9433        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
9434        assert!(
9435            names.contains(&"ExportedFunction"),
9436            "missing ExportedFunction: {:?}",
9437            names
9438        );
9439        assert!(
9440            names.contains(&"unexportedFunction"),
9441            "missing unexportedFunction: {:?}",
9442            names
9443        );
9444        assert!(
9445            names.contains(&"MyStruct"),
9446            "missing struct MyStruct: {:?}",
9447            names
9448        );
9449        assert!(
9450            names.contains(&"Reader"),
9451            "missing interface Reader: {:?}",
9452            names
9453        );
9454        // receiver method
9455        assert!(
9456            names.contains(&"String"),
9457            "missing receiver method String: {:?}",
9458            names
9459        );
9460
9461        // Plan requires ≥4 symbols
9462        assert!(
9463            symbols.len() >= 4,
9464            "expected ≥4 symbols, got {}: {:?}",
9465            symbols.len(),
9466            names
9467        );
9468    }
9469
9470    #[test]
9471    fn go_symbol_kinds_correct() {
9472        let provider = TreeSitterProvider::new();
9473        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
9474
9475        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9476
9477        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
9478        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
9479        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
9480        assert_eq!(find("Reader").kind, SymbolKind::Interface);
9481        assert_eq!(find("String").kind, SymbolKind::Method);
9482        assert_eq!(find("helper").kind, SymbolKind::Method);
9483    }
9484
9485    #[test]
9486    fn go_uppercase_export_detection() {
9487        let provider = TreeSitterProvider::new();
9488        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
9489
9490        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9491
9492        assert!(
9493            find("ExportedFunction").exported,
9494            "ExportedFunction (uppercase) should be exported"
9495        );
9496        assert!(
9497            !find("unexportedFunction").exported,
9498            "unexportedFunction (lowercase) should not be exported"
9499        );
9500        assert!(
9501            find("MyStruct").exported,
9502            "MyStruct (uppercase) should be exported"
9503        );
9504        assert!(
9505            find("Reader").exported,
9506            "Reader (uppercase) should be exported"
9507        );
9508        assert!(
9509            find("String").exported,
9510            "String method (uppercase) should be exported"
9511        );
9512        assert!(
9513            !find("helper").exported,
9514            "helper method (lowercase) should not be exported"
9515        );
9516    }
9517
9518    #[test]
9519    fn go_receiver_method_scope_chain() {
9520        let provider = TreeSitterProvider::new();
9521        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
9522
9523        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
9524
9525        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
9526        assert_eq!(
9527            find("String").scope_chain,
9528            vec!["MyStruct"],
9529            "receiver method should have type in scope chain"
9530        );
9531        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
9532
9533        // Regular function has empty scope chain
9534        assert!(
9535            find("ExportedFunction").scope_chain.is_empty(),
9536            "regular function should have empty scope chain"
9537        );
9538    }
9539
9540    #[test]
9541    fn go_ranges_valid() {
9542        let provider = TreeSitterProvider::new();
9543        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
9544
9545        for s in &symbols {
9546            assert!(
9547                s.range.end_line >= s.range.start_line,
9548                "symbol {} has invalid range: {:?}",
9549                s.name,
9550                s.range
9551            );
9552        }
9553    }
9554
9555    // --- Cross-language ---
9556
9557    #[test]
9558    fn cross_language_all_six_produce_symbols() {
9559        let provider = TreeSitterProvider::new();
9560
9561        let fixtures = [
9562            ("sample.ts", "TypeScript"),
9563            ("sample.tsx", "TSX"),
9564            ("sample.js", "JavaScript"),
9565            ("sample.py", "Python"),
9566            ("sample.rs", "Rust"),
9567            ("sample.go", "Go"),
9568        ];
9569
9570        for (fixture, lang) in &fixtures {
9571            let symbols = provider
9572                .list_symbols(&fixture_path(fixture))
9573                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
9574            assert!(
9575                symbols.len() >= 2,
9576                "{} should produce ≥2 symbols, got {}: {:?}",
9577                lang,
9578                symbols.len(),
9579                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
9580            );
9581        }
9582    }
9583
9584    // --- Symbol cache tests ---
9585
9586    fn cached_freshness_fixture(
9587        content: &str,
9588    ) -> (tempfile::TempDir, PathBuf, SystemTime, u64, blake3::Hash) {
9589        let dir = tempfile::tempdir().unwrap();
9590        let file = dir.path().join("test.rs");
9591        std::fs::write(&file, content).unwrap();
9592        filetime::set_file_mtime(&file, filetime::FileTime::from_unix_time(1, 0)).unwrap();
9593        let metadata = std::fs::metadata(&file).unwrap();
9594        let mtime = metadata.modified().unwrap();
9595        let size = metadata.len();
9596        let hash = cache_freshness::hash_bytes(content.as_bytes());
9597        (dir, file, mtime, size, hash)
9598    }
9599
9600    #[test]
9601    fn cached_freshness_unchanged_metadata_skips_hashing() {
9602        let (_dir, file, mtime, size, hash) = cached_freshness_fixture("pub fn hello() {}\n");
9603
9604        cache_freshness::reset_hash_file_if_small_count_for_debug();
9605        assert!(cached_file_is_fresh(&file, mtime, size, hash, mtime));
9606        assert_eq!(cache_freshness::hash_file_if_small_count_for_debug(), 0);
9607    }
9608
9609    #[test]
9610    fn cached_freshness_changed_mtime_with_identical_content_hashes_fresh() {
9611        let (_dir, file, mtime, size, hash) = cached_freshness_fixture("pub fn hello() {}\n");
9612        filetime::set_file_mtime(&file, filetime::FileTime::from_unix_time(2, 0)).unwrap();
9613
9614        cache_freshness::reset_hash_file_if_small_count_for_debug();
9615        assert!(cached_file_is_fresh(&file, mtime, size, hash, mtime));
9616        assert_eq!(cache_freshness::hash_file_if_small_count_for_debug(), 1);
9617    }
9618
9619    #[test]
9620    fn cached_freshness_changed_mtime_and_same_size_content_hashes_stale() {
9621        let (_dir, file, mtime, size, hash) = cached_freshness_fixture("pub fn one() {}\n");
9622        std::fs::write(&file, "pub fn two() {}\n").unwrap();
9623        filetime::set_file_mtime(&file, filetime::FileTime::from_unix_time(2, 0)).unwrap();
9624        assert_eq!(std::fs::metadata(&file).unwrap().len(), size);
9625
9626        cache_freshness::reset_hash_file_if_small_count_for_debug();
9627        assert!(!cached_file_is_fresh(&file, mtime, size, hash, mtime));
9628        assert_eq!(cache_freshness::hash_file_if_small_count_for_debug(), 1);
9629    }
9630
9631    #[test]
9632    fn cached_freshness_changed_size_skips_hashing() {
9633        let (_dir, file, mtime, size, hash) = cached_freshness_fixture("pub fn hello() {}\n");
9634        std::fs::write(&file, "pub fn hello() {}\n// longer\n").unwrap();
9635
9636        cache_freshness::reset_hash_file_if_small_count_for_debug();
9637        assert!(!cached_file_is_fresh(&file, mtime, size, hash, mtime));
9638        assert_eq!(cache_freshness::hash_file_if_small_count_for_debug(), 0);
9639    }
9640
9641    #[test]
9642    fn symbol_cache_returns_cached_results_on_second_call() {
9643        let dir = tempfile::tempdir().unwrap();
9644        let file = dir.path().join("test.rs");
9645        std::fs::write(&file, "pub fn hello() {}\npub fn world() {}").unwrap();
9646
9647        let mut parser = FileParser::new();
9648
9649        let symbols1 = parser.extract_symbols(&file).unwrap();
9650        assert_eq!(symbols1.len(), 2);
9651
9652        // Second call should return cached result
9653        let symbols2 = parser.extract_symbols(&file).unwrap();
9654        assert_eq!(symbols2.len(), 2);
9655        assert_eq!(symbols1[0].name, symbols2[0].name);
9656
9657        // Verify cache is populated
9658        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
9659    }
9660
9661    #[test]
9662    fn symbol_cache_invalidates_on_file_change() {
9663        let dir = tempfile::tempdir().unwrap();
9664        let file = dir.path().join("test.rs");
9665        std::fs::write(&file, "pub fn hello() {}").unwrap();
9666
9667        let mut parser = FileParser::new();
9668
9669        let symbols1 = parser.extract_symbols(&file).unwrap();
9670        assert_eq!(symbols1.len(), 1);
9671        assert_eq!(symbols1[0].name, "hello");
9672
9673        // Wait to ensure mtime changes (filesystem resolution can be 1s on some OS)
9674        std::thread::sleep(std::time::Duration::from_millis(50));
9675
9676        // Modify file — add a second function
9677        std::fs::write(&file, "pub fn hello() {}\npub fn goodbye() {}").unwrap();
9678
9679        // Should detect mtime change and re-extract
9680        let symbols2 = parser.extract_symbols(&file).unwrap();
9681        assert_eq!(symbols2.len(), 2);
9682        assert!(symbols2.iter().any(|s| s.name == "goodbye"));
9683    }
9684
9685    #[test]
9686    fn symbol_cache_invalidate_method_clears_entry() {
9687        let dir = tempfile::tempdir().unwrap();
9688        let file = dir.path().join("test.rs");
9689        std::fs::write(&file, "pub fn hello() {}").unwrap();
9690
9691        let mut parser = FileParser::new();
9692        parser.extract_symbols(&file).unwrap();
9693        assert!(parser.symbol_cache.read().unwrap().contains_key(&file));
9694
9695        parser.invalidate_symbols(&file);
9696        assert!(!parser.symbol_cache.read().unwrap().contains_key(&file));
9697        // Parse tree cache should also be cleared
9698        assert!(!parser.cache.contains_key(&file));
9699    }
9700
9701    #[test]
9702    fn symbol_cache_works_for_multiple_languages() {
9703        let dir = tempfile::tempdir().unwrap();
9704        let rs_file = dir.path().join("lib.rs");
9705        let ts_file = dir.path().join("app.ts");
9706        let py_file = dir.path().join("main.py");
9707
9708        std::fs::write(&rs_file, "pub fn rust_fn() {}").unwrap();
9709        std::fs::write(&ts_file, "export function tsFn() {}").unwrap();
9710        std::fs::write(&py_file, "def py_fn():\n    pass").unwrap();
9711
9712        let mut parser = FileParser::new();
9713
9714        let rs_syms = parser.extract_symbols(&rs_file).unwrap();
9715        let ts_syms = parser.extract_symbols(&ts_file).unwrap();
9716        let py_syms = parser.extract_symbols(&py_file).unwrap();
9717
9718        assert!(rs_syms.iter().any(|s| s.name == "rust_fn"));
9719        assert!(ts_syms.iter().any(|s| s.name == "tsFn"));
9720        assert!(py_syms.iter().any(|s| s.name == "py_fn"));
9721
9722        // All should be cached now
9723        assert_eq!(parser.symbol_cache.read().unwrap().len(), 3);
9724
9725        // Re-extract should return same results from cache
9726        let rs_syms2 = parser.extract_symbols(&rs_file).unwrap();
9727        assert_eq!(rs_syms.len(), rs_syms2.len());
9728    }
9729
9730    #[test]
9731    fn extract_json_symbols_top_level_keys() {
9732        let dir = tempfile::tempdir().unwrap();
9733        let file = dir.path().join("package.json");
9734        std::fs::write(&file, r#"{"name": "x", "version": "1"}"#).unwrap();
9735
9736        let mut parser = FileParser::new();
9737        let symbols = parser.extract_symbols(&file).unwrap();
9738
9739        assert_eq!(symbols.len(), 2);
9740        assert!(symbols
9741            .iter()
9742            .any(|s| s.name == "name" && s.kind == SymbolKind::Variable));
9743        assert!(symbols
9744            .iter()
9745            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
9746    }
9747
9748    #[test]
9749    fn extract_json_symbols_root_array() {
9750        let dir = tempfile::tempdir().unwrap();
9751        let file = dir.path().join("array.json");
9752        std::fs::write(&file, "[1,2,3]").unwrap();
9753
9754        let mut parser = FileParser::new();
9755        let symbols = parser.extract_symbols(&file).unwrap();
9756
9757        assert_eq!(symbols.len(), 0);
9758    }
9759
9760    #[test]
9761    fn extract_json_symbols_no_recursion_into_nested() {
9762        let dir = tempfile::tempdir().unwrap();
9763        let file = dir.path().join("nested.json");
9764        std::fs::write(&file, r#"{"scripts": {"build": "tsc"}}"#).unwrap();
9765
9766        let mut parser = FileParser::new();
9767        let symbols = parser.extract_symbols(&file).unwrap();
9768
9769        assert_eq!(symbols.len(), 1);
9770        assert_eq!(symbols[0].name, "scripts");
9771        assert!(!symbols.iter().any(|s| s.name == "build"));
9772    }
9773
9774    #[test]
9775    fn extract_scala_symbols_object_and_method() {
9776        let dir = tempfile::tempdir().unwrap();
9777        let file = dir.path().join("Greeter.scala");
9778        std::fs::write(
9779            &file,
9780            "object Greeter {\n  def hello(name: String): String = s\"hi $name\"\n}",
9781        )
9782        .unwrap();
9783
9784        let mut parser = FileParser::new();
9785        let symbols = parser.extract_symbols(&file).unwrap();
9786
9787        assert!(symbols
9788            .iter()
9789            .any(|s| s.name == "Greeter" && s.kind == SymbolKind::Class));
9790        assert!(symbols.iter().any(|s| s.name == "hello"
9791            && s.kind == SymbolKind::Method
9792            && s.scope_chain == vec!["Greeter".to_string()]));
9793    }
9794
9795    #[test]
9796    fn extract_scala_symbols_class_and_trait() {
9797        let dir = tempfile::tempdir().unwrap();
9798        let file = dir.path().join("Types.scala");
9799        std::fs::write(&file, "class Foo\ntrait Bar").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 == "Foo" && s.kind == SymbolKind::Class));
9807        assert!(symbols
9808            .iter()
9809            .any(|s| s.name == "Bar" && s.kind == SymbolKind::Interface));
9810    }
9811
9812    #[test]
9813    fn extract_yaml_symbols_k8s_resource() {
9814        let dir = tempfile::tempdir().unwrap();
9815        let file = dir.path().join("deployment.yaml");
9816        std::fs::write(
9817            &file,
9818            r#"apiVersion: apps/v1
9819kind: Deployment
9820metadata:
9821  name: nginx
9822  namespace: web
9823"#,
9824        )
9825        .unwrap();
9826
9827        let mut parser = FileParser::new();
9828        let symbols = parser.extract_symbols(&file).unwrap();
9829
9830        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Deployment");
9831        let sym = &symbols[0];
9832        assert_eq!(sym.name, "web/Deployment/nginx");
9833        assert_eq!(sym.kind, SymbolKind::Class);
9834        assert!(sym.exported);
9835    }
9836
9837    #[test]
9838    fn extract_yaml_symbols_k8s_no_namespace() {
9839        let dir = tempfile::tempdir().unwrap();
9840        let file = dir.path().join("service.yaml");
9841        std::fs::write(
9842            &file,
9843            r#"apiVersion: v1
9844kind: Service
9845metadata:
9846  name: nginx-svc
9847"#,
9848        )
9849        .unwrap();
9850
9851        let mut parser = FileParser::new();
9852        let symbols = parser.extract_symbols(&file).unwrap();
9853
9854        assert_eq!(symbols.len(), 1, "Expected 1 symbol for K8s Service");
9855        let sym = &symbols[0];
9856        assert_eq!(sym.name, "Service/nginx-svc");
9857        assert_eq!(sym.kind, SymbolKind::Class);
9858    }
9859
9860    #[test]
9861    fn extract_yaml_symbols_multidoc() {
9862        let dir = tempfile::tempdir().unwrap();
9863        let file = dir.path().join("multidoc.yaml");
9864        std::fs::write(
9865            &file,
9866            r#"apiVersion: apps/v1
9867kind: Deployment
9868metadata:
9869  name: a
9870---
9871apiVersion: v1
9872kind: Service
9873metadata:
9874  name: b
9875"#,
9876        )
9877        .unwrap();
9878
9879        let mut parser = FileParser::new();
9880        let symbols = parser.extract_symbols(&file).unwrap();
9881
9882        assert_eq!(symbols.len(), 2, "Expected 2 symbols for multi-doc YAML");
9883        assert!(symbols.iter().any(|s| s.name == "Deployment/a"));
9884        assert!(symbols.iter().any(|s| s.name == "Service/b"));
9885    }
9886
9887    #[test]
9888    fn extract_yaml_symbols_generic_fallback() {
9889        let dir = tempfile::tempdir().unwrap();
9890        let file = dir.path().join("compose.yaml");
9891        std::fs::write(
9892            &file,
9893            r#"version: "3"
9894services:
9895  web: {}
9896volumes:
9897  data: {}
9898"#,
9899        )
9900        .unwrap();
9901
9902        let mut parser = FileParser::new();
9903        let symbols = parser.extract_symbols(&file).unwrap();
9904
9905        // Should have top-level keys: version, services, volumes
9906        assert_eq!(symbols.len(), 3, "Expected 3 symbols for generic YAML");
9907        assert!(symbols
9908            .iter()
9909            .any(|s| s.name == "version" && s.kind == SymbolKind::Variable));
9910        assert!(symbols
9911            .iter()
9912            .any(|s| s.name == "services" && s.kind == SymbolKind::Variable));
9913        assert!(symbols
9914            .iter()
9915            .any(|s| s.name == "volumes" && s.kind == SymbolKind::Variable));
9916    }
9917
9918    #[test]
9919    fn extract_yaml_symbols_empty() {
9920        let dir = tempfile::tempdir().unwrap();
9921        let file = dir.path().join("empty.yaml");
9922        std::fs::write(&file, "").unwrap();
9923
9924        let mut parser = FileParser::new();
9925        let symbols = parser.extract_symbols(&file).unwrap();
9926
9927        assert_eq!(symbols.len(), 0, "Expected 0 symbols for empty YAML");
9928    }
9929
9930    #[test]
9931    fn extract_yaml_symbols_resource_limits() {
9932        let dir = tempfile::tempdir().unwrap();
9933        let file = dir.path().join("deployment.yaml");
9934        std::fs::write(
9935            &file,
9936            r#"apiVersion: apps/v1
9937kind: Deployment
9938metadata:
9939  name: app
9940spec:
9941  template:
9942    spec:
9943      containers:
9944      - name: main
9945        image: myapp:1.0
9946        resources:
9947          limits:
9948            cpu: "2"
9949            memory: 1Gi
9950          requests:
9951            cpu: "1"
9952            memory: 512Mi
9953"#,
9954        )
9955        .unwrap();
9956
9957        let mut parser = FileParser::new();
9958        let symbols = parser.extract_symbols(&file).unwrap();
9959
9960        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
9961        let sym = &symbols[0];
9962        assert_eq!(sym.name, "Deployment/app");
9963        let sig = sym.signature.as_ref().unwrap();
9964        assert!(sig.contains("cpu="), "Signature should contain cpu= field");
9965        assert!(
9966            sig.contains("memory="),
9967            "Signature should contain memory= field"
9968        );
9969    }
9970
9971    #[test]
9972    fn extract_yaml_symbols_env_names() {
9973        let dir = tempfile::tempdir().unwrap();
9974        let file = dir.path().join("deployment.yaml");
9975        std::fs::write(
9976            &file,
9977            r#"apiVersion: apps/v1
9978kind: Deployment
9979metadata:
9980  name: app
9981spec:
9982  template:
9983    spec:
9984      containers:
9985      - name: main
9986        image: myapp:1.0
9987        env:
9988        - name: FOO
9989          value: "bar"
9990        - name: BAR
9991          value: "baz"
9992"#,
9993        )
9994        .unwrap();
9995
9996        let mut parser = FileParser::new();
9997        let symbols = parser.extract_symbols(&file).unwrap();
9998
9999        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Deployment");
10000        let sym = &symbols[0];
10001        let sig = sym.signature.as_ref().unwrap();
10002        assert!(
10003            sig.contains("env=FOO,BAR"),
10004            "Signature should contain env=FOO,BAR"
10005        );
10006    }
10007
10008    #[test]
10009    fn extract_yaml_symbols_rbac_rules() {
10010        let dir = tempfile::tempdir().unwrap();
10011        let file = dir.path().join("role.yaml");
10012        std::fs::write(
10013            &file,
10014            r#"apiVersion: rbac.authorization.k8s.io/v1
10015kind: Role
10016metadata:
10017  name: reader
10018rules:
10019- apiGroups: [""]
10020  resources: [pods, services]
10021  verbs: [get, list, watch]
10022"#,
10023        )
10024        .unwrap();
10025
10026        let mut parser = FileParser::new();
10027        let symbols = parser.extract_symbols(&file).unwrap();
10028
10029        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Role");
10030        let sym = &symbols[0];
10031        let sig = sym.signature.as_ref().unwrap();
10032        assert!(
10033            sig.contains("verbs=get,list,watch"),
10034            "Signature should contain verbs=get,list,watch"
10035        );
10036        assert!(
10037            sig.contains("resources=pods,services"),
10038            "Signature should contain resources=pods,services"
10039        );
10040    }
10041
10042    #[test]
10043    fn extract_yaml_symbols_argo_workflow() {
10044        let dir = tempfile::tempdir().unwrap();
10045        let file = dir.path().join("workflow.yaml");
10046        std::fs::write(
10047            &file,
10048            r#"apiVersion: argoproj.io/v1alpha1
10049kind: Workflow
10050metadata:
10051  name: hello-world
10052spec:
10053  entrypoint: main
10054  templates:
10055  - name: main
10056    container:
10057      image: alpine:3.18
10058      command: [echo]
10059      args: ["hello"]
10060  - name: print
10061    container:
10062      image: alpine:3.18
10063      command: [echo]
10064      args: ["world"]
10065"#,
10066        )
10067        .unwrap();
10068
10069        let mut parser = FileParser::new();
10070        let symbols = parser.extract_symbols(&file).unwrap();
10071
10072        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
10073        let sym = &symbols[0];
10074        assert!(
10075            sym.name.contains("Workflow"),
10076            "Symbol name should contain Workflow"
10077        );
10078        let sig = sym.signature.as_ref().unwrap();
10079        assert!(
10080            sig.contains("entrypoint=main"),
10081            "Signature should contain entrypoint=main"
10082        );
10083        assert!(
10084            sig.contains("templates=main,print"),
10085            "Signature should contain templates=main,print"
10086        );
10087        assert!(
10088            sig.contains("image=alpine:3.18"),
10089            "Signature should contain image=alpine:3.18"
10090        );
10091        assert!(
10092            sig.contains("command=echo"),
10093            "Signature should contain command=echo"
10094        );
10095    }
10096
10097    #[test]
10098    fn extract_yaml_symbols_generatename_fallback() {
10099        let dir = tempfile::tempdir().unwrap();
10100        let file = dir.path().join("workflow.yaml");
10101        std::fs::write(
10102            &file,
10103            r#"apiVersion: argoproj.io/v1alpha1
10104kind: Workflow
10105metadata:
10106  generateName: hello-
10107spec:
10108  entrypoint: main
10109  templates:
10110  - name: main
10111    container:
10112      image: alpine:3.18
10113"#,
10114        )
10115        .unwrap();
10116
10117        let mut parser = FileParser::new();
10118        let symbols = parser.extract_symbols(&file).unwrap();
10119
10120        assert_eq!(symbols.len(), 1, "Expected 1 symbol for Workflow");
10121        let sym = &symbols[0];
10122        assert!(
10123            sym.name.contains("hello-"),
10124            "Symbol name should contain generateName fallback 'hello-'"
10125        );
10126    }
10127
10128    #[test]
10129    fn detect_r_extensions_are_case_sensitive() {
10130        assert_eq!(detect_language(Path::new("analysis.R")), Some(LangId::R));
10131        assert_eq!(detect_language(Path::new("script.r")), Some(LangId::R));
10132    }
10133
10134    #[test]
10135    fn extract_r_symbols_test() {
10136        let dir = tempfile::tempdir().unwrap();
10137        let file = dir.path().join("analysis.R");
10138        std::fs::write(
10139            &file,
10140            r#"
10141# summary function
10142summarise <- function(data, column) {
10143  total <- sum(data[[column]])
10144  total
10145}
10146
10147normalise = function(x) {
10148  x / max(x)
10149}
10150
10151function(y) {
10152  y + 1
10153} -> transform_values
10154
10155threshold <- 10
10156"done" -> status
10157
10158outer <- function(values) {
10159  local_value <- 1
10160  local_value
10161}
10162"#,
10163        )
10164        .unwrap();
10165
10166        let mut parser = FileParser::new();
10167        let symbols = parser.extract_symbols(&file).unwrap();
10168
10169        let get = |name: &str| {
10170            symbols
10171                .iter()
10172                .find(|symbol| symbol.name == name)
10173                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
10174        };
10175
10176        assert_eq!(get("summarise").kind, SymbolKind::Function);
10177        assert_eq!(get("normalise").kind, SymbolKind::Function);
10178        assert_eq!(get("transform_values").kind, SymbolKind::Function);
10179        assert!(
10180            symbols
10181                .iter()
10182                .all(|symbol| !symbol.name.starts_with("function(")),
10183            "rightward function assignments should use the RHS identifier: {symbols:?}"
10184        );
10185        assert_eq!(get("outer").kind, SymbolKind::Function);
10186        assert_eq!(get("threshold").kind, SymbolKind::Variable);
10187        assert_eq!(get("status").kind, SymbolKind::Variable);
10188        assert!(
10189            symbols.iter().all(|symbol| symbol.name != "local_value"),
10190            "nested assignments should not surface as top-level variables: {symbols:?}"
10191        );
10192    }
10193
10194    #[test]
10195    fn extract_pascal_symbols_test() {
10196        let dir = tempfile::tempdir().unwrap();
10197        let file = dir.path().join("MyUnit.pas");
10198        std::fs::write(
10199            &file,
10200            r#"
10201unit MyUnit;
10202
10203interface
10204
10205uses SysUtils;
10206
10207type
10208  TMyClass = class
10209  private
10210    FValue: Integer;
10211  public
10212    constructor Create;
10213    procedure DoSomething; virtual;
10214  end;
10215
10216  TMyRecord = record
10217    X, Y: Integer;
10218  end;
10219
10220  IMyInterface = interface
10221    procedure DoSomethingElse;
10222  end;
10223
10224  TMyEnum = (Red, Green, Blue);
10225
10226const
10227  UNIT_CONST = 42;
10228
10229var
10230  UnitVar: string;
10231
10232implementation
10233
10234constructor TMyClass.Create;
10235begin
10236  FValue := 0;
10237end;
10238
10239procedure TMyClass.DoSomething;
10240begin
10241end;
10242
10243procedure StandaloneProc;
10244begin
10245end;
10246
10247end.
10248"#,
10249        )
10250        .unwrap();
10251
10252        let mut parser = FileParser::new();
10253        let symbols = parser.extract_symbols(&file).unwrap();
10254
10255        let get = |name: &str| {
10256            symbols
10257                .iter()
10258                .find(|symbol| symbol.name == name)
10259                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
10260        };
10261
10262        assert_eq!(get("MyUnit").kind, SymbolKind::Class);
10263        assert_eq!(get("TMyClass").kind, SymbolKind::Class);
10264        assert_eq!(get("TMyRecord").kind, SymbolKind::Struct);
10265        assert_eq!(get("IMyInterface").kind, SymbolKind::Interface);
10266        assert_eq!(get("TMyEnum").kind, SymbolKind::Enum);
10267        assert_eq!(get("UNIT_CONST").kind, SymbolKind::Variable);
10268        assert_eq!(get("UnitVar").kind, SymbolKind::Variable);
10269        assert_eq!(get("StandaloneProc").kind, SymbolKind::Function);
10270
10271        // Methods inside TMyClass
10272        let create_methods: Vec<&Symbol> = symbols.iter().filter(|s| s.name == "Create").collect();
10273        assert_eq!(create_methods.len(), 2); // one in interface, one in implementation
10274        for m in create_methods {
10275            assert_eq!(m.kind, SymbolKind::Method);
10276            assert_eq!(m.scope_chain, vec!["TMyClass".to_string()]);
10277        }
10278
10279        let do_something_methods: Vec<&Symbol> =
10280            symbols.iter().filter(|s| s.name == "DoSomething").collect();
10281        assert_eq!(do_something_methods.len(), 2); // one in interface, one in implementation
10282        for m in do_something_methods {
10283            assert_eq!(m.kind, SymbolKind::Method);
10284            assert_eq!(m.scope_chain, vec!["TMyClass".to_string()]);
10285        }
10286    }
10287
10288    #[test]
10289    fn groovy_fixture_extracts_types_methods_and_properties() {
10290        let provider = TreeSitterProvider::new();
10291        let symbols = provider
10292            .list_symbols(&fixture_path("sample.groovy"))
10293            .unwrap();
10294
10295        let get = |name: &str| {
10296            symbols
10297                .iter()
10298                .find(|symbol| symbol.name == name)
10299                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
10300        };
10301
10302        assert_eq!(get("GreeterSupport").kind, SymbolKind::Interface);
10303        assert_eq!(get("Named").kind, SymbolKind::Interface);
10304        assert_eq!(get("BuildStatus").kind, SymbolKind::Enum);
10305        assert_eq!(get("BuildLogic").kind, SymbolKind::Class);
10306        assert_eq!(get("action").kind, SymbolKind::Variable);
10307        assert_eq!(get("action").scope_chain, vec!["BuildLogic".to_string()]);
10308        assert_eq!(get("greet").kind, SymbolKind::Method);
10309        assert_eq!(get("greet").scope_chain, vec!["BuildLogic".to_string()]);
10310        assert_eq!(get("topLevelHelper").kind, SymbolKind::Function);
10311        assert!(get("topLevelHelper").scope_chain.is_empty());
10312
10313        assert_eq!(get("BuildLogic").range.start_line, 12);
10314        assert_eq!(get("BuildLogic").range.end_line, 23);
10315        assert_eq!(get("action").range.start_line, 14);
10316        assert_eq!(get("action").range.end_line, 14);
10317        assert_eq!(get("greet").range.start_line, 16);
10318        assert_eq!(get("greet").range.end_line, 18);
10319        assert_eq!(get("topLevelHelper").range.start_line, 25);
10320        assert_eq!(get("topLevelHelper").range.end_line, 27);
10321    }
10322
10323    #[test]
10324    fn groovy_gradle_fixture_extracts_task_declarations() {
10325        let provider = TreeSitterProvider::new();
10326        let symbols = provider
10327            .list_symbols(&fixture_path("build.gradle"))
10328            .unwrap();
10329
10330        let get = |name: &str| {
10331            symbols
10332                .iter()
10333                .find(|symbol| symbol.name == name)
10334                .unwrap_or_else(|| panic!("missing {name}; got {symbols:?}"))
10335        };
10336
10337        assert_eq!(get("smokeTest").kind, SymbolKind::Function);
10338        assert_eq!(get("smokeTest").range.start_line, 8);
10339        assert_eq!(get("smokeTest").range.end_line, 12);
10340        assert_eq!(get("ciCheck").kind, SymbolKind::Function);
10341        assert_eq!(get("ciCheck").range.start_line, 14);
10342        assert_eq!(get("ciCheck").range.end_line, 18);
10343    }
10344
10345    #[test]
10346    fn groovy_jenkinsfile_fixture_extracts_pipeline_block() {
10347        let provider = TreeSitterProvider::new();
10348        let symbols = provider.list_symbols(&fixture_path("Jenkinsfile")).unwrap();
10349
10350        let pipeline = symbols
10351            .iter()
10352            .find(|symbol| symbol.name == "pipeline")
10353            .unwrap_or_else(|| panic!("missing pipeline; got {symbols:?}"));
10354        assert_eq!(pipeline.kind, SymbolKind::Function);
10355        assert_eq!(pipeline.range.start_line, 0);
10356        assert_eq!(pipeline.range.end_line, 9);
10357    }
10358}