Skip to main content

aft/
parser.rs

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