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
12const 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#[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
144pub 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
159pub 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
172fn 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
184struct CachedTree {
186 mtime: SystemTime,
187 tree: Tree,
188}
189
190pub 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 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 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 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 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
299pub(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
311pub(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 kind == "attribute_item"
326 || (kind == "line_comment"
328 && node_text(source, &prev).starts_with("///"))
329 || (kind == "block_comment"
331 && node_text(source, &prev).starts_with("/**"))
332 }
333 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
334 kind == "decorator"
336 || (kind == "comment"
338 && node_text(source, &prev).starts_with("/**"))
339 }
340 LangId::Go => {
341 kind == "comment" && is_adjacent_line(&prev, ¤t, source)
343 }
344 LangId::Python => {
345 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
363fn 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 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
382pub(crate) fn node_text<'a>(source: &'a str, node: &Node) -> &'a str {
384 &source[node.byte_range()]
385}
386
387fn 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
415fn 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
423fn 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 let trimmed = first_line.trim_end();
429 let trimmed = trimmed.strip_suffix('{').unwrap_or(trimmed).trim_end();
430 trimmed.to_string()
431}
432
433fn 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 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 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 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 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 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, parent: Some(class_name),
539 });
540 }
541
542 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 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 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 dedup_symbols(&mut symbols);
584 Ok(symbols)
585}
586
587fn 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
684fn 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
701fn 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 let mut decorated_fn_lines = std::collections::HashSet::new();
712
713 {
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 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 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 let kind = if is_method {
780 SymbolKind::Method
781 } else {
782 SymbolKind::Function
783 };
784
785 let sig = if decorated_fn_lines.contains(&def_node.start_position().row) {
787 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 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, parent: scope.last().cloned(),
822 });
823 }
824
825 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 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
872fn 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 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 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 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 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 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 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 if let Some(impl_node) = impl_def_node {
1009 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 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 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
1088fn 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 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 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 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 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
1196fn extract_go_receiver_type(method_node: &Node, source: &str) -> Option<String> {
1199 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 return find_type_identifier_recursive(&child, source);
1207 }
1208 if !child_cursor.goto_next_sibling() {
1209 break;
1210 }
1211 }
1212 }
1213 None
1214}
1215
1216fn 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
1235fn 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
1244fn 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 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 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 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 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
1329fn 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
1340pub 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 #[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 #[test]
1449 fn unsupported_extension_returns_invalid_request() {
1450 let path = fixture_path("sample.ts");
1452 let bad_path = path.with_extension("txt");
1453 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 #[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 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); 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 #[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 #[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 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 #[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 #[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 assert_eq!(tree1_root, tree2_root);
1818 }
1819
1820 #[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 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 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 assert_eq!(
1888 find("inner_method").scope_chain,
1889 vec!["OuterClass", "InnerClass"],
1890 "inner_method should have nested scope chain"
1891 );
1892
1893 assert_eq!(
1895 find("InnerClass").scope_chain,
1896 vec!["OuterClass"],
1897 "InnerClass should have OuterClass in scope"
1898 );
1899
1900 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 #[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 assert!(
1969 names.contains(&"new"),
1970 "missing impl method new: {:?}",
1971 names
1972 );
1973
1974 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); 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 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 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 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 #[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 assert!(
2108 names.contains(&"String"),
2109 "missing receiver method String: {:?}",
2110 names
2111 );
2112
2113 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 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 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 #[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}