1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
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::callgraph::resolve_module_path;
10use crate::error::AftError;
11use crate::symbols::{Range, Symbol, SymbolKind, SymbolMatch};
12
13const MAX_REEXPORT_DEPTH: usize = 10;
14
15const TS_QUERY: &str = r#"
18;; function declarations
19(function_declaration
20 name: (identifier) @fn.name) @fn.def
21
22;; arrow functions assigned to const/let/var
23(lexical_declaration
24 (variable_declarator
25 name: (identifier) @arrow.name
26 value: (arrow_function) @arrow.body)) @arrow.def
27
28;; class declarations
29(class_declaration
30 name: (type_identifier) @class.name) @class.def
31
32;; method definitions inside classes
33(class_declaration
34 name: (type_identifier) @method.class_name
35 body: (class_body
36 (method_definition
37 name: (property_identifier) @method.name) @method.def))
38
39;; interface declarations
40(interface_declaration
41 name: (type_identifier) @interface.name) @interface.def
42
43;; enum declarations
44(enum_declaration
45 name: (identifier) @enum.name) @enum.def
46
47;; type alias declarations
48(type_alias_declaration
49 name: (type_identifier) @type_alias.name) @type_alias.def
50
51;; top-level const/let variable declarations
52(lexical_declaration
53 (variable_declarator
54 name: (identifier) @var.name)) @var.def
55
56;; export statement wrappers (top-level only)
57(export_statement) @export.stmt
58"#;
59
60const JS_QUERY: &str = r#"
61;; function declarations
62(function_declaration
63 name: (identifier) @fn.name) @fn.def
64
65;; arrow functions assigned to const/let/var
66(lexical_declaration
67 (variable_declarator
68 name: (identifier) @arrow.name
69 value: (arrow_function) @arrow.body)) @arrow.def
70
71;; class declarations
72(class_declaration
73 name: (identifier) @class.name) @class.def
74
75;; method definitions inside classes
76(class_declaration
77 name: (identifier) @method.class_name
78 body: (class_body
79 (method_definition
80 name: (property_identifier) @method.name) @method.def))
81
82;; top-level const/let variable declarations
83(lexical_declaration
84 (variable_declarator
85 name: (identifier) @var.name)) @var.def
86
87;; export statement wrappers (top-level only)
88(export_statement) @export.stmt
89"#;
90
91const PY_QUERY: &str = r#"
92;; function definitions (top-level and nested)
93(function_definition
94 name: (identifier) @fn.name) @fn.def
95
96;; class definitions
97(class_definition
98 name: (identifier) @class.name) @class.def
99
100;; decorated definitions (wraps function_definition or class_definition)
101(decorated_definition
102 (decorator) @dec.decorator) @dec.def
103"#;
104
105const RS_QUERY: &str = r#"
106;; free functions (with optional visibility)
107(function_item
108 name: (identifier) @fn.name) @fn.def
109
110;; struct items
111(struct_item
112 name: (type_identifier) @struct.name) @struct.def
113
114;; enum items
115(enum_item
116 name: (type_identifier) @enum.name) @enum.def
117
118;; trait items
119(trait_item
120 name: (type_identifier) @trait.name) @trait.def
121
122;; impl blocks — capture the whole block to find methods
123(impl_item) @impl.def
124
125;; visibility modifiers on any item
126(visibility_modifier) @vis.mod
127"#;
128
129const GO_QUERY: &str = r#"
130;; function declarations
131(function_declaration
132 name: (identifier) @fn.name) @fn.def
133
134;; method declarations (with receiver)
135(method_declaration
136 name: (field_identifier) @method.name) @method.def
137
138;; type declarations (struct and interface)
139(type_declaration
140 (type_spec
141 name: (type_identifier) @type.name
142 type: (_) @type.body)) @type.def
143"#;
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum LangId {
148 TypeScript,
149 Tsx,
150 JavaScript,
151 Python,
152 Rust,
153 Go,
154 Markdown,
155}
156
157pub fn detect_language(path: &Path) -> Option<LangId> {
159 let ext = path.extension()?.to_str()?;
160 match ext {
161 "ts" => Some(LangId::TypeScript),
162 "tsx" => Some(LangId::Tsx),
163 "js" | "jsx" => Some(LangId::JavaScript),
164 "py" => Some(LangId::Python),
165 "rs" => Some(LangId::Rust),
166 "go" => Some(LangId::Go),
167 "md" | "markdown" | "mdx" => Some(LangId::Markdown),
168 _ => None,
169 }
170}
171
172pub fn grammar_for(lang: LangId) -> Language {
174 match lang {
175 LangId::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
176 LangId::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
177 LangId::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
178 LangId::Python => tree_sitter_python::LANGUAGE.into(),
179 LangId::Rust => tree_sitter_rust::LANGUAGE.into(),
180 LangId::Go => tree_sitter_go::LANGUAGE.into(),
181 LangId::Markdown => tree_sitter_md::LANGUAGE.into(),
182 }
183}
184
185fn query_for(lang: LangId) -> Option<&'static str> {
187 match lang {
188 LangId::TypeScript | LangId::Tsx => Some(TS_QUERY),
189 LangId::JavaScript => Some(JS_QUERY),
190 LangId::Python => Some(PY_QUERY),
191 LangId::Rust => Some(RS_QUERY),
192 LangId::Go => Some(GO_QUERY),
193 LangId::Markdown => None,
194 }
195}
196
197struct CachedTree {
199 mtime: SystemTime,
200 tree: Tree,
201}
202
203pub struct FileParser {
206 cache: HashMap<PathBuf, CachedTree>,
207}
208
209impl FileParser {
210 pub fn new() -> Self {
212 Self {
213 cache: HashMap::new(),
214 }
215 }
216
217 pub fn parse(&mut self, path: &Path) -> Result<(&Tree, LangId), AftError> {
220 let lang = detect_language(path).ok_or_else(|| AftError::InvalidRequest {
221 message: format!(
222 "unsupported file extension: {}",
223 path.extension()
224 .and_then(|e| e.to_str())
225 .unwrap_or("<none>")
226 ),
227 })?;
228
229 let canon = path.to_path_buf();
230 let current_mtime = std::fs::metadata(path)
231 .and_then(|m| m.modified())
232 .map_err(|e| AftError::FileNotFound {
233 path: format!("{}: {}", path.display(), e),
234 })?;
235
236 let needs_reparse = match self.cache.get(&canon) {
238 Some(cached) => cached.mtime != current_mtime,
239 None => true,
240 };
241
242 if needs_reparse {
243 let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
244 path: format!("{}: {}", path.display(), e),
245 })?;
246
247 let grammar = grammar_for(lang);
248 let mut parser = Parser::new();
249 parser.set_language(&grammar).map_err(|e| {
250 log::error!("grammar init failed for {:?}: {}", lang, e);
251 AftError::ParseError {
252 message: format!("grammar init failed for {:?}: {}", lang, e),
253 }
254 })?;
255
256 let tree = parser.parse(&source, None).ok_or_else(|| {
257 log::error!("parse failed for {}", path.display());
258 AftError::ParseError {
259 message: format!("tree-sitter parse returned None for {}", path.display()),
260 }
261 })?;
262
263 self.cache.insert(
264 canon.clone(),
265 CachedTree {
266 mtime: current_mtime,
267 tree,
268 },
269 );
270 }
271
272 let cached = self.cache.get(&canon).ok_or_else(|| AftError::ParseError {
273 message: format!("parser cache missing entry for {}", path.display()),
274 })?;
275 Ok((&cached.tree, lang))
276 }
277
278 pub fn parse_cloned(&mut self, path: &Path) -> Result<(Tree, LangId), AftError> {
283 let (tree, lang) = self.parse(path)?;
284 Ok((tree.clone(), lang))
285 }
286
287 pub fn extract_symbols(&mut self, path: &Path) -> Result<Vec<Symbol>, AftError> {
289 let source = std::fs::read_to_string(path).map_err(|e| AftError::FileNotFound {
290 path: format!("{}: {}", path.display(), e),
291 })?;
292
293 let (tree, lang) = self.parse(path)?;
294 let root = tree.root_node();
295
296 if lang == LangId::Markdown {
298 return extract_md_symbols(&source, &root);
299 }
300
301 let query_src = query_for(lang).ok_or_else(|| AftError::InvalidRequest {
302 message: format!("no query patterns implemented for {:?} yet", lang),
303 })?;
304
305 let grammar = grammar_for(lang);
306 let query = Query::new(&grammar, query_src).map_err(|e| {
307 log::error!("query compile failed for {:?}: {}", lang, e);
308 AftError::ParseError {
309 message: format!("query compile error for {:?}: {}", lang, e),
310 }
311 })?;
312
313 match lang {
314 LangId::TypeScript | LangId::Tsx => extract_ts_symbols(&source, &root, &query),
315 LangId::JavaScript => extract_js_symbols(&source, &root, &query),
316 LangId::Python => extract_py_symbols(&source, &root, &query),
317 LangId::Rust => extract_rs_symbols(&source, &root, &query),
318 LangId::Go => extract_go_symbols(&source, &root, &query),
319 LangId::Markdown => Ok(vec![]), }
321 }
322}
323
324pub(crate) fn node_range(node: &Node) -> Range {
326 let start = node.start_position();
327 let end = node.end_position();
328 Range {
329 start_line: start.row as u32,
330 start_col: start.column as u32,
331 end_line: end.row as u32,
332 end_col: end.column as u32,
333 }
334}
335
336pub(crate) fn node_range_with_decorators(node: &Node, source: &str, lang: LangId) -> Range {
342 let mut range = node_range(node);
343
344 let mut current = *node;
345 while let Some(prev) = current.prev_sibling() {
346 let kind = prev.kind();
347 let should_include = match lang {
348 LangId::Rust => {
349 kind == "attribute_item"
351 || (kind == "line_comment"
353 && node_text(source, &prev).starts_with("///"))
354 || (kind == "block_comment"
356 && node_text(source, &prev).starts_with("/**"))
357 }
358 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
359 kind == "decorator"
361 || (kind == "comment"
363 && node_text(source, &prev).starts_with("/**"))
364 }
365 LangId::Go => {
366 kind == "comment" && is_adjacent_line(&prev, ¤t, source)
368 }
369 LangId::Python => {
370 false
372 }
373 LangId::Markdown => false,
374 };
375
376 if should_include {
377 range.start_line = prev.start_position().row as u32;
378 range.start_col = prev.start_position().column as u32;
379 current = prev;
380 } else {
381 break;
382 }
383 }
384
385 range
386}
387
388fn is_adjacent_line(upper: &Node, lower: &Node, source: &str) -> bool {
390 let upper_end = upper.end_position().row;
391 let lower_start = lower.start_position().row;
392
393 if lower_start == 0 || lower_start <= upper_end {
394 return true;
395 }
396
397 let lines: Vec<&str> = source.lines().collect();
399 for row in (upper_end + 1)..lower_start {
400 if row < lines.len() && lines[row].trim().is_empty() {
401 return false;
402 }
403 }
404 true
405}
406
407pub(crate) fn node_text<'a>(source: &'a str, node: &Node) -> &'a str {
409 &source[node.byte_range()]
410}
411
412fn lexical_declaration_has_function_value(node: &Node) -> bool {
413 let mut cursor = node.walk();
414 if !cursor.goto_first_child() {
415 return false;
416 }
417
418 loop {
419 let child = cursor.node();
420 if matches!(
421 child.kind(),
422 "arrow_function" | "function_expression" | "generator_function"
423 ) {
424 return true;
425 }
426
427 if lexical_declaration_has_function_value(&child) {
428 return true;
429 }
430
431 if !cursor.goto_next_sibling() {
432 break;
433 }
434 }
435
436 false
437}
438
439fn collect_export_ranges(source: &str, root: &Node, query: &Query) -> Vec<std::ops::Range<usize>> {
441 let export_idx = query
442 .capture_names()
443 .iter()
444 .position(|n| *n == "export.stmt");
445 let export_idx = match export_idx {
446 Some(i) => i as u32,
447 None => return vec![],
448 };
449
450 let mut cursor = QueryCursor::new();
451 let mut ranges = Vec::new();
452 let mut matches = cursor.matches(query, *root, source.as_bytes());
453
454 while let Some(m) = {
455 matches.advance();
456 matches.get()
457 } {
458 for cap in m.captures {
459 if cap.index == export_idx {
460 ranges.push(cap.node.byte_range());
461 }
462 }
463 }
464 ranges
465}
466
467fn is_exported(node: &Node, export_ranges: &[std::ops::Range<usize>]) -> bool {
469 let r = node.byte_range();
470 export_ranges
471 .iter()
472 .any(|er| er.start <= r.start && r.end <= er.end)
473}
474
475fn extract_signature(source: &str, node: &Node) -> String {
477 let text = node_text(source, node);
478 let first_line = text.lines().next().unwrap_or(text);
479 let trimmed = first_line.trim_end();
481 let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
482 trimmed.to_string()
483}
484
485fn extract_ts_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
487 let lang = LangId::TypeScript;
488 let capture_names = query.capture_names();
489
490 let export_ranges = collect_export_ranges(source, root, query);
491
492 let mut symbols = Vec::new();
493 let mut cursor = QueryCursor::new();
494 let mut matches = cursor.matches(query, *root, source.as_bytes());
495
496 while let Some(m) = {
497 matches.advance();
498 matches.get()
499 } {
500 let mut fn_name_node = None;
502 let mut fn_def_node = None;
503 let mut arrow_name_node = None;
504 let mut arrow_def_node = None;
505 let mut class_name_node = None;
506 let mut class_def_node = None;
507 let mut method_class_name_node = None;
508 let mut method_name_node = None;
509 let mut method_def_node = None;
510 let mut interface_name_node = None;
511 let mut interface_def_node = None;
512 let mut enum_name_node = None;
513 let mut enum_def_node = None;
514 let mut type_alias_name_node = None;
515 let mut type_alias_def_node = None;
516 let mut var_name_node = None;
517 let mut var_def_node = None;
518
519 for cap in m.captures {
520 let Some(&name) = capture_names.get(cap.index as usize) else {
521 continue;
522 };
523 match name {
524 "fn.name" => fn_name_node = Some(cap.node),
525 "fn.def" => fn_def_node = Some(cap.node),
526 "arrow.name" => arrow_name_node = Some(cap.node),
527 "arrow.def" => arrow_def_node = Some(cap.node),
528 "class.name" => class_name_node = Some(cap.node),
529 "class.def" => class_def_node = Some(cap.node),
530 "method.class_name" => method_class_name_node = Some(cap.node),
531 "method.name" => method_name_node = Some(cap.node),
532 "method.def" => method_def_node = Some(cap.node),
533 "interface.name" => interface_name_node = Some(cap.node),
534 "interface.def" => interface_def_node = Some(cap.node),
535 "enum.name" => enum_name_node = Some(cap.node),
536 "enum.def" => enum_def_node = Some(cap.node),
537 "type_alias.name" => type_alias_name_node = Some(cap.node),
538 "type_alias.def" => type_alias_def_node = Some(cap.node),
539 "var.name" => var_name_node = Some(cap.node),
540 "var.def" => var_def_node = Some(cap.node),
541 _ => {}
543 }
544 }
545
546 if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
548 symbols.push(Symbol {
549 name: node_text(source, &name_node).to_string(),
550 kind: SymbolKind::Function,
551 range: node_range_with_decorators(&def_node, source, lang),
552 signature: Some(extract_signature(source, &def_node)),
553 scope_chain: vec![],
554 exported: is_exported(&def_node, &export_ranges),
555 parent: None,
556 });
557 }
558
559 if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
561 symbols.push(Symbol {
562 name: node_text(source, &name_node).to_string(),
563 kind: SymbolKind::Function,
564 range: node_range_with_decorators(&def_node, source, lang),
565 signature: Some(extract_signature(source, &def_node)),
566 scope_chain: vec![],
567 exported: is_exported(&def_node, &export_ranges),
568 parent: None,
569 });
570 }
571
572 if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
574 symbols.push(Symbol {
575 name: node_text(source, &name_node).to_string(),
576 kind: SymbolKind::Class,
577 range: node_range_with_decorators(&def_node, source, lang),
578 signature: Some(extract_signature(source, &def_node)),
579 scope_chain: vec![],
580 exported: is_exported(&def_node, &export_ranges),
581 parent: None,
582 });
583 }
584
585 if let (Some(class_name_node), Some(name_node), Some(def_node)) =
587 (method_class_name_node, method_name_node, method_def_node)
588 {
589 let class_name = node_text(source, &class_name_node).to_string();
590 symbols.push(Symbol {
591 name: node_text(source, &name_node).to_string(),
592 kind: SymbolKind::Method,
593 range: node_range_with_decorators(&def_node, source, lang),
594 signature: Some(extract_signature(source, &def_node)),
595 scope_chain: vec![class_name.clone()],
596 exported: false, parent: Some(class_name),
598 });
599 }
600
601 if let (Some(name_node), Some(def_node)) = (interface_name_node, interface_def_node) {
603 symbols.push(Symbol {
604 name: node_text(source, &name_node).to_string(),
605 kind: SymbolKind::Interface,
606 range: node_range_with_decorators(&def_node, source, lang),
607 signature: Some(extract_signature(source, &def_node)),
608 scope_chain: vec![],
609 exported: is_exported(&def_node, &export_ranges),
610 parent: None,
611 });
612 }
613
614 if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
616 symbols.push(Symbol {
617 name: node_text(source, &name_node).to_string(),
618 kind: SymbolKind::Enum,
619 range: node_range_with_decorators(&def_node, source, lang),
620 signature: Some(extract_signature(source, &def_node)),
621 scope_chain: vec![],
622 exported: is_exported(&def_node, &export_ranges),
623 parent: None,
624 });
625 }
626
627 if let (Some(name_node), Some(def_node)) = (type_alias_name_node, type_alias_def_node) {
629 symbols.push(Symbol {
630 name: node_text(source, &name_node).to_string(),
631 kind: SymbolKind::TypeAlias,
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)) = (var_name_node, var_def_node) {
642 let is_top_level = def_node
644 .parent()
645 .map(|p| p.kind() == "program" || p.kind() == "export_statement")
646 .unwrap_or(false);
647 let is_function_like = lexical_declaration_has_function_value(&def_node);
648 let name = node_text(source, &name_node).to_string();
649 let already_captured = symbols.iter().any(|s| s.name == name);
650 if is_top_level && !is_function_like && !already_captured {
651 symbols.push(Symbol {
652 name,
653 kind: SymbolKind::Variable,
654 range: node_range_with_decorators(&def_node, source, lang),
655 signature: Some(extract_signature(source, &def_node)),
656 scope_chain: vec![],
657 exported: is_exported(&def_node, &export_ranges),
658 parent: None,
659 });
660 }
661 }
662 }
663
664 dedup_symbols(&mut symbols);
666 Ok(symbols)
667}
668
669fn extract_js_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
671 let lang = LangId::JavaScript;
672 let capture_names = query.capture_names();
673
674 let export_ranges = collect_export_ranges(source, root, query);
675
676 let mut symbols = Vec::new();
677 let mut cursor = QueryCursor::new();
678 let mut matches = cursor.matches(query, *root, source.as_bytes());
679
680 while let Some(m) = {
681 matches.advance();
682 matches.get()
683 } {
684 let mut fn_name_node = None;
685 let mut fn_def_node = None;
686 let mut arrow_name_node = None;
687 let mut arrow_def_node = None;
688 let mut class_name_node = None;
689 let mut class_def_node = None;
690 let mut method_class_name_node = None;
691 let mut method_name_node = None;
692 let mut method_def_node = None;
693
694 for cap in m.captures {
695 let Some(&name) = capture_names.get(cap.index as usize) else {
696 continue;
697 };
698 match name {
699 "fn.name" => fn_name_node = Some(cap.node),
700 "fn.def" => fn_def_node = Some(cap.node),
701 "arrow.name" => arrow_name_node = Some(cap.node),
702 "arrow.def" => arrow_def_node = Some(cap.node),
703 "class.name" => class_name_node = Some(cap.node),
704 "class.def" => class_def_node = Some(cap.node),
705 "method.class_name" => method_class_name_node = Some(cap.node),
706 "method.name" => method_name_node = Some(cap.node),
707 "method.def" => method_def_node = Some(cap.node),
708 _ => {}
709 }
710 }
711
712 if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
713 symbols.push(Symbol {
714 name: node_text(source, &name_node).to_string(),
715 kind: SymbolKind::Function,
716 range: node_range_with_decorators(&def_node, source, lang),
717 signature: Some(extract_signature(source, &def_node)),
718 scope_chain: vec![],
719 exported: is_exported(&def_node, &export_ranges),
720 parent: None,
721 });
722 }
723
724 if let (Some(name_node), Some(def_node)) = (arrow_name_node, arrow_def_node) {
725 symbols.push(Symbol {
726 name: node_text(source, &name_node).to_string(),
727 kind: SymbolKind::Function,
728 range: node_range_with_decorators(&def_node, source, lang),
729 signature: Some(extract_signature(source, &def_node)),
730 scope_chain: vec![],
731 exported: is_exported(&def_node, &export_ranges),
732 parent: None,
733 });
734 }
735
736 if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
737 symbols.push(Symbol {
738 name: node_text(source, &name_node).to_string(),
739 kind: SymbolKind::Class,
740 range: node_range_with_decorators(&def_node, source, lang),
741 signature: Some(extract_signature(source, &def_node)),
742 scope_chain: vec![],
743 exported: is_exported(&def_node, &export_ranges),
744 parent: None,
745 });
746 }
747
748 if let (Some(class_name_node), Some(name_node), Some(def_node)) =
749 (method_class_name_node, method_name_node, method_def_node)
750 {
751 let class_name = node_text(source, &class_name_node).to_string();
752 symbols.push(Symbol {
753 name: node_text(source, &name_node).to_string(),
754 kind: SymbolKind::Method,
755 range: node_range_with_decorators(&def_node, source, lang),
756 signature: Some(extract_signature(source, &def_node)),
757 scope_chain: vec![class_name.clone()],
758 exported: false,
759 parent: Some(class_name),
760 });
761 }
762 }
763
764 dedup_symbols(&mut symbols);
765 Ok(symbols)
766}
767
768fn py_scope_chain(node: &Node, source: &str) -> Vec<String> {
771 let mut chain = Vec::new();
772 let mut current = node.parent();
773 while let Some(parent) = current {
774 if parent.kind() == "class_definition" {
775 if let Some(name_node) = parent.child_by_field_name("name") {
776 chain.push(node_text(source, &name_node).to_string());
777 }
778 }
779 current = parent.parent();
780 }
781 chain.reverse();
782 chain
783}
784
785fn extract_py_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
787 let lang = LangId::Python;
788 let capture_names = query.capture_names();
789
790 let mut symbols = Vec::new();
791 let mut cursor = QueryCursor::new();
792 let mut matches = cursor.matches(query, *root, source.as_bytes());
793
794 let mut decorated_fn_lines = std::collections::HashSet::new();
796
797 {
799 let mut cursor2 = QueryCursor::new();
800 let mut matches2 = cursor2.matches(query, *root, source.as_bytes());
801 while let Some(m) = {
802 matches2.advance();
803 matches2.get()
804 } {
805 let mut dec_def_node = None;
806 let mut dec_decorator_node = None;
807
808 for cap in m.captures {
809 let Some(&name) = capture_names.get(cap.index as usize) else {
810 continue;
811 };
812 match name {
813 "dec.def" => dec_def_node = Some(cap.node),
814 "dec.decorator" => dec_decorator_node = Some(cap.node),
815 _ => {}
816 }
817 }
818
819 if let (Some(def_node), Some(_dec_node)) = (dec_def_node, dec_decorator_node) {
820 let mut child_cursor = def_node.walk();
822 if child_cursor.goto_first_child() {
823 loop {
824 let child = child_cursor.node();
825 if child.kind() == "function_definition"
826 || child.kind() == "class_definition"
827 {
828 decorated_fn_lines.insert(child.start_position().row);
829 }
830 if !child_cursor.goto_next_sibling() {
831 break;
832 }
833 }
834 }
835 }
836 }
837 }
838
839 while let Some(m) = {
840 matches.advance();
841 matches.get()
842 } {
843 let mut fn_name_node = None;
844 let mut fn_def_node = None;
845 let mut class_name_node = None;
846 let mut class_def_node = None;
847
848 for cap in m.captures {
849 let Some(&name) = capture_names.get(cap.index as usize) else {
850 continue;
851 };
852 match name {
853 "fn.name" => fn_name_node = Some(cap.node),
854 "fn.def" => fn_def_node = Some(cap.node),
855 "class.name" => class_name_node = Some(cap.node),
856 "class.def" => class_def_node = Some(cap.node),
857 _ => {}
858 }
859 }
860
861 if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
863 let scope = py_scope_chain(&def_node, source);
864 let is_method = !scope.is_empty();
865 let name = node_text(source, &name_node).to_string();
866 let kind = if is_method {
868 SymbolKind::Method
869 } else {
870 SymbolKind::Function
871 };
872
873 let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
875 let mut sig_parts = Vec::new();
877 let mut parent = def_node.parent();
878 while let Some(p) = parent {
879 if p.kind() == "decorated_definition" {
880 let mut dc = p.walk();
882 if dc.goto_first_child() {
883 loop {
884 if dc.node().kind() == "decorator" {
885 sig_parts.push(node_text(source, &dc.node()).to_string());
886 }
887 if !dc.goto_next_sibling() {
888 break;
889 }
890 }
891 }
892 break;
893 }
894 parent = p.parent();
895 }
896 sig_parts.push(extract_signature(source, &def_node));
897 Some(sig_parts.join("\n"))
898 } else {
899 Some(extract_signature(source, &def_node))
900 };
901
902 symbols.push(Symbol {
903 name,
904 kind,
905 range: node_range_with_decorators(&def_node, source, lang),
906 signature: sig,
907 scope_chain: scope.clone(),
908 exported: false, parent: scope.last().cloned(),
910 });
911 }
912
913 if let (Some(name_node), Some(def_node)) = (class_name_node, class_def_node) {
915 let scope = py_scope_chain(&def_node, source);
916
917 let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
919 let mut sig_parts = Vec::new();
920 let mut parent = def_node.parent();
921 while let Some(p) = parent {
922 if p.kind() == "decorated_definition" {
923 let mut dc = p.walk();
924 if dc.goto_first_child() {
925 loop {
926 if dc.node().kind() == "decorator" {
927 sig_parts.push(node_text(source, &dc.node()).to_string());
928 }
929 if !dc.goto_next_sibling() {
930 break;
931 }
932 }
933 }
934 break;
935 }
936 parent = p.parent();
937 }
938 sig_parts.push(extract_signature(source, &def_node));
939 Some(sig_parts.join("\n"))
940 } else {
941 Some(extract_signature(source, &def_node))
942 };
943
944 symbols.push(Symbol {
945 name: node_text(source, &name_node).to_string(),
946 kind: SymbolKind::Class,
947 range: node_range_with_decorators(&def_node, source, lang),
948 signature: sig,
949 scope_chain: scope.clone(),
950 exported: false,
951 parent: scope.last().cloned(),
952 });
953 }
954 }
955
956 dedup_symbols(&mut symbols);
957 Ok(symbols)
958}
959
960fn extract_rs_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
963 let lang = LangId::Rust;
964 let capture_names = query.capture_names();
965
966 let mut vis_ranges: Vec<std::ops::Range<usize>> = Vec::new();
968 {
969 let vis_idx = capture_names.iter().position(|n| *n == "vis.mod");
970 if let Some(idx) = vis_idx {
971 let idx = idx as u32;
972 let mut cursor = QueryCursor::new();
973 let mut matches = cursor.matches(query, *root, source.as_bytes());
974 while let Some(m) = {
975 matches.advance();
976 matches.get()
977 } {
978 for cap in m.captures {
979 if cap.index == idx {
980 vis_ranges.push(cap.node.byte_range());
981 }
982 }
983 }
984 }
985 }
986
987 let is_pub = |node: &Node| -> bool {
988 let mut child_cursor = node.walk();
990 if child_cursor.goto_first_child() {
991 loop {
992 if child_cursor.node().kind() == "visibility_modifier" {
993 return true;
994 }
995 if !child_cursor.goto_next_sibling() {
996 break;
997 }
998 }
999 }
1000 false
1001 };
1002
1003 let mut symbols = Vec::new();
1004 let mut cursor = QueryCursor::new();
1005 let mut matches = cursor.matches(query, *root, source.as_bytes());
1006
1007 while let Some(m) = {
1008 matches.advance();
1009 matches.get()
1010 } {
1011 let mut fn_name_node = None;
1012 let mut fn_def_node = None;
1013 let mut struct_name_node = None;
1014 let mut struct_def_node = None;
1015 let mut enum_name_node = None;
1016 let mut enum_def_node = None;
1017 let mut trait_name_node = None;
1018 let mut trait_def_node = None;
1019 let mut impl_def_node = None;
1020
1021 for cap in m.captures {
1022 let Some(&name) = capture_names.get(cap.index as usize) else {
1023 continue;
1024 };
1025 match name {
1026 "fn.name" => fn_name_node = Some(cap.node),
1027 "fn.def" => fn_def_node = Some(cap.node),
1028 "struct.name" => struct_name_node = Some(cap.node),
1029 "struct.def" => struct_def_node = Some(cap.node),
1030 "enum.name" => enum_name_node = Some(cap.node),
1031 "enum.def" => enum_def_node = Some(cap.node),
1032 "trait.name" => trait_name_node = Some(cap.node),
1033 "trait.def" => trait_def_node = Some(cap.node),
1034 "impl.def" => impl_def_node = Some(cap.node),
1035 _ => {}
1036 }
1037 }
1038
1039 if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1041 let parent = def_node.parent();
1042 let in_impl = parent
1043 .map(|p| p.kind() == "declaration_list")
1044 .unwrap_or(false);
1045 if !in_impl {
1046 symbols.push(Symbol {
1047 name: node_text(source, &name_node).to_string(),
1048 kind: SymbolKind::Function,
1049 range: node_range_with_decorators(&def_node, source, lang),
1050 signature: Some(extract_signature(source, &def_node)),
1051 scope_chain: vec![],
1052 exported: is_pub(&def_node),
1053 parent: None,
1054 });
1055 }
1056 }
1057
1058 if let (Some(name_node), Some(def_node)) = (struct_name_node, struct_def_node) {
1060 symbols.push(Symbol {
1061 name: node_text(source, &name_node).to_string(),
1062 kind: SymbolKind::Struct,
1063 range: node_range_with_decorators(&def_node, source, lang),
1064 signature: Some(extract_signature(source, &def_node)),
1065 scope_chain: vec![],
1066 exported: is_pub(&def_node),
1067 parent: None,
1068 });
1069 }
1070
1071 if let (Some(name_node), Some(def_node)) = (enum_name_node, enum_def_node) {
1073 symbols.push(Symbol {
1074 name: node_text(source, &name_node).to_string(),
1075 kind: SymbolKind::Enum,
1076 range: node_range_with_decorators(&def_node, source, lang),
1077 signature: Some(extract_signature(source, &def_node)),
1078 scope_chain: vec![],
1079 exported: is_pub(&def_node),
1080 parent: None,
1081 });
1082 }
1083
1084 if let (Some(name_node), Some(def_node)) = (trait_name_node, trait_def_node) {
1086 symbols.push(Symbol {
1087 name: node_text(source, &name_node).to_string(),
1088 kind: SymbolKind::Interface,
1089 range: node_range_with_decorators(&def_node, source, lang),
1090 signature: Some(extract_signature(source, &def_node)),
1091 scope_chain: vec![],
1092 exported: is_pub(&def_node),
1093 parent: None,
1094 });
1095 }
1096
1097 if let Some(impl_node) = impl_def_node {
1099 let mut type_names: Vec<String> = Vec::new();
1103 let mut child_cursor = impl_node.walk();
1104 if child_cursor.goto_first_child() {
1105 loop {
1106 let child = child_cursor.node();
1107 if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1108 type_names.push(node_text(source, &child).to_string());
1109 }
1110 if !child_cursor.goto_next_sibling() {
1111 break;
1112 }
1113 }
1114 }
1115
1116 let scope_name = if type_names.len() >= 2 {
1117 format!("{} for {}", type_names[0], type_names[1])
1119 } else if type_names.len() == 1 {
1120 type_names[0].clone()
1121 } else {
1122 String::new()
1123 };
1124
1125 let parent_name = type_names.last().cloned().unwrap_or_default();
1126
1127 let mut child_cursor = impl_node.walk();
1129 if child_cursor.goto_first_child() {
1130 loop {
1131 let child = child_cursor.node();
1132 if child.kind() == "declaration_list" {
1133 let mut fn_cursor = child.walk();
1134 if fn_cursor.goto_first_child() {
1135 loop {
1136 let fn_node = fn_cursor.node();
1137 if fn_node.kind() == "function_item" {
1138 if let Some(name_node) = fn_node.child_by_field_name("name") {
1139 symbols.push(Symbol {
1140 name: node_text(source, &name_node).to_string(),
1141 kind: SymbolKind::Method,
1142 range: node_range_with_decorators(
1143 &fn_node, source, lang,
1144 ),
1145 signature: Some(extract_signature(source, &fn_node)),
1146 scope_chain: if scope_name.is_empty() {
1147 vec![]
1148 } else {
1149 vec![scope_name.clone()]
1150 },
1151 exported: is_pub(&fn_node),
1152 parent: if parent_name.is_empty() {
1153 None
1154 } else {
1155 Some(parent_name.clone())
1156 },
1157 });
1158 }
1159 }
1160 if !fn_cursor.goto_next_sibling() {
1161 break;
1162 }
1163 }
1164 }
1165 }
1166 if !child_cursor.goto_next_sibling() {
1167 break;
1168 }
1169 }
1170 }
1171 }
1172 }
1173
1174 dedup_symbols(&mut symbols);
1175 Ok(symbols)
1176}
1177
1178fn extract_go_symbols(source: &str, root: &Node, query: &Query) -> Result<Vec<Symbol>, AftError> {
1182 let lang = LangId::Go;
1183 let capture_names = query.capture_names();
1184
1185 let is_go_exported = |name: &str| -> bool {
1186 name.chars()
1187 .next()
1188 .map(|c| c.is_uppercase())
1189 .unwrap_or(false)
1190 };
1191
1192 let mut symbols = Vec::new();
1193 let mut cursor = QueryCursor::new();
1194 let mut matches = cursor.matches(query, *root, source.as_bytes());
1195
1196 while let Some(m) = {
1197 matches.advance();
1198 matches.get()
1199 } {
1200 let mut fn_name_node = None;
1201 let mut fn_def_node = None;
1202 let mut method_name_node = None;
1203 let mut method_def_node = None;
1204 let mut type_name_node = None;
1205 let mut type_body_node = None;
1206 let mut type_def_node = None;
1207
1208 for cap in m.captures {
1209 let Some(&name) = capture_names.get(cap.index as usize) else {
1210 continue;
1211 };
1212 match name {
1213 "fn.name" => fn_name_node = Some(cap.node),
1214 "fn.def" => fn_def_node = Some(cap.node),
1215 "method.name" => method_name_node = Some(cap.node),
1216 "method.def" => method_def_node = Some(cap.node),
1217 "type.name" => type_name_node = Some(cap.node),
1218 "type.body" => type_body_node = Some(cap.node),
1219 "type.def" => type_def_node = Some(cap.node),
1220 _ => {}
1221 }
1222 }
1223
1224 if let (Some(name_node), Some(def_node)) = (fn_name_node, fn_def_node) {
1226 let name = node_text(source, &name_node).to_string();
1227 symbols.push(Symbol {
1228 exported: is_go_exported(&name),
1229 name,
1230 kind: SymbolKind::Function,
1231 range: node_range_with_decorators(&def_node, source, lang),
1232 signature: Some(extract_signature(source, &def_node)),
1233 scope_chain: vec![],
1234 parent: None,
1235 });
1236 }
1237
1238 if let (Some(name_node), Some(def_node)) = (method_name_node, method_def_node) {
1240 let name = node_text(source, &name_node).to_string();
1241
1242 let receiver_type = extract_go_receiver_type(&def_node, source);
1244 let scope_chain = if let Some(ref rt) = receiver_type {
1245 vec![rt.clone()]
1246 } else {
1247 vec![]
1248 };
1249
1250 symbols.push(Symbol {
1251 exported: is_go_exported(&name),
1252 name,
1253 kind: SymbolKind::Method,
1254 range: node_range_with_decorators(&def_node, source, lang),
1255 signature: Some(extract_signature(source, &def_node)),
1256 scope_chain,
1257 parent: receiver_type,
1258 });
1259 }
1260
1261 if let (Some(name_node), Some(body_node), Some(def_node)) =
1263 (type_name_node, type_body_node, type_def_node)
1264 {
1265 let name = node_text(source, &name_node).to_string();
1266 let kind = match body_node.kind() {
1267 "struct_type" => SymbolKind::Struct,
1268 "interface_type" => SymbolKind::Interface,
1269 _ => SymbolKind::TypeAlias,
1270 };
1271
1272 symbols.push(Symbol {
1273 exported: is_go_exported(&name),
1274 name,
1275 kind,
1276 range: node_range_with_decorators(&def_node, source, lang),
1277 signature: Some(extract_signature(source, &def_node)),
1278 scope_chain: vec![],
1279 parent: None,
1280 });
1281 }
1282 }
1283
1284 dedup_symbols(&mut symbols);
1285 Ok(symbols)
1286}
1287
1288fn extract_go_receiver_type(method_node: &Node, source: &str) -> Option<String> {
1291 let mut child_cursor = method_node.walk();
1293 if child_cursor.goto_first_child() {
1294 loop {
1295 let child = child_cursor.node();
1296 if child.kind() == "parameter_list" {
1297 return find_type_identifier_recursive(&child, source);
1299 }
1300 if !child_cursor.goto_next_sibling() {
1301 break;
1302 }
1303 }
1304 }
1305 None
1306}
1307
1308fn find_type_identifier_recursive(node: &Node, source: &str) -> Option<String> {
1310 if node.kind() == "type_identifier" {
1311 return Some(node_text(source, node).to_string());
1312 }
1313 let mut cursor = node.walk();
1314 if cursor.goto_first_child() {
1315 loop {
1316 if let Some(result) = find_type_identifier_recursive(&cursor.node(), source) {
1317 return Some(result);
1318 }
1319 if !cursor.goto_next_sibling() {
1320 break;
1321 }
1322 }
1323 }
1324 None
1325}
1326
1327fn extract_md_symbols(source: &str, root: &Node) -> Result<Vec<Symbol>, AftError> {
1331 let mut symbols = Vec::new();
1332 extract_md_sections(source, root, &mut symbols, &[]);
1333 Ok(symbols)
1334}
1335
1336fn extract_md_sections(
1338 source: &str,
1339 node: &Node,
1340 symbols: &mut Vec<Symbol>,
1341 scope_chain: &[String],
1342) {
1343 let mut cursor = node.walk();
1344 if !cursor.goto_first_child() {
1345 return;
1346 }
1347
1348 loop {
1349 let child = cursor.node();
1350 match child.kind() {
1351 "section" => {
1352 let mut section_cursor = child.walk();
1355 let mut heading_name = String::new();
1356 let mut heading_level: u8 = 0;
1357
1358 if section_cursor.goto_first_child() {
1359 loop {
1360 let section_child = section_cursor.node();
1361 if section_child.kind() == "atx_heading" {
1362 let mut h_cursor = section_child.walk();
1364 if h_cursor.goto_first_child() {
1365 loop {
1366 let h_child = h_cursor.node();
1367 let kind = h_child.kind();
1368 if kind.starts_with("atx_h") && kind.ends_with("_marker") {
1369 heading_level = kind
1371 .strip_prefix("atx_h")
1372 .and_then(|s| s.strip_suffix("_marker"))
1373 .and_then(|s| s.parse::<u8>().ok())
1374 .unwrap_or(1);
1375 } else if h_child.kind() == "inline" {
1376 heading_name =
1377 node_text(source, &h_child).trim().to_string();
1378 }
1379 if !h_cursor.goto_next_sibling() {
1380 break;
1381 }
1382 }
1383 }
1384 }
1385 if !section_cursor.goto_next_sibling() {
1386 break;
1387 }
1388 }
1389 }
1390
1391 if !heading_name.is_empty() {
1392 let range = node_range(&child);
1393 let signature = format!(
1394 "{} {}",
1395 "#".repeat((heading_level as usize).min(6)),
1396 heading_name
1397 );
1398
1399 symbols.push(Symbol {
1400 name: heading_name.clone(),
1401 kind: SymbolKind::Heading,
1402 range,
1403 signature: Some(signature),
1404 scope_chain: scope_chain.to_vec(),
1405 exported: false,
1406 parent: scope_chain.last().cloned(),
1407 });
1408
1409 let mut new_scope = scope_chain.to_vec();
1411 new_scope.push(heading_name);
1412 extract_md_sections(source, &child, symbols, &new_scope);
1413 }
1414 }
1415 _ => {}
1416 }
1417
1418 if !cursor.goto_next_sibling() {
1419 break;
1420 }
1421 }
1422}
1423
1424fn dedup_symbols(symbols: &mut Vec<Symbol>) {
1428 let mut seen = std::collections::HashSet::new();
1429 symbols.retain(|s| {
1430 let key = (s.name.clone(), format!("{:?}", s.kind), s.range.start_line);
1431 seen.insert(key)
1432 });
1433}
1434
1435pub struct TreeSitterProvider {
1438 parser: RefCell<FileParser>,
1439}
1440
1441#[derive(Debug, Clone)]
1442struct ReExportTarget {
1443 file: PathBuf,
1444 symbol_name: String,
1445}
1446
1447impl TreeSitterProvider {
1448 pub fn new() -> Self {
1450 Self {
1451 parser: RefCell::new(FileParser::new()),
1452 }
1453 }
1454
1455 fn resolve_symbol_inner(
1456 &self,
1457 file: &Path,
1458 name: &str,
1459 depth: usize,
1460 visited: &mut HashSet<(PathBuf, String)>,
1461 ) -> Result<Vec<SymbolMatch>, AftError> {
1462 if depth > MAX_REEXPORT_DEPTH {
1463 return Ok(Vec::new());
1464 }
1465
1466 let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
1467 if !visited.insert((canonical_file, name.to_string())) {
1468 return Ok(Vec::new());
1469 }
1470
1471 let symbols = self.parser.borrow_mut().extract_symbols(file)?;
1472 let local_matches = symbol_matches_in_file(file, &symbols, name);
1473 if !local_matches.is_empty() {
1474 return Ok(local_matches);
1475 }
1476
1477 if name == "default" {
1478 let default_matches = self.resolve_local_default_export(file, &symbols)?;
1479 if !default_matches.is_empty() {
1480 return Ok(default_matches);
1481 }
1482 }
1483
1484 let reexport_targets = self.collect_reexport_targets(file, name)?;
1485 let mut matches = Vec::new();
1486 let mut seen = HashSet::new();
1487 for target in reexport_targets {
1488 for resolved in
1489 self.resolve_symbol_inner(&target.file, &target.symbol_name, depth + 1, visited)?
1490 {
1491 let key = format!(
1492 "{}:{}:{}:{}:{}:{}",
1493 resolved.file,
1494 resolved.symbol.name,
1495 resolved.symbol.range.start_line,
1496 resolved.symbol.range.start_col,
1497 resolved.symbol.range.end_line,
1498 resolved.symbol.range.end_col
1499 );
1500 if seen.insert(key) {
1501 matches.push(resolved);
1502 }
1503 }
1504 }
1505
1506 Ok(matches)
1507 }
1508
1509 fn collect_reexport_targets(
1510 &self,
1511 file: &Path,
1512 requested_name: &str,
1513 ) -> Result<Vec<ReExportTarget>, AftError> {
1514 let (source, tree, lang) = self.read_parsed_file(file)?;
1515 if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
1516 return Ok(Vec::new());
1517 }
1518
1519 let mut targets = Vec::new();
1520 let root = tree.root_node();
1521 let from_dir = file.parent().unwrap_or_else(|| Path::new("."));
1522
1523 let mut cursor = root.walk();
1524 if !cursor.goto_first_child() {
1525 return Ok(targets);
1526 }
1527
1528 loop {
1529 let node = cursor.node();
1530 if node.kind() == "export_statement" {
1531 let Some(source_node) = node.child_by_field_name("source") else {
1532 if !cursor.goto_next_sibling() {
1533 break;
1534 }
1535 continue;
1536 };
1537
1538 let Some(module_path) = string_content(&source, &source_node) else {
1539 if !cursor.goto_next_sibling() {
1540 break;
1541 }
1542 continue;
1543 };
1544
1545 let Some(target_file) = resolve_module_path(from_dir, &module_path) else {
1546 if !cursor.goto_next_sibling() {
1547 break;
1548 }
1549 continue;
1550 };
1551
1552 if let Some(export_clause) = find_child_by_kind(node, "export_clause") {
1553 if let Some(symbol_name) =
1554 resolve_export_clause_name(&source, &export_clause, requested_name)
1555 {
1556 targets.push(ReExportTarget {
1557 file: target_file,
1558 symbol_name,
1559 });
1560 }
1561 } else if export_statement_has_wildcard(&source, &node) {
1562 targets.push(ReExportTarget {
1563 file: target_file,
1564 symbol_name: requested_name.to_string(),
1565 });
1566 }
1567 }
1568
1569 if !cursor.goto_next_sibling() {
1570 break;
1571 }
1572 }
1573
1574 Ok(targets)
1575 }
1576
1577 fn resolve_local_default_export(
1578 &self,
1579 file: &Path,
1580 symbols: &[Symbol],
1581 ) -> Result<Vec<SymbolMatch>, AftError> {
1582 let (source, tree, lang) = self.read_parsed_file(file)?;
1583 if !matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
1584 return Ok(Vec::new());
1585 }
1586
1587 let root = tree.root_node();
1588 let mut matches = Vec::new();
1589 let mut seen = HashSet::new();
1590
1591 let mut cursor = root.walk();
1592 if !cursor.goto_first_child() {
1593 return Ok(matches);
1594 }
1595
1596 loop {
1597 let node = cursor.node();
1598 if node.kind() == "export_statement"
1599 && node.child_by_field_name("source").is_none()
1600 && node_contains_token(&source, &node, "default")
1601 {
1602 if let Some(target_name) = default_export_target_name(&source, &node) {
1603 for symbol_match in symbol_matches_in_file(file, symbols, &target_name) {
1604 let key = format!(
1605 "{}:{}:{}:{}:{}:{}",
1606 symbol_match.file,
1607 symbol_match.symbol.name,
1608 symbol_match.symbol.range.start_line,
1609 symbol_match.symbol.range.start_col,
1610 symbol_match.symbol.range.end_line,
1611 symbol_match.symbol.range.end_col
1612 );
1613 if seen.insert(key) {
1614 matches.push(symbol_match);
1615 }
1616 }
1617 }
1618 }
1619
1620 if !cursor.goto_next_sibling() {
1621 break;
1622 }
1623 }
1624
1625 Ok(matches)
1626 }
1627
1628 fn read_parsed_file(&self, file: &Path) -> Result<(String, Tree, LangId), AftError> {
1629 let source = std::fs::read_to_string(file).map_err(|e| AftError::FileNotFound {
1630 path: format!("{}: {}", file.display(), e),
1631 })?;
1632 let (tree, lang) = {
1633 let mut parser = self.parser.borrow_mut();
1634 parser.parse_cloned(file)?
1635 };
1636 Ok((source, tree, lang))
1637 }
1638}
1639
1640fn symbol_matches_in_file(file: &Path, symbols: &[Symbol], name: &str) -> Vec<SymbolMatch> {
1641 symbols
1642 .iter()
1643 .filter(|symbol| symbol.name == name)
1644 .cloned()
1645 .map(|symbol| SymbolMatch {
1646 file: file.display().to_string(),
1647 symbol,
1648 })
1649 .collect()
1650}
1651
1652fn string_content(source: &str, node: &Node) -> Option<String> {
1653 let text = node_text(source, node);
1654 if text.len() < 2 {
1655 return None;
1656 }
1657
1658 Some(
1659 text.trim_start_matches(|c| c == '\'' || c == '"')
1660 .trim_end_matches(|c| c == '\'' || c == '"')
1661 .to_string(),
1662 )
1663}
1664
1665fn find_child_by_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
1666 let mut cursor = node.walk();
1667 if !cursor.goto_first_child() {
1668 return None;
1669 }
1670
1671 loop {
1672 let child = cursor.node();
1673 if child.kind() == kind {
1674 return Some(child);
1675 }
1676 if !cursor.goto_next_sibling() {
1677 break;
1678 }
1679 }
1680
1681 None
1682}
1683
1684fn resolve_export_clause_name(
1685 source: &str,
1686 export_clause: &Node,
1687 requested_name: &str,
1688) -> Option<String> {
1689 let mut cursor = export_clause.walk();
1690 if !cursor.goto_first_child() {
1691 return None;
1692 }
1693
1694 loop {
1695 let child = cursor.node();
1696 if child.kind() == "export_specifier" {
1697 let (source_name, exported_name) = export_specifier_names(source, &child)?;
1698 if exported_name == requested_name {
1699 return Some(source_name);
1700 }
1701 }
1702
1703 if !cursor.goto_next_sibling() {
1704 break;
1705 }
1706 }
1707
1708 None
1709}
1710
1711fn export_specifier_names(source: &str, specifier: &Node) -> Option<(String, String)> {
1712 let source_name = specifier
1713 .child_by_field_name("name")
1714 .map(|node| node_text(source, &node).to_string());
1715 let alias_name = specifier
1716 .child_by_field_name("alias")
1717 .map(|node| node_text(source, &node).to_string());
1718
1719 if let Some(source_name) = source_name {
1720 let exported_name = alias_name.unwrap_or_else(|| source_name.clone());
1721 return Some((source_name, exported_name));
1722 }
1723
1724 let mut names = Vec::new();
1725 let mut cursor = specifier.walk();
1726 if cursor.goto_first_child() {
1727 loop {
1728 let child = cursor.node();
1729 let child_text = node_text(source, &child).trim();
1730 if matches!(
1731 child.kind(),
1732 "identifier" | "type_identifier" | "property_identifier"
1733 ) || child_text == "default"
1734 {
1735 names.push(child_text.to_string());
1736 }
1737 if !cursor.goto_next_sibling() {
1738 break;
1739 }
1740 }
1741 }
1742
1743 match names.as_slice() {
1744 [name] => Some((name.clone(), name.clone())),
1745 [source_name, exported_name, ..] => Some((source_name.clone(), exported_name.clone())),
1746 _ => None,
1747 }
1748}
1749
1750fn export_statement_has_wildcard(source: &str, node: &Node) -> bool {
1751 let mut cursor = node.walk();
1752 if !cursor.goto_first_child() {
1753 return false;
1754 }
1755
1756 loop {
1757 if node_text(source, &cursor.node()).trim() == "*" {
1758 return true;
1759 }
1760 if !cursor.goto_next_sibling() {
1761 break;
1762 }
1763 }
1764
1765 false
1766}
1767
1768fn node_contains_token(source: &str, node: &Node, token: &str) -> bool {
1769 let mut cursor = node.walk();
1770 if !cursor.goto_first_child() {
1771 return false;
1772 }
1773
1774 loop {
1775 if node_text(source, &cursor.node()).trim() == token {
1776 return true;
1777 }
1778 if !cursor.goto_next_sibling() {
1779 break;
1780 }
1781 }
1782
1783 false
1784}
1785
1786fn default_export_target_name(source: &str, export_stmt: &Node) -> Option<String> {
1787 let mut cursor = export_stmt.walk();
1788 if !cursor.goto_first_child() {
1789 return None;
1790 }
1791
1792 loop {
1793 let child = cursor.node();
1794 match child.kind() {
1795 "function_declaration"
1796 | "class_declaration"
1797 | "interface_declaration"
1798 | "enum_declaration"
1799 | "type_alias_declaration"
1800 | "lexical_declaration" => {
1801 if let Some(name_node) = child.child_by_field_name("name") {
1802 return Some(node_text(source, &name_node).to_string());
1803 }
1804
1805 if child.kind() == "lexical_declaration" {
1806 let mut child_cursor = child.walk();
1807 if child_cursor.goto_first_child() {
1808 loop {
1809 let nested = child_cursor.node();
1810 if nested.kind() == "variable_declarator" {
1811 if let Some(name_node) = nested.child_by_field_name("name") {
1812 return Some(node_text(source, &name_node).to_string());
1813 }
1814 }
1815 if !child_cursor.goto_next_sibling() {
1816 break;
1817 }
1818 }
1819 }
1820 }
1821 }
1822 "identifier" | "type_identifier" => {
1823 let text = node_text(source, &child);
1824 if text != "export" && text != "default" {
1825 return Some(text.to_string());
1826 }
1827 }
1828 _ => {}
1829 }
1830
1831 if !cursor.goto_next_sibling() {
1832 break;
1833 }
1834 }
1835
1836 None
1837}
1838
1839impl crate::language::LanguageProvider for TreeSitterProvider {
1840 fn resolve_symbol(&self, file: &Path, name: &str) -> Result<Vec<SymbolMatch>, AftError> {
1841 let matches = self.resolve_symbol_inner(file, name, 0, &mut HashSet::new())?;
1842
1843 if matches.is_empty() {
1844 Err(AftError::SymbolNotFound {
1845 name: name.to_string(),
1846 file: file.display().to_string(),
1847 })
1848 } else {
1849 Ok(matches)
1850 }
1851 }
1852
1853 fn list_symbols(&self, file: &Path) -> Result<Vec<Symbol>, AftError> {
1854 self.parser.borrow_mut().extract_symbols(file)
1855 }
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860 use super::*;
1861 use crate::language::LanguageProvider;
1862 use std::path::PathBuf;
1863
1864 fn fixture_path(name: &str) -> PathBuf {
1865 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1866 .join("tests")
1867 .join("fixtures")
1868 .join(name)
1869 }
1870
1871 #[test]
1874 fn detect_ts() {
1875 assert_eq!(
1876 detect_language(Path::new("foo.ts")),
1877 Some(LangId::TypeScript)
1878 );
1879 }
1880
1881 #[test]
1882 fn detect_tsx() {
1883 assert_eq!(detect_language(Path::new("foo.tsx")), Some(LangId::Tsx));
1884 }
1885
1886 #[test]
1887 fn detect_js() {
1888 assert_eq!(
1889 detect_language(Path::new("foo.js")),
1890 Some(LangId::JavaScript)
1891 );
1892 }
1893
1894 #[test]
1895 fn detect_jsx() {
1896 assert_eq!(
1897 detect_language(Path::new("foo.jsx")),
1898 Some(LangId::JavaScript)
1899 );
1900 }
1901
1902 #[test]
1903 fn detect_py() {
1904 assert_eq!(detect_language(Path::new("foo.py")), Some(LangId::Python));
1905 }
1906
1907 #[test]
1908 fn detect_rs() {
1909 assert_eq!(detect_language(Path::new("foo.rs")), Some(LangId::Rust));
1910 }
1911
1912 #[test]
1913 fn detect_go() {
1914 assert_eq!(detect_language(Path::new("foo.go")), Some(LangId::Go));
1915 }
1916
1917 #[test]
1918 fn detect_unknown_returns_none() {
1919 assert_eq!(detect_language(Path::new("foo.txt")), None);
1920 }
1921
1922 #[test]
1925 fn unsupported_extension_returns_invalid_request() {
1926 let path = fixture_path("sample.ts");
1928 let bad_path = path.with_extension("txt");
1929 std::fs::write(&bad_path, "hello").unwrap();
1931 let provider = TreeSitterProvider::new();
1932 let result = provider.list_symbols(&bad_path);
1933 std::fs::remove_file(&bad_path).ok();
1934 match result {
1935 Err(AftError::InvalidRequest { message }) => {
1936 assert!(
1937 message.contains("unsupported file extension"),
1938 "msg: {}",
1939 message
1940 );
1941 assert!(message.contains("txt"), "msg: {}", message);
1942 }
1943 other => panic!("expected InvalidRequest, got {:?}", other),
1944 }
1945 }
1946
1947 #[test]
1950 fn ts_extracts_all_symbol_kinds() {
1951 let provider = TreeSitterProvider::new();
1952 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
1953
1954 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1955 assert!(
1956 names.contains(&"greet"),
1957 "missing function greet: {:?}",
1958 names
1959 );
1960 assert!(names.contains(&"add"), "missing arrow fn add: {:?}", names);
1961 assert!(
1962 names.contains(&"UserService"),
1963 "missing class UserService: {:?}",
1964 names
1965 );
1966 assert!(
1967 names.contains(&"Config"),
1968 "missing interface Config: {:?}",
1969 names
1970 );
1971 assert!(
1972 names.contains(&"Status"),
1973 "missing enum Status: {:?}",
1974 names
1975 );
1976 assert!(
1977 names.contains(&"UserId"),
1978 "missing type alias UserId: {:?}",
1979 names
1980 );
1981 assert!(
1982 names.contains(&"internalHelper"),
1983 "missing non-exported fn: {:?}",
1984 names
1985 );
1986
1987 assert!(
1989 symbols.len() >= 6,
1990 "expected ≥6 symbols, got {}: {:?}",
1991 symbols.len(),
1992 names
1993 );
1994 }
1995
1996 #[test]
1997 fn ts_symbol_kinds_correct() {
1998 let provider = TreeSitterProvider::new();
1999 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
2000
2001 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2002
2003 assert_eq!(find("greet").kind, SymbolKind::Function);
2004 assert_eq!(find("add").kind, SymbolKind::Function); assert_eq!(find("UserService").kind, SymbolKind::Class);
2006 assert_eq!(find("Config").kind, SymbolKind::Interface);
2007 assert_eq!(find("Status").kind, SymbolKind::Enum);
2008 assert_eq!(find("UserId").kind, SymbolKind::TypeAlias);
2009 }
2010
2011 #[test]
2012 fn ts_export_detection() {
2013 let provider = TreeSitterProvider::new();
2014 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
2015
2016 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2017
2018 assert!(find("greet").exported, "greet should be exported");
2019 assert!(find("add").exported, "add should be exported");
2020 assert!(
2021 find("UserService").exported,
2022 "UserService should be exported"
2023 );
2024 assert!(find("Config").exported, "Config should be exported");
2025 assert!(find("Status").exported, "Status should be exported");
2026 assert!(
2027 !find("internalHelper").exported,
2028 "internalHelper should not be exported"
2029 );
2030 }
2031
2032 #[test]
2033 fn ts_method_scope_chain() {
2034 let provider = TreeSitterProvider::new();
2035 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
2036
2037 let methods: Vec<&Symbol> = symbols
2038 .iter()
2039 .filter(|s| s.kind == SymbolKind::Method)
2040 .collect();
2041 assert!(!methods.is_empty(), "should have at least one method");
2042
2043 for method in &methods {
2044 assert_eq!(
2045 method.scope_chain,
2046 vec!["UserService"],
2047 "method {} should have UserService in scope chain",
2048 method.name
2049 );
2050 assert_eq!(method.parent.as_deref(), Some("UserService"));
2051 }
2052 }
2053
2054 #[test]
2055 fn ts_signatures_present() {
2056 let provider = TreeSitterProvider::new();
2057 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
2058
2059 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2060
2061 let greet_sig = find("greet").signature.as_ref().unwrap();
2062 assert!(
2063 greet_sig.contains("greet"),
2064 "signature should contain function name: {}",
2065 greet_sig
2066 );
2067 }
2068
2069 #[test]
2070 fn ts_ranges_valid() {
2071 let provider = TreeSitterProvider::new();
2072 let symbols = provider.list_symbols(&fixture_path("sample.ts")).unwrap();
2073
2074 for s in &symbols {
2075 assert!(
2076 s.range.end_line >= s.range.start_line,
2077 "symbol {} has invalid range: {:?}",
2078 s.name,
2079 s.range
2080 );
2081 }
2082 }
2083
2084 #[test]
2087 fn js_extracts_core_symbols() {
2088 let provider = TreeSitterProvider::new();
2089 let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
2090
2091 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2092 assert!(
2093 names.contains(&"multiply"),
2094 "missing function multiply: {:?}",
2095 names
2096 );
2097 assert!(
2098 names.contains(&"divide"),
2099 "missing arrow fn divide: {:?}",
2100 names
2101 );
2102 assert!(
2103 names.contains(&"EventEmitter"),
2104 "missing class EventEmitter: {:?}",
2105 names
2106 );
2107 assert!(
2108 names.contains(&"main"),
2109 "missing default export fn main: {:?}",
2110 names
2111 );
2112
2113 assert!(
2114 symbols.len() >= 4,
2115 "expected ≥4 symbols, got {}: {:?}",
2116 symbols.len(),
2117 names
2118 );
2119 }
2120
2121 #[test]
2122 fn js_arrow_fn_correctly_named() {
2123 let provider = TreeSitterProvider::new();
2124 let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
2125
2126 let divide = symbols.iter().find(|s| s.name == "divide").unwrap();
2127 assert_eq!(divide.kind, SymbolKind::Function);
2128 assert!(divide.exported, "divide should be exported");
2129
2130 let internal = symbols.iter().find(|s| s.name == "internalUtil").unwrap();
2131 assert_eq!(internal.kind, SymbolKind::Function);
2132 assert!(!internal.exported, "internalUtil should not be exported");
2133 }
2134
2135 #[test]
2136 fn js_method_scope_chain() {
2137 let provider = TreeSitterProvider::new();
2138 let symbols = provider.list_symbols(&fixture_path("sample.js")).unwrap();
2139
2140 let methods: Vec<&Symbol> = symbols
2141 .iter()
2142 .filter(|s| s.kind == SymbolKind::Method)
2143 .collect();
2144
2145 for method in &methods {
2146 assert_eq!(
2147 method.scope_chain,
2148 vec!["EventEmitter"],
2149 "method {} should have EventEmitter in scope chain",
2150 method.name
2151 );
2152 }
2153 }
2154
2155 #[test]
2158 fn tsx_extracts_react_component() {
2159 let provider = TreeSitterProvider::new();
2160 let symbols = provider.list_symbols(&fixture_path("sample.tsx")).unwrap();
2161
2162 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2163 assert!(
2164 names.contains(&"Button"),
2165 "missing React component Button: {:?}",
2166 names
2167 );
2168 assert!(
2169 names.contains(&"Counter"),
2170 "missing class Counter: {:?}",
2171 names
2172 );
2173 assert!(
2174 names.contains(&"formatLabel"),
2175 "missing function formatLabel: {:?}",
2176 names
2177 );
2178
2179 assert!(
2180 symbols.len() >= 2,
2181 "expected ≥2 symbols, got {}: {:?}",
2182 symbols.len(),
2183 names
2184 );
2185 }
2186
2187 #[test]
2188 fn tsx_jsx_doesnt_break_parser() {
2189 let provider = TreeSitterProvider::new();
2191 let result = provider.list_symbols(&fixture_path("sample.tsx"));
2192 assert!(
2193 result.is_ok(),
2194 "TSX parsing should succeed: {:?}",
2195 result.err()
2196 );
2197 }
2198
2199 #[test]
2202 fn resolve_symbol_finds_match() {
2203 let provider = TreeSitterProvider::new();
2204 let matches = provider
2205 .resolve_symbol(&fixture_path("sample.ts"), "greet")
2206 .unwrap();
2207 assert_eq!(matches.len(), 1);
2208 assert_eq!(matches[0].symbol.name, "greet");
2209 assert_eq!(matches[0].symbol.kind, SymbolKind::Function);
2210 }
2211
2212 #[test]
2213 fn resolve_symbol_not_found() {
2214 let provider = TreeSitterProvider::new();
2215 let result = provider.resolve_symbol(&fixture_path("sample.ts"), "nonexistent");
2216 assert!(matches!(result, Err(AftError::SymbolNotFound { .. })));
2217 }
2218
2219 #[test]
2220 fn resolve_symbol_follows_reexport_chains() {
2221 let dir = tempfile::tempdir().unwrap();
2222 let config = dir.path().join("config.ts");
2223 let barrel1 = dir.path().join("barrel1.ts");
2224 let barrel2 = dir.path().join("barrel2.ts");
2225 let barrel3 = dir.path().join("barrel3.ts");
2226 let index = dir.path().join("index.ts");
2227
2228 std::fs::write(
2229 &config,
2230 "export class Config {}\nexport default class DefaultConfig {}\n",
2231 )
2232 .unwrap();
2233 std::fs::write(
2234 &barrel1,
2235 "export { Config } from './config';\nexport { default as NamedDefault } from './config';\n",
2236 )
2237 .unwrap();
2238 std::fs::write(
2239 &barrel2,
2240 "export { Config as RenamedConfig } from './barrel1';\n",
2241 )
2242 .unwrap();
2243 std::fs::write(
2244 &barrel3,
2245 "export * from './barrel2';\nexport * from './barrel1';\n",
2246 )
2247 .unwrap();
2248 std::fs::write(
2249 &index,
2250 "export class Config {}\nexport { RenamedConfig as FinalConfig } from './barrel3';\nexport * from './barrel3';\n",
2251 )
2252 .unwrap();
2253
2254 let provider = TreeSitterProvider::new();
2255 let config_canon = std::fs::canonicalize(&config).unwrap();
2256
2257 let direct = provider.resolve_symbol(&barrel1, "Config").unwrap();
2258 assert_eq!(direct.len(), 1);
2259 assert_eq!(direct[0].symbol.name, "Config");
2260 assert_eq!(direct[0].file, config_canon.display().to_string());
2261
2262 let renamed = provider.resolve_symbol(&barrel2, "RenamedConfig").unwrap();
2263 assert_eq!(renamed.len(), 1);
2264 assert_eq!(renamed[0].symbol.name, "Config");
2265 assert_eq!(renamed[0].file, config_canon.display().to_string());
2266
2267 let wildcard_chain = provider.resolve_symbol(&index, "FinalConfig").unwrap();
2268 assert_eq!(wildcard_chain.len(), 1);
2269 assert_eq!(wildcard_chain[0].symbol.name, "Config");
2270 assert_eq!(wildcard_chain[0].file, config_canon.display().to_string());
2271
2272 let wildcard_default = provider.resolve_symbol(&index, "NamedDefault").unwrap();
2273 assert_eq!(wildcard_default.len(), 1);
2274 assert_eq!(wildcard_default[0].symbol.name, "DefaultConfig");
2275 assert_eq!(wildcard_default[0].file, config_canon.display().to_string());
2276
2277 let local = provider.resolve_symbol(&index, "Config").unwrap();
2278 assert_eq!(local.len(), 1);
2279 assert_eq!(local[0].symbol.name, "Config");
2280 assert_eq!(local[0].file, index.display().to_string());
2281 }
2282
2283 #[test]
2286 fn symbol_range_includes_rust_attributes() {
2287 let dir = tempfile::tempdir().unwrap();
2288 let path = dir.path().join("test_attrs.rs");
2289 std::fs::write(
2290 &path,
2291 "/// This is a doc comment\n#[test]\n#[cfg(test)]\nfn my_test_fn() {\n assert!(true);\n}\n",
2292 )
2293 .unwrap();
2294
2295 let provider = TreeSitterProvider::new();
2296 let matches = provider.resolve_symbol(&path, "my_test_fn").unwrap();
2297 assert_eq!(matches.len(), 1);
2298 assert_eq!(
2299 matches[0].symbol.range.start_line, 0,
2300 "symbol range should include preceding /// doc comment, got start_line={}",
2301 matches[0].symbol.range.start_line
2302 );
2303 }
2304
2305 #[test]
2306 fn symbol_range_includes_go_doc_comment() {
2307 let dir = tempfile::tempdir().unwrap();
2308 let path = dir.path().join("test_doc.go");
2309 std::fs::write(
2310 &path,
2311 "package main\n\n// MyFunc does something useful.\n// It has a multi-line doc.\nfunc MyFunc() {\n}\n",
2312 )
2313 .unwrap();
2314
2315 let provider = TreeSitterProvider::new();
2316 let matches = provider.resolve_symbol(&path, "MyFunc").unwrap();
2317 assert_eq!(matches.len(), 1);
2318 assert_eq!(
2319 matches[0].symbol.range.start_line, 2,
2320 "symbol range should include preceding doc comments, got start_line={}",
2321 matches[0].symbol.range.start_line
2322 );
2323 }
2324
2325 #[test]
2326 fn symbol_range_skips_unrelated_comments() {
2327 let dir = tempfile::tempdir().unwrap();
2328 let path = dir.path().join("test_gap.go");
2329 std::fs::write(
2330 &path,
2331 "package main\n\n// This is a standalone comment\n\nfunc Standalone() {\n}\n",
2332 )
2333 .unwrap();
2334
2335 let provider = TreeSitterProvider::new();
2336 let matches = provider.resolve_symbol(&path, "Standalone").unwrap();
2337 assert_eq!(matches.len(), 1);
2338 assert_eq!(
2339 matches[0].symbol.range.start_line, 4,
2340 "symbol range should NOT include comment separated by blank line, got start_line={}",
2341 matches[0].symbol.range.start_line
2342 );
2343 }
2344
2345 #[test]
2346 fn parse_cache_returns_same_tree() {
2347 let mut parser = FileParser::new();
2348 let path = fixture_path("sample.ts");
2349
2350 let (tree1, _) = parser.parse(&path).unwrap();
2351 let tree1_root = tree1.root_node().byte_range();
2352
2353 let (tree2, _) = parser.parse(&path).unwrap();
2354 let tree2_root = tree2.root_node().byte_range();
2355
2356 assert_eq!(tree1_root, tree2_root);
2358 }
2359
2360 #[test]
2363 fn py_extracts_all_symbols() {
2364 let provider = TreeSitterProvider::new();
2365 let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
2366
2367 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2368 assert!(
2369 names.contains(&"top_level_function"),
2370 "missing top_level_function: {:?}",
2371 names
2372 );
2373 assert!(names.contains(&"MyClass"), "missing MyClass: {:?}", names);
2374 assert!(
2375 names.contains(&"instance_method"),
2376 "missing method instance_method: {:?}",
2377 names
2378 );
2379 assert!(
2380 names.contains(&"decorated_function"),
2381 "missing decorated_function: {:?}",
2382 names
2383 );
2384
2385 assert!(
2387 symbols.len() >= 4,
2388 "expected ≥4 symbols, got {}: {:?}",
2389 symbols.len(),
2390 names
2391 );
2392 }
2393
2394 #[test]
2395 fn py_symbol_kinds_correct() {
2396 let provider = TreeSitterProvider::new();
2397 let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
2398
2399 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2400
2401 assert_eq!(find("top_level_function").kind, SymbolKind::Function);
2402 assert_eq!(find("MyClass").kind, SymbolKind::Class);
2403 assert_eq!(find("instance_method").kind, SymbolKind::Method);
2404 assert_eq!(find("decorated_function").kind, SymbolKind::Function);
2405 assert_eq!(find("OuterClass").kind, SymbolKind::Class);
2406 assert_eq!(find("InnerClass").kind, SymbolKind::Class);
2407 assert_eq!(find("inner_method").kind, SymbolKind::Method);
2408 assert_eq!(find("outer_method").kind, SymbolKind::Method);
2409 }
2410
2411 #[test]
2412 fn py_method_scope_chain() {
2413 let provider = TreeSitterProvider::new();
2414 let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
2415
2416 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2417
2418 assert_eq!(
2420 find("instance_method").scope_chain,
2421 vec!["MyClass"],
2422 "instance_method should have MyClass in scope chain"
2423 );
2424 assert_eq!(find("instance_method").parent.as_deref(), Some("MyClass"));
2425
2426 assert_eq!(
2428 find("inner_method").scope_chain,
2429 vec!["OuterClass", "InnerClass"],
2430 "inner_method should have nested scope chain"
2431 );
2432
2433 assert_eq!(
2435 find("InnerClass").scope_chain,
2436 vec!["OuterClass"],
2437 "InnerClass should have OuterClass in scope"
2438 );
2439
2440 assert!(
2442 find("top_level_function").scope_chain.is_empty(),
2443 "top-level function should have empty scope chain"
2444 );
2445 }
2446
2447 #[test]
2448 fn py_decorated_function_signature() {
2449 let provider = TreeSitterProvider::new();
2450 let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
2451
2452 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2453
2454 let sig = find("decorated_function").signature.as_ref().unwrap();
2455 assert!(
2456 sig.contains("@staticmethod"),
2457 "decorated function signature should include decorator: {}",
2458 sig
2459 );
2460 assert!(
2461 sig.contains("def decorated_function"),
2462 "signature should include function def: {}",
2463 sig
2464 );
2465 }
2466
2467 #[test]
2468 fn py_ranges_valid() {
2469 let provider = TreeSitterProvider::new();
2470 let symbols = provider.list_symbols(&fixture_path("sample.py")).unwrap();
2471
2472 for s in &symbols {
2473 assert!(
2474 s.range.end_line >= s.range.start_line,
2475 "symbol {} has invalid range: {:?}",
2476 s.name,
2477 s.range
2478 );
2479 }
2480 }
2481
2482 #[test]
2485 fn rs_extracts_all_symbols() {
2486 let provider = TreeSitterProvider::new();
2487 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2488
2489 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2490 assert!(
2491 names.contains(&"public_function"),
2492 "missing public_function: {:?}",
2493 names
2494 );
2495 assert!(
2496 names.contains(&"private_function"),
2497 "missing private_function: {:?}",
2498 names
2499 );
2500 assert!(names.contains(&"MyStruct"), "missing MyStruct: {:?}", names);
2501 assert!(names.contains(&"Color"), "missing enum Color: {:?}", names);
2502 assert!(
2503 names.contains(&"Drawable"),
2504 "missing trait Drawable: {:?}",
2505 names
2506 );
2507 assert!(
2509 names.contains(&"new"),
2510 "missing impl method new: {:?}",
2511 names
2512 );
2513
2514 assert!(
2516 symbols.len() >= 6,
2517 "expected ≥6 symbols, got {}: {:?}",
2518 symbols.len(),
2519 names
2520 );
2521 }
2522
2523 #[test]
2524 fn rs_symbol_kinds_correct() {
2525 let provider = TreeSitterProvider::new();
2526 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2527
2528 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2529
2530 assert_eq!(find("public_function").kind, SymbolKind::Function);
2531 assert_eq!(find("private_function").kind, SymbolKind::Function);
2532 assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
2533 assert_eq!(find("Color").kind, SymbolKind::Enum);
2534 assert_eq!(find("Drawable").kind, SymbolKind::Interface); assert_eq!(find("new").kind, SymbolKind::Method);
2536 }
2537
2538 #[test]
2539 fn rs_pub_export_detection() {
2540 let provider = TreeSitterProvider::new();
2541 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2542
2543 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2544
2545 assert!(
2546 find("public_function").exported,
2547 "pub fn should be exported"
2548 );
2549 assert!(
2550 !find("private_function").exported,
2551 "non-pub fn should not be exported"
2552 );
2553 assert!(find("MyStruct").exported, "pub struct should be exported");
2554 assert!(find("Color").exported, "pub enum should be exported");
2555 assert!(find("Drawable").exported, "pub trait should be exported");
2556 assert!(
2557 find("new").exported,
2558 "pub fn inside impl should be exported"
2559 );
2560 assert!(
2561 !find("helper").exported,
2562 "non-pub fn inside impl should not be exported"
2563 );
2564 }
2565
2566 #[test]
2567 fn rs_impl_method_scope_chain() {
2568 let provider = TreeSitterProvider::new();
2569 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2570
2571 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2572
2573 assert_eq!(
2575 find("new").scope_chain,
2576 vec!["MyStruct"],
2577 "impl method should have type in scope chain"
2578 );
2579 assert_eq!(find("new").parent.as_deref(), Some("MyStruct"));
2580
2581 assert!(
2583 find("public_function").scope_chain.is_empty(),
2584 "free function should have empty scope chain"
2585 );
2586 }
2587
2588 #[test]
2589 fn rs_trait_impl_scope_chain() {
2590 let provider = TreeSitterProvider::new();
2591 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2592
2593 let draw = symbols.iter().find(|s| s.name == "draw").unwrap();
2595 assert_eq!(
2596 draw.scope_chain,
2597 vec!["Drawable for MyStruct"],
2598 "trait impl method should have 'Trait for Type' scope"
2599 );
2600 assert_eq!(draw.parent.as_deref(), Some("MyStruct"));
2601 }
2602
2603 #[test]
2604 fn rs_ranges_valid() {
2605 let provider = TreeSitterProvider::new();
2606 let symbols = provider.list_symbols(&fixture_path("sample.rs")).unwrap();
2607
2608 for s in &symbols {
2609 assert!(
2610 s.range.end_line >= s.range.start_line,
2611 "symbol {} has invalid range: {:?}",
2612 s.name,
2613 s.range
2614 );
2615 }
2616 }
2617
2618 #[test]
2621 fn go_extracts_all_symbols() {
2622 let provider = TreeSitterProvider::new();
2623 let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2624
2625 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2626 assert!(
2627 names.contains(&"ExportedFunction"),
2628 "missing ExportedFunction: {:?}",
2629 names
2630 );
2631 assert!(
2632 names.contains(&"unexportedFunction"),
2633 "missing unexportedFunction: {:?}",
2634 names
2635 );
2636 assert!(
2637 names.contains(&"MyStruct"),
2638 "missing struct MyStruct: {:?}",
2639 names
2640 );
2641 assert!(
2642 names.contains(&"Reader"),
2643 "missing interface Reader: {:?}",
2644 names
2645 );
2646 assert!(
2648 names.contains(&"String"),
2649 "missing receiver method String: {:?}",
2650 names
2651 );
2652
2653 assert!(
2655 symbols.len() >= 4,
2656 "expected ≥4 symbols, got {}: {:?}",
2657 symbols.len(),
2658 names
2659 );
2660 }
2661
2662 #[test]
2663 fn go_symbol_kinds_correct() {
2664 let provider = TreeSitterProvider::new();
2665 let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2666
2667 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2668
2669 assert_eq!(find("ExportedFunction").kind, SymbolKind::Function);
2670 assert_eq!(find("unexportedFunction").kind, SymbolKind::Function);
2671 assert_eq!(find("MyStruct").kind, SymbolKind::Struct);
2672 assert_eq!(find("Reader").kind, SymbolKind::Interface);
2673 assert_eq!(find("String").kind, SymbolKind::Method);
2674 assert_eq!(find("helper").kind, SymbolKind::Method);
2675 }
2676
2677 #[test]
2678 fn go_uppercase_export_detection() {
2679 let provider = TreeSitterProvider::new();
2680 let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2681
2682 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2683
2684 assert!(
2685 find("ExportedFunction").exported,
2686 "ExportedFunction (uppercase) should be exported"
2687 );
2688 assert!(
2689 !find("unexportedFunction").exported,
2690 "unexportedFunction (lowercase) should not be exported"
2691 );
2692 assert!(
2693 find("MyStruct").exported,
2694 "MyStruct (uppercase) should be exported"
2695 );
2696 assert!(
2697 find("Reader").exported,
2698 "Reader (uppercase) should be exported"
2699 );
2700 assert!(
2701 find("String").exported,
2702 "String method (uppercase) should be exported"
2703 );
2704 assert!(
2705 !find("helper").exported,
2706 "helper method (lowercase) should not be exported"
2707 );
2708 }
2709
2710 #[test]
2711 fn go_receiver_method_scope_chain() {
2712 let provider = TreeSitterProvider::new();
2713 let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2714
2715 let find = |name: &str| symbols.iter().find(|s| s.name == name).unwrap();
2716
2717 assert_eq!(
2719 find("String").scope_chain,
2720 vec!["MyStruct"],
2721 "receiver method should have type in scope chain"
2722 );
2723 assert_eq!(find("String").parent.as_deref(), Some("MyStruct"));
2724
2725 assert!(
2727 find("ExportedFunction").scope_chain.is_empty(),
2728 "regular function should have empty scope chain"
2729 );
2730 }
2731
2732 #[test]
2733 fn go_ranges_valid() {
2734 let provider = TreeSitterProvider::new();
2735 let symbols = provider.list_symbols(&fixture_path("sample.go")).unwrap();
2736
2737 for s in &symbols {
2738 assert!(
2739 s.range.end_line >= s.range.start_line,
2740 "symbol {} has invalid range: {:?}",
2741 s.name,
2742 s.range
2743 );
2744 }
2745 }
2746
2747 #[test]
2750 fn cross_language_all_six_produce_symbols() {
2751 let provider = TreeSitterProvider::new();
2752
2753 let fixtures = [
2754 ("sample.ts", "TypeScript"),
2755 ("sample.tsx", "TSX"),
2756 ("sample.js", "JavaScript"),
2757 ("sample.py", "Python"),
2758 ("sample.rs", "Rust"),
2759 ("sample.go", "Go"),
2760 ];
2761
2762 for (fixture, lang) in &fixtures {
2763 let symbols = provider
2764 .list_symbols(&fixture_path(fixture))
2765 .unwrap_or_else(|e| panic!("{} ({}) failed: {:?}", lang, fixture, e));
2766 assert!(
2767 symbols.len() >= 2,
2768 "{} should produce ≥2 symbols, got {}: {:?}",
2769 lang,
2770 symbols.len(),
2771 symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
2772 );
2773 }
2774 }
2775}