Skip to main content

aft/
parser.rs

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