Skip to main content

aft/
parser.rs

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