Skip to main content

aft/
parser.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use streaming_iterator::StreamingIterator;
7use tree_sitter::{Language, Node, Parser, Query, QueryCursor, Tree};
8
9use crate::error::AftError;
10use crate::symbols::{Range, Symbol, SymbolKind, SymbolMatch};
11
12// --- Query patterns embedded at compile time ---
13
14const TS_QUERY: &str = r#"
15;; function declarations
16(function_declaration
17  name: (identifier) @fn.name) @fn.def
18
19;; arrow functions assigned to const/let/var
20(lexical_declaration
21  (variable_declarator
22    name: (identifier) @arrow.name
23    value: (arrow_function) @arrow.body)) @arrow.def
24
25;; class declarations
26(class_declaration
27  name: (type_identifier) @class.name) @class.def
28
29;; method definitions inside classes
30(class_declaration
31  name: (type_identifier) @method.class_name
32  body: (class_body
33    (method_definition
34      name: (property_identifier) @method.name) @method.def))
35
36;; interface declarations
37(interface_declaration
38  name: (type_identifier) @interface.name) @interface.def
39
40;; enum declarations
41(enum_declaration
42  name: (identifier) @enum.name) @enum.def
43
44;; type alias declarations
45(type_alias_declaration
46  name: (type_identifier) @type_alias.name) @type_alias.def
47
48;; export statement wrappers (top-level only)
49(export_statement) @export.stmt
50"#;
51
52const JS_QUERY: &str = r#"
53;; function declarations
54(function_declaration
55  name: (identifier) @fn.name) @fn.def
56
57;; arrow functions assigned to const/let/var
58(lexical_declaration
59  (variable_declarator
60    name: (identifier) @arrow.name
61    value: (arrow_function) @arrow.body)) @arrow.def
62
63;; class declarations
64(class_declaration
65  name: (identifier) @class.name) @class.def
66
67;; method definitions inside classes
68(class_declaration
69  name: (identifier) @method.class_name
70  body: (class_body
71    (method_definition
72      name: (property_identifier) @method.name) @method.def))
73
74;; export statement wrappers (top-level only)
75(export_statement) @export.stmt
76"#;
77
78const PY_QUERY: &str = r#"
79;; function definitions (top-level and nested)
80(function_definition
81  name: (identifier) @fn.name) @fn.def
82
83;; class definitions
84(class_definition
85  name: (identifier) @class.name) @class.def
86
87;; decorated definitions (wraps function_definition or class_definition)
88(decorated_definition
89  (decorator) @dec.decorator) @dec.def
90"#;
91
92const RS_QUERY: &str = r#"
93;; free functions (with optional visibility)
94(function_item
95  name: (identifier) @fn.name) @fn.def
96
97;; struct items
98(struct_item
99  name: (type_identifier) @struct.name) @struct.def
100
101;; enum items
102(enum_item
103  name: (type_identifier) @enum.name) @enum.def
104
105;; trait items
106(trait_item
107  name: (type_identifier) @trait.name) @trait.def
108
109;; impl blocks — capture the whole block to find methods
110(impl_item) @impl.def
111
112;; visibility modifiers on any item
113(visibility_modifier) @vis.mod
114"#;
115
116const GO_QUERY: &str = r#"
117;; function declarations
118(function_declaration
119  name: (identifier) @fn.name) @fn.def
120
121;; method declarations (with receiver)
122(method_declaration
123  name: (field_identifier) @method.name) @method.def
124
125;; type declarations (struct and interface)
126(type_declaration
127  (type_spec
128    name: (type_identifier) @type.name
129    type: (_) @type.body)) @type.def
130"#;
131
132/// Supported language identifier.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134pub enum LangId {
135    TypeScript,
136    Tsx,
137    JavaScript,
138    Python,
139    Rust,
140    Go,
141    Markdown,
142}
143
144/// Maps file extension to language identifier.
145pub fn detect_language(path: &Path) -> Option<LangId> {
146    let ext = path.extension()?.to_str()?;
147    match ext {
148        "ts" => Some(LangId::TypeScript),
149        "tsx" => Some(LangId::Tsx),
150        "js" | "jsx" => Some(LangId::JavaScript),
151        "py" => Some(LangId::Python),
152        "rs" => Some(LangId::Rust),
153        "go" => Some(LangId::Go),
154        "md" | "markdown" | "mdx" => Some(LangId::Markdown),
155        _ => None,
156    }
157}
158
159/// Returns the tree-sitter Language grammar for a given LangId.
160pub fn grammar_for(lang: LangId) -> Language {
161    match lang {
162        LangId::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
163        LangId::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
164        LangId::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
165        LangId::Python => tree_sitter_python::LANGUAGE.into(),
166        LangId::Rust => tree_sitter_rust::LANGUAGE.into(),
167        LangId::Go => tree_sitter_go::LANGUAGE.into(),
168        LangId::Markdown => tree_sitter_md::LANGUAGE.into(),
169    }
170}
171
172/// Returns the query pattern string for a given LangId, if implemented.
173fn query_for(lang: LangId) -> Option<&'static str> {
174    match lang {
175        LangId::TypeScript | LangId::Tsx => Some(TS_QUERY),
176        LangId::JavaScript => Some(JS_QUERY),
177        LangId::Python => Some(PY_QUERY),
178        LangId::Rust => Some(RS_QUERY),
179        LangId::Go => Some(GO_QUERY),
180        LangId::Markdown => None,
181    }
182}
183
184/// Cached parse result: mtime at parse time + the tree.
185struct CachedTree {
186    mtime: SystemTime,
187    tree: Tree,
188}
189
190/// Core parsing engine. Handles language detection, parse tree caching,
191/// and query pattern execution via tree-sitter.
192pub struct FileParser {
193    cache: HashMap<PathBuf, CachedTree>,
194}
195
196impl FileParser {
197    pub fn new() -> Self {
198        Self {
199            cache: HashMap::new(),
200        }
201    }
202
203    /// Parse a file, returning the tree and detected language. Uses cache if
204    /// the file hasn't been modified since last parse.
205    pub fn parse(&mut self, path: &Path) -> Result<(&Tree, LangId), AftError> {
206        let lang = detect_language(path).ok_or_else(|| AftError::InvalidRequest {
207            message: format!(
208                "unsupported file extension: {}",
209                path.extension()
210                    .and_then(|e| e.to_str())
211                    .unwrap_or("<none>")
212            ),
213        })?;
214
215        let canon = path.to_path_buf();
216        let current_mtime = std::fs::metadata(path)
217            .and_then(|m| m.modified())
218            .map_err(|e| AftError::FileNotFound {
219                path: format!("{}: {}", path.display(), e),
220            })?;
221
222        // Check cache validity
223        let needs_reparse = match self.cache.get(&canon) {
224            Some(cached) => cached.mtime != current_mtime,
225            None => true,
226        };
227
228        if needs_reparse {
229            let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
230                path: format!("{}: {}", path.display(), e),
231            })?;
232
233            let grammar = grammar_for(lang);
234            let mut parser = Parser::new();
235            parser.set_language(&grammar).map_err(|e| {
236                eprintln!("[aft] grammar init failed for {:?}: {}", lang, e);
237                AftError::ParseError {
238                    message: format!("grammar init failed for {:?}: {}", lang, e),
239                }
240            })?;
241
242            let tree = parser.parse(&source, None).ok_or_else(|| {
243                eprintln!("[aft] parse failed for {}", path.display());
244                AftError::ParseError {
245                    message: format!("tree-sitter parse returned None for {}", path.display()),
246                }
247            })?;
248
249            self.cache.insert(
250                canon.clone(),
251                CachedTree {
252                    mtime: current_mtime,
253                    tree,
254                },
255            );
256        }
257
258        let cached = self.cache.get(&canon).unwrap();
259        Ok((&cached.tree, lang))
260    }
261
262    /// Extract symbols from a file using language-specific query patterns.
263    pub fn extract_symbols(&mut self, path: &Path) -> Result<Vec<Symbol>, AftError> {
264        let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
265            path: format!("{}: {}", path.display(), e),
266        })?;
267
268        let (tree, lang) = self.parse(path)?;
269        let root = tree.root_node();
270
271        // Markdown uses direct tree walking, not query patterns
272        if lang == LangId::Markdown {
273            return extract_md_symbols(&source, &root);
274        }
275
276        let query_src = query_for(lang).ok_or_else(|| AftError::InvalidRequest {
277            message: format!("no query patterns implemented for {:?} yet", lang),
278        })?;
279
280        let grammar = grammar_for(lang);
281        let query = Query::new(&grammar, query_src).map_err(|e| {
282            eprintln!("[aft] query compile failed for {:?}: {}", lang, e);
283            AftError::ParseError {
284                message: format!("query compile error for {:?}: {}", lang, e),
285            }
286        })?;
287
288        match lang {
289            LangId::TypeScript | LangId::Tsx => extract_ts_symbols(&source, &root, &query),
290            LangId::JavaScript => extract_js_symbols(&source, &root, &query),
291            LangId::Python => extract_py_symbols(&source, &root, &query),
292            LangId::Rust => extract_rs_symbols(&source, &root, &query),
293            LangId::Go => extract_go_symbols(&source, &root, &query),
294            LangId::Markdown => unreachable!(),
295        }
296    }
297}
298
299/// Build a Range from a tree-sitter Node.
300pub(crate) fn node_range(node: &Node) -> Range {
301    let start = node.start_position();
302    let end = node.end_position();
303    Range {
304        start_line: start.row as u32,
305        start_col: start.column as u32,
306        end_line: end.row as u32,
307        end_col: end.column as u32,
308    }
309}
310
311/// Build a Range from a tree-sitter Node, expanding upward to include
312/// preceding attributes, decorators, and doc comments that belong to the symbol.
313///
314/// This ensures that when agents edit/replace a symbol, they get the full
315/// declaration including `#[test]`, `#[derive(...)]`, `/// doc`, `@decorator`, etc.
316pub(crate) fn node_range_with_decorators(node: &Node, source: &str, lang: LangId) -> Range {
317    let mut range = node_range(node);
318
319    let mut current = *node;
320    while let Some(prev) = current.prev_sibling() {
321        let kind = prev.kind();
322        let should_include = match lang {
323            LangId::Rust => {
324                // Include #[...] attributes
325                kind == "attribute_item"
326                    // Include /// doc comments (but not regular // comments)
327                    || (kind == "line_comment"
328                        && node_text(source, &prev).starts_with("///"))
329                    // Include /** ... */ doc comments
330                    || (kind == "block_comment"
331                        && node_text(source, &prev).starts_with("/**"))
332            }
333            LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
334                // Include @decorator
335                kind == "decorator"
336                    // Include /** JSDoc */ comments
337                    || (kind == "comment"
338                        && node_text(source, &prev).starts_with("/**"))
339            }
340            LangId::Go => {
341                // Include doc comments only if immediately above (no blank line gap)
342                kind == "comment" && is_adjacent_line(&prev, &current, source)
343            }
344            LangId::Python => {
345                // Decorators are handled by decorated_definition capture
346                false
347            }
348            LangId::Markdown => false,
349        };
350
351        if should_include {
352            range.start_line = prev.start_position().row as u32;
353            range.start_col = prev.start_position().column as u32;
354            current = prev;
355        } else {
356            break;
357        }
358    }
359
360    range
361}
362
363/// Check if two nodes are on adjacent lines (no blank line between them).
364fn is_adjacent_line(upper: &Node, lower: &Node, source: &str) -> bool {
365    let upper_end = upper.end_position().row;
366    let lower_start = lower.start_position().row;
367
368    if lower_start == 0 || lower_start <= upper_end {
369        return true;
370    }
371
372    // Check that there's no blank line between them
373    let lines: Vec<&str> = source.lines().collect();
374    for row in (upper_end + 1)..lower_start {
375        if row < lines.len() && lines[row].trim().is_empty() {
376            return false;
377        }
378    }
379    true
380}
381
382/// Extract the text of a node from source.
383pub(crate) fn node_text<'a>(source: &'a str, node: &Node) -> &'a str {
384    &source[node.byte_range()]
385}
386
387/// Collect byte ranges of all export_statement nodes from query matches.
388fn collect_export_ranges(source: &str, root: &Node, query: &Query) -> Vec<std::ops::Range<usize>> {
389    let export_idx = query
390        .capture_names()
391        .iter()
392        .position(|n| *n == "export.stmt");
393    let export_idx = match export_idx {
394        Some(i) => i as u32,
395        None => return vec![],
396    };
397
398    let mut cursor = QueryCursor::new();
399    let mut ranges = Vec::new();
400    let mut matches = cursor.matches(query, *root, source.as_bytes());
401
402    while let Some(m) = {
403        matches.advance();
404        matches.get()
405    } {
406        for cap in m.captures {
407            if cap.index == export_idx {
408                ranges.push(cap.node.byte_range());
409            }
410        }
411    }
412    ranges
413}
414
415/// Check if a node's byte range is contained within any export statement.
416fn is_exported(node: &Node, export_ranges: &[std::ops::Range<usize>]) -> bool {
417    let r = node.byte_range();
418    export_ranges
419        .iter()
420        .any(|er| er.start <= r.start && r.end <= er.end)
421}
422
423/// Extract the first line of a node as its signature.
424fn extract_signature(source: &str, node: &Node) -> String {
425    let text = node_text(source, node);
426    let first_line = text.lines().next().unwrap_or(text);
427    // Trim trailing opening brace if present
428    let trimmed = first_line.trim_end();
429    let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
430    trimmed.to_string()
431}
432
433/// Extract symbols from TypeScript / TSX source.
434fn extract_ts_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
435    let lang = LangId::TypeScript;
436    let capture_names = query.capture_names();
437
438    let export_ranges = collect_export_ranges(source, root, query);
439
440    let mut symbols = Vec::new();
441    let mut cursor = QueryCursor::new();
442    let mut matches = cursor.matches(query, *root, source.as_bytes());
443
444    while let Some(m) = {
445        matches.advance();
446        matches.get()
447    } {
448        // Determine what kind of match this is by looking at capture names
449        let mut fn_name_node = None;
450        let mut fn_def_node = None;
451        let mut arrow_name_node = None;
452        let mut arrow_def_node = None;
453        let mut class_name_node = None;
454        let mut class_def_node = None;
455        let mut method_class_name_node = None;
456        let mut method_name_node = None;
457        let mut method_def_node = None;
458        let mut interface_name_node = None;
459        let mut interface_def_node = None;
460        let mut enum_name_node = None;
461        let mut enum_def_node = None;
462        let mut type_alias_name_node = None;
463        let mut type_alias_def_node = None;
464
465        for cap in m.captures {
466            let name = capture_names[cap.index as usize];
467            match name {
468                "fn.name" => fn_name_node = Some(cap.node),
469                "fn.def" => fn_def_node = Some(cap.node),
470                "arrow.name" => arrow_name_node = Some(cap.node),
471                "arrow.def" => arrow_def_node = Some(cap.node),
472                "class.name" => class_name_node = Some(cap.node),
473                "class.def" => class_def_node = Some(cap.node),
474                "method.class_name" => method_class_name_node = Some(cap.node),
475                "method.name" => method_name_node = Some(cap.node),
476                "method.def" => method_def_node = Some(cap.node),
477                "interface.name" => interface_name_node = Some(cap.node),
478                "interface.def" => interface_def_node = Some(cap.node),
479                "enum.name" => enum_name_node = Some(cap.node),
480                "enum.def" => enum_def_node = Some(cap.node),
481                "type_alias.name" => type_alias_name_node = Some(cap.node),
482                "type_alias.def" => type_alias_def_node = Some(cap.node),
483                _ => {}
484            }
485        }
486
487        // Function declaration
488        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
489            symbols.push(Symbol {
490                name: node_text(source, &name_node).to_string(),
491                kind: SymbolKind::Function,
492                range: node_range_with_decorators(&def_node, source, lang),
493                signature: Some(extract_signature(source, &def_node)),
494                scope_chain: vec![],
495                exported: is_exported(&def_node, &export_ranges),
496                parent: None,
497            });
498        }
499
500        // Arrow function
501        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
502            symbols.push(Symbol {
503                name: node_text(source, &name_node).to_string(),
504                kind: SymbolKind::Function,
505                range: node_range_with_decorators(&def_node, source, lang),
506                signature: Some(extract_signature(source, &def_node)),
507                scope_chain: vec![],
508                exported: is_exported(&def_node, &export_ranges),
509                parent: None,
510            });
511        }
512
513        // Class declaration
514        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
515            symbols.push(Symbol {
516                name: node_text(source, &name_node).to_string(),
517                kind: SymbolKind::Class,
518                range: node_range_with_decorators(&def_node, source, lang),
519                signature: Some(extract_signature(source, &def_node)),
520                scope_chain: vec![],
521                exported: is_exported(&def_node, &export_ranges),
522                parent: None,
523            });
524        }
525
526        // Method definition
527        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
528            (method_class_name_node, method_name_node, method_def_node)
529        {
530            let class_name = node_text(source, &class_name_node).to_string();
531            symbols.push(Symbol {
532                name: node_text(source, &name_node).to_string(),
533                kind: SymbolKind::Method,
534                range: node_range_with_decorators(&def_node, source, lang),
535                signature: Some(extract_signature(source, &def_node)),
536                scope_chain: vec![class_name.clone()],
537                exported: false, // methods inherit export from class
538                parent: Some(class_name),
539            });
540        }
541
542        // Interface declaration
543        if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
544            symbols.push(Symbol {
545                name: node_text(source, &name_node).to_string(),
546                kind: SymbolKind::Interface,
547                range: node_range_with_decorators(&def_node, source, lang),
548                signature: Some(extract_signature(source, &def_node)),
549                scope_chain: vec![],
550                exported: is_exported(&def_node, &export_ranges),
551                parent: None,
552            });
553        }
554
555        // Enum declaration
556        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
557            symbols.push(Symbol {
558                name: node_text(source, &name_node).to_string(),
559                kind: SymbolKind::Enum,
560                range: node_range_with_decorators(&def_node, source, lang),
561                signature: Some(extract_signature(source, &def_node)),
562                scope_chain: vec![],
563                exported: is_exported(&def_node, &export_ranges),
564                parent: None,
565            });
566        }
567
568        // Type alias
569        if let (Some(name_node), Some(def_node)) = (type_alias_name_node, type_alias_def_node) {
570            symbols.push(Symbol {
571                name: node_text(source, &name_node).to_string(),
572                kind: SymbolKind::TypeAlias,
573                range: node_range_with_decorators(&def_node, source, lang),
574                signature: Some(extract_signature(source, &def_node)),
575                scope_chain: vec![],
576                exported: is_exported(&def_node, &export_ranges),
577                parent: None,
578            });
579        }
580    }
581
582    // Deduplicate: methods can appear as both class and method captures
583    dedup_symbols(&mut symbols);
584    Ok(symbols)
585}
586
587/// Extract symbols from JavaScript source.
588fn extract_js_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
589    let lang = LangId::JavaScript;
590    let capture_names = query.capture_names();
591
592    let export_ranges = collect_export_ranges(source, root, query);
593
594    let mut symbols = Vec::new();
595    let mut cursor = QueryCursor::new();
596    let mut matches = cursor.matches(query, *root, source.as_bytes());
597
598    while let Some(m) = {
599        matches.advance();
600        matches.get()
601    } {
602        let mut fn_name_node = None;
603        let mut fn_def_node = None;
604        let mut arrow_name_node = None;
605        let mut arrow_def_node = None;
606        let mut class_name_node = None;
607        let mut class_def_node = None;
608        let mut method_class_name_node = None;
609        let mut method_name_node = None;
610        let mut method_def_node = None;
611
612        for cap in m.captures {
613            let name = capture_names[cap.index as usize];
614            match name {
615                "fn.name" => fn_name_node = Some(cap.node),
616                "fn.def" => fn_def_node = Some(cap.node),
617                "arrow.name" => arrow_name_node = Some(cap.node),
618                "arrow.def" => arrow_def_node = Some(cap.node),
619                "class.name" => class_name_node = Some(cap.node),
620                "class.def" => class_def_node = Some(cap.node),
621                "method.class_name" => method_class_name_node = Some(cap.node),
622                "method.name" => method_name_node = Some(cap.node),
623                "method.def" => method_def_node = Some(cap.node),
624                _ => {}
625            }
626        }
627
628        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
629            symbols.push(Symbol {
630                name: node_text(source, &name_node).to_string(),
631                kind: SymbolKind::Function,
632                range: node_range_with_decorators(&def_node, source, lang),
633                signature: Some(extract_signature(source, &def_node)),
634                scope_chain: vec![],
635                exported: is_exported(&def_node, &export_ranges),
636                parent: None,
637            });
638        }
639
640        if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
641            symbols.push(Symbol {
642                name: node_text(source, &name_node).to_string(),
643                kind: SymbolKind::Function,
644                range: node_range_with_decorators(&def_node, source, lang),
645                signature: Some(extract_signature(source, &def_node)),
646                scope_chain: vec![],
647                exported: is_exported(&def_node, &export_ranges),
648                parent: None,
649            });
650        }
651
652        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
653            symbols.push(Symbol {
654                name: node_text(source, &name_node).to_string(),
655                kind: SymbolKind::Class,
656                range: node_range_with_decorators(&def_node, source, lang),
657                signature: Some(extract_signature(source, &def_node)),
658                scope_chain: vec![],
659                exported: is_exported(&def_node, &export_ranges),
660                parent: None,
661            });
662        }
663
664        if let (Some(class_name_node), Some(name_node), Some(def_node)) =
665            (method_class_name_node, method_name_node, method_def_node)
666        {
667            let class_name = node_text(source, &class_name_node).to_string();
668            symbols.push(Symbol {
669                name: node_text(source, &name_node).to_string(),
670                kind: SymbolKind::Method,
671                range: node_range_with_decorators(&def_node, source, lang),
672                signature: Some(extract_signature(source, &def_node)),
673                scope_chain: vec![class_name.clone()],
674                exported: false,
675                parent: Some(class_name),
676            });
677        }
678    }
679
680    dedup_symbols(&mut symbols);
681    Ok(symbols)
682}
683
684/// Walk parent nodes to build a scope chain for Python symbols.
685/// A function inside `class_definition > block` gets the class name in its scope.
686fn py_scope_chain(node: &Node, source: &str) -> Vec<String> {
687    let mut chain = Vec::new();
688    let mut current = node.parent();
689    while let Some(parent) = current {
690        if parent.kind() == "class_definition" {
691            if let Some(name_node) = parent.child_by_field_name("name") {
692                chain.push(node_text(source, &name_node).to_string());
693            }
694        }
695        current = parent.parent();
696    }
697    chain.reverse();
698    chain
699}
700
701/// Extract symbols from Python source.
702fn extract_py_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
703    let lang = LangId::Python;
704    let capture_names = query.capture_names();
705
706    let mut symbols = Vec::new();
707    let mut cursor = QueryCursor::new();
708    let mut matches = cursor.matches(query, *root, source.as_bytes());
709
710    // Track decorated definitions to avoid double-counting
711    let mut decorated_fn_lines = std::collections::HashSet::new();
712
713    // First pass: collect decorated definition info
714    {
715        let mut cursor2 = QueryCursor::new();
716        let mut matches2 = cursor2.matches(query, *root, source.as_bytes());
717        while let Some(m) = {
718            matches2.advance();
719            matches2.get()
720        } {
721            let mut dec_def_node = None;
722            let mut dec_decorator_node = None;
723
724            for cap in m.captures {
725                let name = capture_names[cap.index as usize];
726                match name {
727                    "dec.def" => dec_def_node = Some(cap.node),
728                    "dec.decorator" => dec_decorator_node = Some(cap.node),
729                    _ => {}
730                }
731            }
732
733            if let (Some(def_node), Some(_dec_node)) = (dec_def_node, dec_decorator_node) {
734                // Find the inner function_definition or class_definition
735                let mut child_cursor = def_node.walk();
736                if child_cursor.goto_first_child() {
737                    loop {
738                        let child = child_cursor.node();
739                        if child.kind() == "function_definition"
740                            || child.kind() == "class_definition"
741                        {
742                            decorated_fn_lines.insert(child.start_position().row);
743                        }
744                        if !child_cursor.goto_next_sibling() {
745                            break;
746                        }
747                    }
748                }
749            }
750        }
751    }
752
753    while let Some(m) = {
754        matches.advance();
755        matches.get()
756    } {
757        let mut fn_name_node = None;
758        let mut fn_def_node = None;
759        let mut class_name_node = None;
760        let mut class_def_node = None;
761
762        for cap in m.captures {
763            let name = capture_names[cap.index as usize];
764            match name {
765                "fn.name" => fn_name_node = Some(cap.node),
766                "fn.def" => fn_def_node = Some(cap.node),
767                "class.name" => class_name_node = Some(cap.node),
768                "class.def" => class_def_node = Some(cap.node),
769                _ => {}
770            }
771        }
772
773        // Function definition
774        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
775            let scope = py_scope_chain(&def_node, source);
776            let is_method = !scope.is_empty();
777            let name = node_text(source, &name_node).to_string();
778            // Skip __init__ and other dunders as separate symbols — they're methods
779            let kind = if is_method {
780                SymbolKind::Method
781            } else {
782                SymbolKind::Function
783            };
784
785            // Build signature — include decorator if this is a decorated function
786            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
787                // Find the decorated_definition parent to get decorator text
788                let mut sig_parts = Vec::new();
789                let mut parent = def_node.parent();
790                while let Some(p) = parent {
791                    if p.kind() == "decorated_definition" {
792                        // Get decorator lines
793                        let mut dc = p.walk();
794                        if dc.goto_first_child() {
795                            loop {
796                                if dc.node().kind() == "decorator" {
797                                    sig_parts.push(node_text(source, &dc.node()).to_string());
798                                }
799                                if !dc.goto_next_sibling() {
800                                    break;
801                                }
802                            }
803                        }
804                        break;
805                    }
806                    parent = p.parent();
807                }
808                sig_parts.push(extract_signature(source, &def_node));
809                Some(sig_parts.join("\n"))
810            } else {
811                Some(extract_signature(source, &def_node))
812            };
813
814            symbols.push(Symbol {
815                name,
816                kind,
817                range: node_range_with_decorators(&def_node, source, lang),
818                signature: sig,
819                scope_chain: scope.clone(),
820                exported: false, // Python has no export concept
821                parent: scope.last().cloned(),
822            });
823        }
824
825        // Class definition
826        if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
827            let scope = py_scope_chain(&def_node, source);
828
829            // Build signature — include decorator if decorated
830            let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
831                let mut sig_parts = Vec::new();
832                let mut parent = def_node.parent();
833                while let Some(p) = parent {
834                    if p.kind() == "decorated_definition" {
835                        let mut dc = p.walk();
836                        if dc.goto_first_child() {
837                            loop {
838                                if dc.node().kind() == "decorator" {
839                                    sig_parts.push(node_text(source, &dc.node()).to_string());
840                                }
841                                if !dc.goto_next_sibling() {
842                                    break;
843                                }
844                            }
845                        }
846                        break;
847                    }
848                    parent = p.parent();
849                }
850                sig_parts.push(extract_signature(source, &def_node));
851                Some(sig_parts.join("\n"))
852            } else {
853                Some(extract_signature(source, &def_node))
854            };
855
856            symbols.push(Symbol {
857                name: node_text(source, &name_node).to_string(),
858                kind: SymbolKind::Class,
859                range: node_range_with_decorators(&def_node, source, lang),
860                signature: sig,
861                scope_chain: scope.clone(),
862                exported: false,
863                parent: scope.last().cloned(),
864            });
865        }
866    }
867
868    dedup_symbols(&mut symbols);
869    Ok(symbols)
870}
871
872/// Extract symbols from Rust source.
873/// Handles: free functions, struct, enum, trait (as Interface), impl methods with scope chains.
874fn extract_rs_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
875    let lang = LangId::Rust;
876    let capture_names = query.capture_names();
877
878    // Collect all visibility_modifier byte ranges first
879    let mut vis_ranges: Vec<std::ops::Range<usize>> = Vec::new();
880    {
881        let vis_idx = capture_names.iter().position(|n| *n == "vis.mod");
882        if let Some(idx) = vis_idx {
883            let idx = idx as u32;
884            let mut cursor = QueryCursor::new();
885            let mut matches = cursor.matches(query, *root, source.as_bytes());
886            while let Some(m) = {
887                matches.advance();
888                matches.get()
889            } {
890                for cap in m.captures {
891                    if cap.index == idx {
892                        vis_ranges.push(cap.node.byte_range());
893                    }
894                }
895            }
896        }
897    }
898
899    let is_pub = |node: &Node| -> bool {
900        // Check if the node has a visibility_modifier as a direct child
901        let mut child_cursor = node.walk();
902        if child_cursor.goto_first_child() {
903            loop {
904                if child_cursor.node().kind() == "visibility_modifier" {
905                    return true;
906                }
907                if !child_cursor.goto_next_sibling() {
908                    break;
909                }
910            }
911        }
912        false
913    };
914
915    let mut symbols = Vec::new();
916    let mut cursor = QueryCursor::new();
917    let mut matches = cursor.matches(query, *root, source.as_bytes());
918
919    while let Some(m) = {
920        matches.advance();
921        matches.get()
922    } {
923        let mut fn_name_node = None;
924        let mut fn_def_node = None;
925        let mut struct_name_node = None;
926        let mut struct_def_node = None;
927        let mut enum_name_node = None;
928        let mut enum_def_node = None;
929        let mut trait_name_node = None;
930        let mut trait_def_node = None;
931        let mut impl_def_node = None;
932
933        for cap in m.captures {
934            let name = capture_names[cap.index as usize];
935            match name {
936                "fn.name" => fn_name_node = Some(cap.node),
937                "fn.def" => fn_def_node = Some(cap.node),
938                "struct.name" => struct_name_node = Some(cap.node),
939                "struct.def" => struct_def_node = Some(cap.node),
940                "enum.name" => enum_name_node = Some(cap.node),
941                "enum.def" => enum_def_node = Some(cap.node),
942                "trait.name" => trait_name_node = Some(cap.node),
943                "trait.def" => trait_def_node = Some(cap.node),
944                "impl.def" => impl_def_node = Some(cap.node),
945                _ => {}
946            }
947        }
948
949        // Free function (not inside impl block — check parent)
950        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
951            let parent = def_node.parent();
952            let in_impl = parent
953                .map(|p| p.kind() == "declaration_list")
954                .unwrap_or(false);
955            if !in_impl {
956                symbols.push(Symbol {
957                    name: node_text(source, &name_node).to_string(),
958                    kind: SymbolKind::Function,
959                    range: node_range_with_decorators(&def_node, source, lang),
960                    signature: Some(extract_signature(source, &def_node)),
961                    scope_chain: vec![],
962                    exported: is_pub(&def_node),
963                    parent: None,
964                });
965            }
966        }
967
968        // Struct
969        if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
970            symbols.push(Symbol {
971                name: node_text(source, &name_node).to_string(),
972                kind: SymbolKind::Struct,
973                range: node_range_with_decorators(&def_node, source, lang),
974                signature: Some(extract_signature(source, &def_node)),
975                scope_chain: vec![],
976                exported: is_pub(&def_node),
977                parent: None,
978            });
979        }
980
981        // Enum
982        if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
983            symbols.push(Symbol {
984                name: node_text(source, &name_node).to_string(),
985                kind: SymbolKind::Enum,
986                range: node_range_with_decorators(&def_node, source, lang),
987                signature: Some(extract_signature(source, &def_node)),
988                scope_chain: vec![],
989                exported: is_pub(&def_node),
990                parent: None,
991            });
992        }
993
994        // Trait (mapped to Interface kind)
995        if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
996            symbols.push(Symbol {
997                name: node_text(source, &name_node).to_string(),
998                kind: SymbolKind::Interface,
999                range: node_range_with_decorators(&def_node, source, lang),
1000                signature: Some(extract_signature(source, &def_node)),
1001                scope_chain: vec![],
1002                exported: is_pub(&def_node),
1003                parent: None,
1004            });
1005        }
1006
1007        // Impl block — extract methods from inside
1008        if let Some(impl_node) = impl_def_node {
1009            // Find the type name(s) from the impl
1010            // `impl TypeName { ... }` → scope = ["TypeName"]
1011            // `impl Trait for TypeName { ... }` → scope = ["Trait for TypeName"]
1012            let mut type_names: Vec<String> = Vec::new();
1013            let mut child_cursor = impl_node.walk();
1014            if child_cursor.goto_first_child() {
1015                loop {
1016                    let child = child_cursor.node();
1017                    if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1018                        type_names.push(node_text(source, &child).to_string());
1019                    }
1020                    if !child_cursor.goto_next_sibling() {
1021                        break;
1022                    }
1023                }
1024            }
1025
1026            let scope_name = if type_names.len() >= 2 {
1027                // impl Trait for Type
1028                format!("{} for {}", type_names[0], type_names[1])
1029            } else if type_names.len() == 1 {
1030                type_names[0].clone()
1031            } else {
1032                String::new()
1033            };
1034
1035            let parent_name = type_names.last().cloned().unwrap_or_default();
1036
1037            // Find declaration_list and extract function_items
1038            let mut child_cursor = impl_node.walk();
1039            if child_cursor.goto_first_child() {
1040                loop {
1041                    let child = child_cursor.node();
1042                    if child.kind() == "declaration_list" {
1043                        let mut fn_cursor = child.walk();
1044                        if fn_cursor.goto_first_child() {
1045                            loop {
1046                                let fn_node = fn_cursor.node();
1047                                if fn_node.kind() == "function_item" {
1048                                    if let Some(name_node) = fn_node.child_by_field_name("name") {
1049                                        symbols.push(Symbol {
1050                                            name: node_text(source, &name_node).to_string(),
1051                                            kind: SymbolKind::Method,
1052                                            range: node_range_with_decorators(
1053                                                &fn_node, source, lang,
1054                                            ),
1055                                            signature: Some(extract_signature(source, &fn_node)),
1056                                            scope_chain: if scope_name.is_empty() {
1057                                                vec![]
1058                                            } else {
1059                                                vec![scope_name.clone()]
1060                                            },
1061                                            exported: is_pub(&fn_node),
1062                                            parent: if parent_name.is_empty() {
1063                                                None
1064                                            } else {
1065                                                Some(parent_name.clone())
1066                                            },
1067                                        });
1068                                    }
1069                                }
1070                                if !fn_cursor.goto_next_sibling() {
1071                                    break;
1072                                }
1073                            }
1074                        }
1075                    }
1076                    if !child_cursor.goto_next_sibling() {
1077                        break;
1078                    }
1079                }
1080            }
1081        }
1082    }
1083
1084    dedup_symbols(&mut symbols);
1085    Ok(symbols)
1086}
1087
1088/// Extract symbols from Go source.
1089/// Handles: functions, methods (with receiver scope chain), struct/interface types,
1090/// uppercase-first-letter export detection.
1091fn extract_go_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1092    let lang = LangId::Go;
1093    let capture_names = query.capture_names();
1094
1095    let is_go_exported = |name: &str| -> bool {
1096        name.chars()
1097            .next()
1098            .map(|c| c.is_uppercase())
1099            .unwrap_or(false)
1100    };
1101
1102    let mut symbols = Vec::new();
1103    let mut cursor = QueryCursor::new();
1104    let mut matches = cursor.matches(query, *root, source.as_bytes());
1105
1106    while let Some(m) = {
1107        matches.advance();
1108        matches.get()
1109    } {
1110        let mut fn_name_node = None;
1111        let mut fn_def_node = None;
1112        let mut method_name_node = None;
1113        let mut method_def_node = None;
1114        let mut type_name_node = None;
1115        let mut type_body_node = None;
1116        let mut type_def_node = None;
1117
1118        for cap in m.captures {
1119            let name = capture_names[cap.index as usize];
1120            match name {
1121                "fn.name" => fn_name_node = Some(cap.node),
1122                "fn.def" => fn_def_node = Some(cap.node),
1123                "method.name" => method_name_node = Some(cap.node),
1124                "method.def" => method_def_node = Some(cap.node),
1125                "type.name" => type_name_node = Some(cap.node),
1126                "type.body" => type_body_node = Some(cap.node),
1127                "type.def" => type_def_node = Some(cap.node),
1128                _ => {}
1129            }
1130        }
1131
1132        // Function declaration
1133        if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1134            let name = node_text(source, &name_node).to_string();
1135            symbols.push(Symbol {
1136                exported: is_go_exported(&name),
1137                name,
1138                kind: SymbolKind::Function,
1139                range: node_range_with_decorators(&def_node, source, lang),
1140                signature: Some(extract_signature(source, &def_node)),
1141                scope_chain: vec![],
1142                parent: None,
1143            });
1144        }
1145
1146        // Method declaration (with receiver)
1147        if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
1148            let name = node_text(source, &name_node).to_string();
1149
1150            // Extract receiver type from the first parameter_list
1151            let receiver_type = extract_go_receiver_type(&def_node, source);
1152            let scope_chain = if let Some(ref rt) = receiver_type {
1153                vec![rt.clone()]
1154            } else {
1155                vec![]
1156            };
1157
1158            symbols.push(Symbol {
1159                exported: is_go_exported(&name),
1160                name,
1161                kind: SymbolKind::Method,
1162                range: node_range_with_decorators(&def_node, source, lang),
1163                signature: Some(extract_signature(source, &def_node)),
1164                scope_chain,
1165                parent: receiver_type,
1166            });
1167        }
1168
1169        // Type declarations (struct or interface)
1170        if let (Some(name_node), Some(body_node), Some(def_node)) =
1171            (type_name_node, type_body_node, type_def_node)
1172        {
1173            let name = node_text(source, &name_node).to_string();
1174            let kind = match body_node.kind() {
1175                "struct_type" => SymbolKind::Struct,
1176                "interface_type" => SymbolKind::Interface,
1177                _ => SymbolKind::TypeAlias,
1178            };
1179
1180            symbols.push(Symbol {
1181                exported: is_go_exported(&name),
1182                name,
1183                kind,
1184                range: node_range_with_decorators(&def_node, source, lang),
1185                signature: Some(extract_signature(source, &def_node)),
1186                scope_chain: vec![],
1187                parent: None,
1188            });
1189        }
1190    }
1191
1192    dedup_symbols(&mut symbols);
1193    Ok(symbols)
1194}
1195
1196/// Extract the receiver type from a Go method_declaration node.
1197/// e.g. `func (m *MyStruct) String()` → Some("MyStruct")
1198fn extract_go_receiver_type(method_node: &Node, source: &str) -> Option<String> {
1199    // The first parameter_list is the receiver
1200    let mut child_cursor = method_node.walk();
1201    if child_cursor.goto_first_child() {
1202        loop {
1203            let child = child_cursor.node();
1204            if child.kind() == "parameter_list" {
1205                // Walk into parameter_list to find type_identifier
1206                return find_type_identifier_recursive(&child, source);
1207            }
1208            if !child_cursor.goto_next_sibling() {
1209                break;
1210            }
1211        }
1212    }
1213    None
1214}
1215
1216/// Recursively find the first type_identifier node in a subtree.
1217fn find_type_identifier_recursive(node: &Node, source: &str) -> Option<String> {
1218    if node.kind() == "type_identifier" {
1219        return Some(node_text(source, node).to_string());
1220    }
1221    let mut cursor = node.walk();
1222    if cursor.goto_first_child() {
1223        loop {
1224            if let Some(result) = find_type_identifier_recursive(&cursor.node(), source) {
1225                return Some(result);
1226            }
1227            if !cursor.goto_next_sibling() {
1228                break;
1229            }
1230        }
1231    }
1232    None
1233}
1234
1235/// Extract markdown headings as symbols.
1236/// Each heading becomes a symbol with kind `Heading`, and its range covers the entire
1237/// section (from the heading to the next heading at the same or higher level, or EOF).
1238fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
1239    let mut symbols = Vec::new();
1240    extract_md_sections(source, root, &mut symbols, &[]);
1241    Ok(symbols)
1242}
1243
1244/// Recursively walk `section` nodes to build the heading hierarchy.
1245fn extract_md_sections(
1246    source: &str,
1247    node: &Node,
1248    symbols: &mut Vec<Symbol>,
1249    scope_chain: &[String],
1250) {
1251    let mut cursor = node.walk();
1252    if !cursor.goto_first_child() {
1253        return;
1254    }
1255
1256    loop {
1257        let child = cursor.node();
1258        match child.kind() {
1259            "section" => {
1260                // A section contains an atx_heading as its first child,
1261                // followed by content and possibly nested sections.
1262                let mut section_cursor = child.walk();
1263                let mut heading_name = String::new();
1264                let mut heading_level: u8 = 0;
1265
1266                if section_cursor.goto_first_child() {
1267                    loop {
1268                        let section_child = section_cursor.node();
1269                        if section_child.kind() == "atx_heading" {
1270                            // Extract heading level from marker type
1271                            let mut h_cursor = section_child.walk();
1272                            if h_cursor.goto_first_child() {
1273                                loop {
1274                                    let h_child = h_cursor.node();
1275                                    let kind = h_child.kind();
1276                                    if kind.starts_with("atx_h") && kind.ends_with("_marker") {
1277                                        // "atx_h1_marker" → level 1, "atx_h2_marker" → level 2, etc.
1278                                        heading_level = kind
1279                                            .strip_prefix("atx_h")
1280                                            .and_then(|s| s.strip_suffix("_marker"))
1281                                            .and_then(|s| s.parse::<u8>().ok())
1282                                            .unwrap_or(1);
1283                                    } else if h_child.kind() == "inline" {
1284                                        heading_name =
1285                                            node_text(source, &h_child).trim().to_string();
1286                                    }
1287                                    if !h_cursor.goto_next_sibling() {
1288                                        break;
1289                                    }
1290                                }
1291                            }
1292                        }
1293                        if !section_cursor.goto_next_sibling() {
1294                            break;
1295                        }
1296                    }
1297                }
1298
1299                if !heading_name.is_empty() {
1300                    let range = node_range(&child);
1301                    let signature =
1302                        format!("{} {}", "#".repeat(heading_level as usize), heading_name);
1303
1304                    symbols.push(Symbol {
1305                        name: heading_name.clone(),
1306                        kind: SymbolKind::Heading,
1307                        range,
1308                        signature: Some(signature),
1309                        scope_chain: scope_chain.to_vec(),
1310                        exported: false,
1311                        parent: scope_chain.last().cloned(),
1312                    });
1313
1314                    // Recurse into the section for nested headings
1315                    let mut new_scope = scope_chain.to_vec();
1316                    new_scope.push(heading_name);
1317                    extract_md_sections(source, &child, symbols, &new_scope);
1318                }
1319            }
1320            _ => {}
1321        }
1322
1323        if !cursor.goto_next_sibling() {
1324            break;
1325        }
1326    }
1327}
1328
1329/// Remove duplicate symbols based on (name, kind, start_line).
1330/// Class declarations can match both "class" and "method" patterns,
1331/// producing duplicates.
1332fn dedup_symbols(symbols: &mut Vec<Symbol>) {
1333    let mut seen = std::collections::HashSet::new();
1334    symbols.retain(|s| {
1335        let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
1336        seen.insert(key)
1337    });
1338}
1339
1340/// Provider that uses tree-sitter for real symbol extraction.
1341/// Implements the `LanguageProvider` trait from `language.rs`.
1342pub struct TreeSitterProvider {
1343    parser: RefCell<FileParser>,
1344}
1345
1346impl TreeSitterProvider {
1347    pub fn new() -> Self {
1348        Self {
1349            parser: RefCell::new(FileParser::new()),
1350        }
1351    }
1352}
1353
1354impl crate::language::LanguageProvider for TreeSitterProvider {
1355    fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
1356        let symbols = self.parser.borrow_mut().extract_symbols(file)?;
1357
1358        let matches: Vec<SymbolMatch> = symbols
1359            .into_iter()
1360            .filter(|s| s.name == name)
1361            .map(|s| SymbolMatch {
1362                file: file.display().to_string(),
1363                symbol: s,
1364            })
1365            .collect();
1366
1367        if matches.is_empty() {
1368            Err(AftError::SymbolNotFound {
1369                name: name.to_string(),
1370                file: file.display().to_string(),
1371            })
1372        } else {
1373            Ok(matches)
1374        }
1375    }
1376
1377    fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
1378        self.parser.borrow_mut().extract_symbols(file)
1379    }
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use crate::language::LanguageProvider;
1386    use std::path::PathBuf;
1387
1388    fn fixture_path(name: &str) -> PathBuf {
1389        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1390            .join("tests")
1391            .join("fixtures")
1392            .join(name)
1393    }
1394
1395    // --- Language detection ---
1396
1397    #[test]
1398    fn detect_ts() {
1399        assert_eq!(
1400            detect_language(Path::new("foo.ts")),
1401            Some(LangId::TypeScript)
1402        );
1403    }
1404
1405    #[test]
1406    fn detect_tsx() {
1407        assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
1408    }
1409
1410    #[test]
1411    fn detect_js() {
1412        assert_eq!(
1413            detect_language(Path::new("foo.js")),
1414            Some(LangId::JavaScript)
1415        );
1416    }
1417
1418    #[test]
1419    fn detect_jsx() {
1420        assert_eq!(
1421            detect_language(Path::new("foo.jsx")),
1422            Some(LangId::JavaScript)
1423        );
1424    }
1425
1426    #[test]
1427    fn detect_py() {
1428        assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
1429    }
1430
1431    #[test]
1432    fn detect_rs() {
1433        assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
1434    }
1435
1436    #[test]
1437    fn detect_go() {
1438        assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
1439    }
1440
1441    #[test]
1442    fn detect_unknown_returns_none() {
1443        assert_eq!(detect_language(Path::new("foo.txt")), None);
1444    }
1445
1446    // --- Unsupported extension error ---
1447
1448    #[test]
1449    fn unsupported_extension_returns_invalid_request() {
1450        // Use a file that exists but has an unsupported extension
1451        let path = fixture_path("sample.ts");
1452        let bad_path = path.with_extension("txt");
1453        // Create a dummy file so the error comes from language detection, not I/O
1454        std::fs::write(&bad_path, "hello").unwrap();
1455        let provider = TreeSitterProvider::new();
1456        let result = provider.list_symbols(&bad_path);
1457        std::fs::remove_file(&bad_path).ok();
1458        match result {
1459            Err(AftError::InvalidRequest { message }) => {
1460                assert!(
1461                    message.contains("unsupported file extension"),
1462                    "msg: {}",
1463                    message
1464                );
1465                assert!(message.contains("txt"), "msg: {}", message);
1466            }
1467            other => panic!("expected InvalidRequest, got {:?}", other),
1468        }
1469    }
1470
1471    // --- TypeScript extraction ---
1472
1473    #[test]
1474    fn ts_extracts_all_symbol_kinds() {
1475        let provider = TreeSitterProvider::new();
1476        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1477
1478        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1479        assert!(
1480            names.contains(&"greet"),
1481            "missing function greet: {:?}",
1482            names
1483        );
1484        assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
1485        assert!(
1486            names.contains(&"UserService"),
1487            "missing class UserService: {:?}",
1488            names
1489        );
1490        assert!(
1491            names.contains(&"Config"),
1492            "missing interface Config: {:?}",
1493            names
1494        );
1495        assert!(
1496            names.contains(&"Status"),
1497            "missing enum Status: {:?}",
1498            names
1499        );
1500        assert!(
1501            names.contains(&"UserId"),
1502            "missing type alias UserId: {:?}",
1503            names
1504        );
1505        assert!(
1506            names.contains(&"internalHelper"),
1507            "missing non-exported fn: {:?}",
1508            names
1509        );
1510
1511        // At least 6 unique symbols as required
1512        assert!(
1513            symbols.len() >= 6,
1514            "expected ≥6 symbols, got {}: {:?}",
1515            symbols.len(),
1516            names
1517        );
1518    }
1519
1520    #[test]
1521    fn ts_symbol_kinds_correct() {
1522        let provider = TreeSitterProvider::new();
1523        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1524
1525        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1526
1527        assert_eq!(find("greet").kind, SymbolKind::Function);
1528        assert_eq!(find("add").kind, SymbolKind::Function); // arrow fn → Function
1529        assert_eq!(find("UserService").kind, SymbolKind::Class);
1530        assert_eq!(find("Config").kind, SymbolKind::Interface);
1531        assert_eq!(find("Status").kind, SymbolKind::Enum);
1532        assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
1533    }
1534
1535    #[test]
1536    fn ts_export_detection() {
1537        let provider = TreeSitterProvider::new();
1538        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1539
1540        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1541
1542        assert!(find("greet").exported, "greet should be exported");
1543        assert!(find("add").exported, "add should be exported");
1544        assert!(
1545            find("UserService").exported,
1546            "UserService should be exported"
1547        );
1548        assert!(find("Config").exported, "Config should be exported");
1549        assert!(find("Status").exported, "Status should be exported");
1550        assert!(
1551            !find("internalHelper").exported,
1552            "internalHelper should not be exported"
1553        );
1554    }
1555
1556    #[test]
1557    fn ts_method_scope_chain() {
1558        let provider = TreeSitterProvider::new();
1559        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1560
1561        let methods: Vec<&Symbol> = symbols
1562            .iter()
1563            .filter(|s| s.kind == SymbolKind::Method)
1564            .collect();
1565        assert!(!methods.is_empty(), "should have at least one method");
1566
1567        for method in &methods {
1568            assert_eq!(
1569                method.scope_chain,
1570                vec!["UserService"],
1571                "method {} should have UserService in scope chain",
1572                method.name
1573            );
1574            assert_eq!(method.parent.as_deref(), Some("UserService"));
1575        }
1576    }
1577
1578    #[test]
1579    fn ts_signatures_present() {
1580        let provider = TreeSitterProvider::new();
1581        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1582
1583        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1584
1585        let greet_sig = find("greet").signature.as_ref().unwrap();
1586        assert!(
1587            greet_sig.contains("greet"),
1588            "signature should contain function name: {}",
1589            greet_sig
1590        );
1591    }
1592
1593    #[test]
1594    fn ts_ranges_valid() {
1595        let provider = TreeSitterProvider::new();
1596        let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1597
1598        for s in &symbols {
1599            assert!(
1600                s.range.end_line >= s.range.start_line,
1601                "symbol {} has invalid range: {:?}",
1602                s.name,
1603                s.range
1604            );
1605        }
1606    }
1607
1608    // --- JavaScript extraction ---
1609
1610    #[test]
1611    fn js_extracts_core_symbols() {
1612        let provider = TreeSitterProvider::new();
1613        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
1614
1615        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1616        assert!(
1617            names.contains(&"multiply"),
1618            "missing function multiply: {:?}",
1619            names
1620        );
1621        assert!(
1622            names.contains(&"divide"),
1623            "missing arrow fn divide: {:?}",
1624            names
1625        );
1626        assert!(
1627            names.contains(&"EventEmitter"),
1628            "missing class EventEmitter: {:?}",
1629            names
1630        );
1631        assert!(
1632            names.contains(&"main"),
1633            "missing default export fn main: {:?}",
1634            names
1635        );
1636
1637        assert!(
1638            symbols.len() >= 4,
1639            "expected ≥4 symbols, got {}: {:?}",
1640            symbols.len(),
1641            names
1642        );
1643    }
1644
1645    #[test]
1646    fn js_arrow_fn_correctly_named() {
1647        let provider = TreeSitterProvider::new();
1648        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
1649
1650        let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
1651        assert_eq!(divide.kind, SymbolKind::Function);
1652        assert!(divide.exported, "divide should be exported");
1653
1654        let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
1655        assert_eq!(internal.kind, SymbolKind::Function);
1656        assert!(!internal.exported, "internalUtil should not be exported");
1657    }
1658
1659    #[test]
1660    fn js_method_scope_chain() {
1661        let provider = TreeSitterProvider::new();
1662        let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
1663
1664        let methods: Vec<&Symbol> = symbols
1665            .iter()
1666            .filter(|s| s.kind == SymbolKind::Method)
1667            .collect();
1668
1669        for method in &methods {
1670            assert_eq!(
1671                method.scope_chain,
1672                vec!["EventEmitter"],
1673                "method {} should have EventEmitter in scope chain",
1674                method.name
1675            );
1676        }
1677    }
1678
1679    // --- TSX extraction ---
1680
1681    #[test]
1682    fn tsx_extracts_react_component() {
1683        let provider = TreeSitterProvider::new();
1684        let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
1685
1686        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1687        assert!(
1688            names.contains(&"Button"),
1689            "missing React component Button: {:?}",
1690            names
1691        );
1692        assert!(
1693            names.contains(&"Counter"),
1694            "missing class Counter: {:?}",
1695            names
1696        );
1697        assert!(
1698            names.contains(&"formatLabel"),
1699            "missing function formatLabel: {:?}",
1700            names
1701        );
1702
1703        assert!(
1704            symbols.len() >= 2,
1705            "expected ≥2 symbols, got {}: {:?}",
1706            symbols.len(),
1707            names
1708        );
1709    }
1710
1711    #[test]
1712    fn tsx_jsx_doesnt_break_parser() {
1713        // Main assertion: TSX grammar handles JSX without errors
1714        let provider = TreeSitterProvider::new();
1715        let result = provider.list_symbols(&fixture_path("sample.tsx"));
1716        assert!(
1717            result.is_ok(),
1718            "TSX parsing should succeed: {:?}",
1719            result.err()
1720        );
1721    }
1722
1723    // --- resolve_symbol ---
1724
1725    #[test]
1726    fn resolve_symbol_finds_match() {
1727        let provider = TreeSitterProvider::new();
1728        let matches = provider
1729            .resolve_symbol(&fixture_path("sample.ts"), "greet")
1730            .unwrap();
1731        assert_eq!(matches.len(), 1);
1732        assert_eq!(matches[0].symbol.name, "greet");
1733        assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
1734    }
1735
1736    #[test]
1737    fn resolve_symbol_not_found() {
1738        let provider = TreeSitterProvider::new();
1739        let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
1740        assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
1741    }
1742
1743    // --- Parse tree caching ---
1744
1745    #[test]
1746    fn symbol_range_includes_rust_attributes() {
1747        let dir = tempfile::tempdir().unwrap();
1748        let path = dir.path().join("test_attrs.rs");
1749        std::fs::write(
1750            &path,
1751            "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n    assert!(true);\n}\n",
1752        )
1753        .unwrap();
1754
1755        let provider = TreeSitterProvider::new();
1756        let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
1757        assert_eq!(matches.len(), 1);
1758        assert_eq!(
1759            matches[0].symbol.range.start_line, 0,
1760            "symbol range should include preceding /// doc comment, got start_line={}",
1761            matches[0].symbol.range.start_line
1762        );
1763    }
1764
1765    #[test]
1766    fn symbol_range_includes_go_doc_comment() {
1767        let dir = tempfile::tempdir().unwrap();
1768        let path = dir.path().join("test_doc.go");
1769        std::fs::write(
1770            &path,
1771            "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
1772        )
1773        .unwrap();
1774
1775        let provider = TreeSitterProvider::new();
1776        let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
1777        assert_eq!(matches.len(), 1);
1778        assert_eq!(
1779            matches[0].symbol.range.start_line, 2,
1780            "symbol range should include preceding doc comments, got start_line={}",
1781            matches[0].symbol.range.start_line
1782        );
1783    }
1784
1785    #[test]
1786    fn symbol_range_skips_unrelated_comments() {
1787        let dir = tempfile::tempdir().unwrap();
1788        let path = dir.path().join("test_gap.go");
1789        std::fs::write(
1790            &path,
1791            "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
1792        )
1793        .unwrap();
1794
1795        let provider = TreeSitterProvider::new();
1796        let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
1797        assert_eq!(matches.len(), 1);
1798        assert_eq!(
1799            matches[0].symbol.range.start_line, 4,
1800            "symbol range should NOT include comment separated by blank line, got start_line={}",
1801            matches[0].symbol.range.start_line
1802        );
1803    }
1804
1805    #[test]
1806    fn parse_cache_returns_same_tree() {
1807        let mut parser = FileParser::new();
1808        let path = fixture_path("sample.ts");
1809
1810        let (tree1, _) = parser.parse(&path).unwrap();
1811        let tree1_root = tree1.root_node().byte_range();
1812
1813        let (tree2, _) = parser.parse(&path).unwrap();
1814        let tree2_root = tree2.root_node().byte_range();
1815
1816        // Same tree (cache hit) should return identical root node range
1817        assert_eq!(tree1_root, tree2_root);
1818    }
1819
1820    // --- Python extraction ---
1821
1822    #[test]
1823    fn py_extracts_all_symbols() {
1824        let provider = TreeSitterProvider::new();
1825        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
1826
1827        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1828        assert!(
1829            names.contains(&"top_level_function"),
1830            "missing top_level_function: {:?}",
1831            names
1832        );
1833        assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
1834        assert!(
1835            names.contains(&"instance_method"),
1836            "missing method instance_method: {:?}",
1837            names
1838        );
1839        assert!(
1840            names.contains(&"decorated_function"),
1841            "missing decorated_function: {:?}",
1842            names
1843        );
1844
1845        // Plan requires ≥4 symbols
1846        assert!(
1847            symbols.len() >= 4,
1848            "expected ≥4 symbols, got {}: {:?}",
1849            symbols.len(),
1850            names
1851        );
1852    }
1853
1854    #[test]
1855    fn py_symbol_kinds_correct() {
1856        let provider = TreeSitterProvider::new();
1857        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
1858
1859        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1860
1861        assert_eq!(find("top_level_function").kind, SymbolKind::Function);
1862        assert_eq!(find("MyClass").kind, SymbolKind::Class);
1863        assert_eq!(find("instance_method").kind, SymbolKind::Method);
1864        assert_eq!(find("decorated_function").kind, SymbolKind::Function);
1865        assert_eq!(find("OuterClass").kind, SymbolKind::Class);
1866        assert_eq!(find("InnerClass").kind, SymbolKind::Class);
1867        assert_eq!(find("inner_method").kind, SymbolKind::Method);
1868        assert_eq!(find("outer_method").kind, SymbolKind::Method);
1869    }
1870
1871    #[test]
1872    fn py_method_scope_chain() {
1873        let provider = TreeSitterProvider::new();
1874        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
1875
1876        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1877
1878        // Method inside MyClass
1879        assert_eq!(
1880            find("instance_method").scope_chain,
1881            vec!["MyClass"],
1882            "instance_method should have MyClass in scope chain"
1883        );
1884        assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
1885
1886        // Method inside OuterClass > InnerClass
1887        assert_eq!(
1888            find("inner_method").scope_chain,
1889            vec!["OuterClass", "InnerClass"],
1890            "inner_method should have nested scope chain"
1891        );
1892
1893        // InnerClass itself should have OuterClass in scope
1894        assert_eq!(
1895            find("InnerClass").scope_chain,
1896            vec!["OuterClass"],
1897            "InnerClass should have OuterClass in scope"
1898        );
1899
1900        // Top-level function has empty scope
1901        assert!(
1902            find("top_level_function").scope_chain.is_empty(),
1903            "top-level function should have empty scope chain"
1904        );
1905    }
1906
1907    #[test]
1908    fn py_decorated_function_signature() {
1909        let provider = TreeSitterProvider::new();
1910        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
1911
1912        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1913
1914        let sig = find("decorated_function").signature.as_ref().unwrap();
1915        assert!(
1916            sig.contains("@staticmethod"),
1917            "decorated function signature should include decorator: {}",
1918            sig
1919        );
1920        assert!(
1921            sig.contains("def decorated_function"),
1922            "signature should include function def: {}",
1923            sig
1924        );
1925    }
1926
1927    #[test]
1928    fn py_ranges_valid() {
1929        let provider = TreeSitterProvider::new();
1930        let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
1931
1932        for s in &symbols {
1933            assert!(
1934                s.range.end_line >= s.range.start_line,
1935                "symbol {} has invalid range: {:?}",
1936                s.name,
1937                s.range
1938            );
1939        }
1940    }
1941
1942    // --- Rust extraction ---
1943
1944    #[test]
1945    fn rs_extracts_all_symbols() {
1946        let provider = TreeSitterProvider::new();
1947        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
1948
1949        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1950        assert!(
1951            names.contains(&"public_function"),
1952            "missing public_function: {:?}",
1953            names
1954        );
1955        assert!(
1956            names.contains(&"private_function"),
1957            "missing private_function: {:?}",
1958            names
1959        );
1960        assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
1961        assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
1962        assert!(
1963            names.contains(&"Drawable"),
1964            "missing trait Drawable: {:?}",
1965            names
1966        );
1967        // impl methods
1968        assert!(
1969            names.contains(&"new"),
1970            "missing impl method new: {:?}",
1971            names
1972        );
1973
1974        // Plan requires ≥6 symbols
1975        assert!(
1976            symbols.len() >= 6,
1977            "expected ≥6 symbols, got {}: {:?}",
1978            symbols.len(),
1979            names
1980        );
1981    }
1982
1983    #[test]
1984    fn rs_symbol_kinds_correct() {
1985        let provider = TreeSitterProvider::new();
1986        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
1987
1988        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
1989
1990        assert_eq!(find("public_function").kind, SymbolKind::Function);
1991        assert_eq!(find("private_function").kind, SymbolKind::Function);
1992        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
1993        assert_eq!(find("Color").kind, SymbolKind::Enum);
1994        assert_eq!(find("Drawable").kind, SymbolKind::Interface); // trait → Interface
1995        assert_eq!(find("new").kind, SymbolKind::Method);
1996    }
1997
1998    #[test]
1999    fn rs_pub_export_detection() {
2000        let provider = TreeSitterProvider::new();
2001        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2002
2003        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2004
2005        assert!(
2006            find("public_function").exported,
2007            "pub fn should be exported"
2008        );
2009        assert!(
2010            !find("private_function").exported,
2011            "non-pub fn should not be exported"
2012        );
2013        assert!(find("MyStruct").exported, "pub struct should be exported");
2014        assert!(find("Color").exported, "pub enum should be exported");
2015        assert!(find("Drawable").exported, "pub trait should be exported");
2016        assert!(
2017            find("new").exported,
2018            "pub fn inside impl should be exported"
2019        );
2020        assert!(
2021            !find("helper").exported,
2022            "non-pub fn inside impl should not be exported"
2023        );
2024    }
2025
2026    #[test]
2027    fn rs_impl_method_scope_chain() {
2028        let provider = TreeSitterProvider::new();
2029        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2030
2031        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2032
2033        // `impl MyStruct { fn new() }` → scope chain = ["MyStruct"]
2034        assert_eq!(
2035            find("new").scope_chain,
2036            vec!["MyStruct"],
2037            "impl method should have type in scope chain"
2038        );
2039        assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
2040
2041        // Free function has empty scope chain
2042        assert!(
2043            find("public_function").scope_chain.is_empty(),
2044            "free function should have empty scope chain"
2045        );
2046    }
2047
2048    #[test]
2049    fn rs_trait_impl_scope_chain() {
2050        let provider = TreeSitterProvider::new();
2051        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2052
2053        // `impl Drawable for MyStruct { fn draw() }` → scope = ["Drawable for MyStruct"]
2054        let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
2055        assert_eq!(
2056            draw.scope_chain,
2057            vec!["Drawable for MyStruct"],
2058            "trait impl method should have 'Trait for Type' scope"
2059        );
2060        assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
2061    }
2062
2063    #[test]
2064    fn rs_ranges_valid() {
2065        let provider = TreeSitterProvider::new();
2066        let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2067
2068        for s in &symbols {
2069            assert!(
2070                s.range.end_line >= s.range.start_line,
2071                "symbol {} has invalid range: {:?}",
2072                s.name,
2073                s.range
2074            );
2075        }
2076    }
2077
2078    // --- Go extraction ---
2079
2080    #[test]
2081    fn go_extracts_all_symbols() {
2082        let provider = TreeSitterProvider::new();
2083        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2084
2085        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2086        assert!(
2087            names.contains(&"ExportedFunction"),
2088            "missing ExportedFunction: {:?}",
2089            names
2090        );
2091        assert!(
2092            names.contains(&"unexportedFunction"),
2093            "missing unexportedFunction: {:?}",
2094            names
2095        );
2096        assert!(
2097            names.contains(&"MyStruct"),
2098            "missing struct MyStruct: {:?}",
2099            names
2100        );
2101        assert!(
2102            names.contains(&"Reader"),
2103            "missing interface Reader: {:?}",
2104            names
2105        );
2106        // receiver method
2107        assert!(
2108            names.contains(&"String"),
2109            "missing receiver method String: {:?}",
2110            names
2111        );
2112
2113        // Plan requires ≥4 symbols
2114        assert!(
2115            symbols.len() >= 4,
2116            "expected ≥4 symbols, got {}: {:?}",
2117            symbols.len(),
2118            names
2119        );
2120    }
2121
2122    #[test]
2123    fn go_symbol_kinds_correct() {
2124        let provider = TreeSitterProvider::new();
2125        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2126
2127        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2128
2129        assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
2130        assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
2131        assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
2132        assert_eq!(find("Reader").kind, SymbolKind::Interface);
2133        assert_eq!(find("String").kind, SymbolKind::Method);
2134        assert_eq!(find("helper").kind, SymbolKind::Method);
2135    }
2136
2137    #[test]
2138    fn go_uppercase_export_detection() {
2139        let provider = TreeSitterProvider::new();
2140        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2141
2142        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2143
2144        assert!(
2145            find("ExportedFunction").exported,
2146            "ExportedFunction (uppercase) should be exported"
2147        );
2148        assert!(
2149            !find("unexportedFunction").exported,
2150            "unexportedFunction (lowercase) should not be exported"
2151        );
2152        assert!(
2153            find("MyStruct").exported,
2154            "MyStruct (uppercase) should be exported"
2155        );
2156        assert!(
2157            find("Reader").exported,
2158            "Reader (uppercase) should be exported"
2159        );
2160        assert!(
2161            find("String").exported,
2162            "String method (uppercase) should be exported"
2163        );
2164        assert!(
2165            !find("helper").exported,
2166            "helper method (lowercase) should not be exported"
2167        );
2168    }
2169
2170    #[test]
2171    fn go_receiver_method_scope_chain() {
2172        let provider = TreeSitterProvider::new();
2173        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2174
2175        let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2176
2177        // `func (m *MyStruct) String()` → scope chain = ["MyStruct"]
2178        assert_eq!(
2179            find("String").scope_chain,
2180            vec!["MyStruct"],
2181            "receiver method should have type in scope chain"
2182        );
2183        assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
2184
2185        // Regular function has empty scope chain
2186        assert!(
2187            find("ExportedFunction").scope_chain.is_empty(),
2188            "regular function should have empty scope chain"
2189        );
2190    }
2191
2192    #[test]
2193    fn go_ranges_valid() {
2194        let provider = TreeSitterProvider::new();
2195        let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2196
2197        for s in &symbols {
2198            assert!(
2199                s.range.end_line >= s.range.start_line,
2200                "symbol {} has invalid range: {:?}",
2201                s.name,
2202                s.range
2203            );
2204        }
2205    }
2206
2207    // --- Cross-language ---
2208
2209    #[test]
2210    fn cross_language_all_six_produce_symbols() {
2211        let provider = TreeSitterProvider::new();
2212
2213        let fixtures = [
2214            ("sample.ts", "TypeScript"),
2215            ("sample.tsx", "TSX"),
2216            ("sample.js", "JavaScript"),
2217            ("sample.py", "Python"),
2218            ("sample.rs", "Rust"),
2219            ("sample.go", "Go"),
2220        ];
2221
2222        for (fixture, lang) in &fixtures {
2223            let symbols = provider
2224                .list_symbols(&fixture_path(fixture))
2225                .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
2226            assert!(
2227                symbols.len() >= 2,
2228                "{} should produce ≥2 symbols, got {}: {:?}",
2229                lang,
2230                symbols.len(),
2231                symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
2232            );
2233        }
2234    }
2235}