1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use tracing::warn;
5use tree_sitter::Language;
6
7use astmap_core::SymbolKind;
8
9use super::{
10 collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
11 symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
12 LanguageResolver, LanguageSupport, ParseResult,
13};
14
15pub(super) fn lang() -> LanguageSupport {
16 LanguageSupport {
17 name: "typescript",
18 extensions: &["ts", "tsx"],
19 parser: &TypeScriptParser,
20 resolver_factory: |root| Box::new(TypeScriptResolver::new(root)),
21 config_files: &["tsconfig.json"],
22 sibling_fn: |_| None, }
24}
25
26pub(crate) struct TypeScriptParser;
29
30impl LanguageParser for TypeScriptParser {
31 fn parse(&self, source: &str, file_path: &Path) -> ParseResult {
32 let language = if file_path.extension().is_some_and(|e| e == "tsx") {
33 tsx_language()
34 } else {
35 ts_language()
36 };
37 parse_ts(source, language)
38 }
39
40 fn language_name(&self) -> &str {
41 "typescript"
42 }
43}
44
45fn ts_language() -> Language {
46 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
47}
48
49fn tsx_language() -> Language {
50 tree_sitter_typescript::LANGUAGE_TSX.into()
51}
52
53fn parse_ts(source: &str, language: Language) -> ParseResult {
54 parse_with_tree_sitter(
55 source,
56 &language,
57 |root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
58 extract_calls,
59 |root, src, symbols| {
60 collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_ts_builtin_type)
61 },
62 )
63}
64
65fn extract_from_node(
66 node: tree_sitter::Node,
67 source: &str,
68 symbols: &mut Vec<ExtractedSymbol>,
69 imports: &mut Vec<ExtractedImport>,
70 parent_index: Option<usize>,
71) {
72 let kind = node.kind();
73
74 match kind {
75 "function_declaration" => {
76 if let Some(sym) = extract_function_or_component(&node, source, parent_index) {
77 symbols.push(sym);
78 return;
79 }
80 }
81 "class_declaration" => {
82 if let Some(sym) = extract_class(&node, source, parent_index) {
83 let idx = symbols.len();
84 symbols.push(sym);
85 if let Some(body) = node.child_by_field_name("body") {
87 for i in 0..body.child_count() {
88 if let Some(child) = body.child(i as u32) {
89 extract_from_node(child, source, symbols, imports, Some(idx));
90 }
91 }
92 }
93 return;
94 }
95 }
96 "method_definition" => {
97 if let Some(sym) = extract_method(&node, source, parent_index) {
98 symbols.push(sym);
99 return;
100 }
101 }
102 "interface_declaration" => {
103 if let Some(sym) =
104 extract_named_symbol(&node, source, SymbolKind::Interface, parent_index)
105 {
106 symbols.push(sym);
107 return;
108 }
109 }
110 "type_alias_declaration" => {
111 if let Some(sym) =
112 extract_named_symbol(&node, source, SymbolKind::TypeAlias, parent_index)
113 {
114 symbols.push(sym);
115 return;
116 }
117 }
118 "enum_declaration" => {
119 if let Some(sym) = extract_named_symbol(&node, source, SymbolKind::Enum, parent_index) {
120 symbols.push(sym);
121 return;
122 }
123 }
124 "export_statement" => {
125 for i in 0..node.child_count() {
126 if let Some(child) = node.child(i as u32) {
127 extract_from_node(child, source, symbols, imports, parent_index);
128 }
129 }
130 return;
131 }
132 "import_statement" => {
133 if let Some(imp) = extract_import(&node, source) {
134 imports.push(imp);
135 }
136 return;
137 }
138 "lexical_declaration" => {
139 let is_exported = node
140 .parent()
141 .is_some_and(|p| p.kind() == "export_statement");
142 for i in 0..node.child_count() {
143 if let Some(child) = node.child(i as u32) {
144 if child.kind() == "variable_declarator" {
145 if let Some(sym) =
146 extract_arrow_function_symbol(&child, source, parent_index, is_exported)
147 {
148 symbols.push(sym);
149 } else if is_exported {
150 if let Some(sym) = extract_variable_symbol(&child, source, parent_index)
151 {
152 symbols.push(sym);
153 }
154 }
155 }
156 }
157 }
158 return;
159 }
160 _ => {}
161 }
162
163 for i in 0..node.child_count() {
165 if let Some(child) = node.child(i as u32) {
166 extract_from_node(child, source, symbols, imports, parent_index);
167 }
168 }
169}
170
171fn extract_function_or_component(
172 node: &tree_sitter::Node,
173 source: &str,
174 parent_index: Option<usize>,
175) -> Option<ExtractedSymbol> {
176 let name_node = node.child_by_field_name("name")?;
177 let name = node_text(&name_node, source);
178 let is_component = is_pascal_case(&name) && body_returns_jsx(node, source);
179
180 let kind = if parent_index.is_some() {
181 SymbolKind::Method
182 } else if is_component {
183 SymbolKind::Component
184 } else {
185 SymbolKind::Function
186 };
187
188 Some(symbol_from_node(name, kind, node, source, parent_index))
189}
190
191fn extract_class(
192 node: &tree_sitter::Node,
193 source: &str,
194 parent_index: Option<usize>,
195) -> Option<ExtractedSymbol> {
196 let name_node = node.child_by_field_name("name")?;
197 let name = node_text(&name_node, source);
198
199 let kind = if extends_react_component(node, source) {
201 SymbolKind::Component
202 } else {
203 SymbolKind::Class
204 };
205
206 Some(symbol_from_node(name, kind, node, source, parent_index))
207}
208
209fn extract_method(
210 node: &tree_sitter::Node,
211 source: &str,
212 parent_index: Option<usize>,
213) -> Option<ExtractedSymbol> {
214 let name_node = node.child_by_field_name("name")?;
215 let name = node_text(&name_node, source);
216 Some(symbol_from_node(
217 name,
218 SymbolKind::Method,
219 node,
220 source,
221 parent_index,
222 ))
223}
224
225fn extract_named_symbol(
226 node: &tree_sitter::Node,
227 source: &str,
228 kind: SymbolKind,
229 parent_index: Option<usize>,
230) -> Option<ExtractedSymbol> {
231 let name_node = node.child_by_field_name("name")?;
232 let name = node_text(&name_node, source);
233 Some(symbol_from_node(name, kind, node, source, parent_index))
234}
235
236fn extract_variable_symbol(
237 declarator: &tree_sitter::Node,
238 source: &str,
239 parent_index: Option<usize>,
240) -> Option<ExtractedSymbol> {
241 let name_node = declarator.child_by_field_name("name")?;
242 let name = node_text(&name_node, source);
243
244 let parent = declarator.parent()?;
246 let top = parent.parent().unwrap_or(parent);
247
248 Some(symbol_from_node(
249 name,
250 SymbolKind::Variable,
251 &top,
252 source,
253 parent_index,
254 ))
255}
256
257fn extract_arrow_function_symbol(
258 declarator: &tree_sitter::Node,
259 source: &str,
260 parent_index: Option<usize>,
261 is_exported: bool,
262) -> Option<ExtractedSymbol> {
263 let name_node = declarator.child_by_field_name("name")?;
264 let name = node_text(&name_node, source);
265
266 let value = declarator.child_by_field_name("value")?;
267 if value.kind() != "arrow_function" {
268 return None;
269 }
270
271 if !is_exported {
272 return None;
273 }
274
275 let is_component = is_pascal_case(&name) && arrow_returns_jsx(&value, source);
276
277 let kind = if is_component {
278 SymbolKind::Component
279 } else {
280 SymbolKind::Function
281 };
282
283 let parent = declarator.parent()?;
285 let top = parent.parent().unwrap_or(parent);
286
287 Some(symbol_from_node(name, kind, &top, source, parent_index))
288}
289
290fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
291 let src_node = node.child_by_field_name("source")?;
292 let raw = node_text(&src_node, source);
293 let path = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
295
296 let imported_symbols: Vec<String> = (0..node.child_count())
299 .filter_map(|i| node.child(i as u32))
300 .flat_map(|child| match child.kind() {
301 "import_clause" => collect_import_names(&child, source),
302 "identifier" => vec![node_text(&child, source)],
303 _ => Vec::new(),
304 })
305 .collect();
306
307 Some(ExtractedImport {
308 path,
309 imported_symbols,
310 })
311}
312
313fn collect_import_names(node: &tree_sitter::Node, source: &str) -> Vec<String> {
314 (0..node.child_count())
315 .filter_map(|i| node.child(i as u32))
316 .flat_map(|child| match child.kind() {
317 "identifier" => vec![node_text(&child, source)],
318 "named_imports" => (0..child.child_count())
319 .filter_map(|j| child.child(j as u32))
320 .filter(|spec| spec.kind() == "import_specifier")
321 .filter_map(|spec| {
322 let local = spec
323 .child_by_field_name("alias")
324 .or_else(|| spec.child_by_field_name("name"))
325 .unwrap_or(spec);
326 (local.kind() == "identifier").then(|| node_text(&local, source))
327 })
328 .collect(),
329 "namespace_import" => (0..child.child_count())
330 .filter_map(|k| child.child(k as u32))
331 .filter(|ns_child| ns_child.kind() == "identifier")
332 .map(|ns_child| node_text(&ns_child, source))
333 .collect(),
334 _ => collect_import_names(&child, source),
335 })
336 .collect()
337}
338
339fn extract_calls(
342 root: tree_sitter::Node,
343 source: &str,
344 symbols: &[ExtractedSymbol],
345) -> Vec<ExtractedCall> {
346 let mut calls = Vec::new();
347 collect_calls(root, source, symbols, &mut calls);
348 calls
349}
350
351fn collect_calls(
352 node: tree_sitter::Node,
353 source: &str,
354 symbols: &[ExtractedSymbol],
355 calls: &mut Vec<ExtractedCall>,
356) {
357 match node.kind() {
358 "call_expression" => {
359 if let Some(func_node) = node.child_by_field_name("function") {
360 let callee_text = node_text(&func_node, source);
361 let callee_name = callee_text.rsplit('.').next().unwrap_or(&callee_text);
362 let call_line = node.start_position().row + 1;
363
364 if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
365 calls.push(ExtractedCall {
366 caller_name: caller.to_string(),
367 callee_name: callee_name.to_string(),
368 line: call_line,
369 });
370 }
371 }
372 }
373 "new_expression" => {
374 if let Some(constructor) = node.child_by_field_name("constructor") {
375 let callee_name = node_text(&constructor, source);
376 let call_line = node.start_position().row + 1;
377
378 if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
379 calls.push(ExtractedCall {
380 caller_name: caller.to_string(),
381 callee_name: callee_name.to_string(),
382 line: call_line,
383 });
384 }
385 }
386 }
387 "jsx_opening_element" | "jsx_self_closing_element" => {
389 for ci in 0..node.child_count() {
391 if let Some(tag) = node.child(ci as u32) {
392 if tag.kind() == "identifier" || tag.kind() == "member_expression" {
393 let tag_name = node_text(&tag, source);
394 if is_pascal_case(&tag_name) {
396 let line = node.start_position().row + 1;
397 if let Some(caller) = find_enclosing_symbol(symbols, line) {
398 calls.push(ExtractedCall {
399 caller_name: caller.to_string(),
400 callee_name: tag_name,
401 line,
402 });
403 }
404 }
405 break;
406 }
407 }
408 }
409 }
410 _ => {}
411 }
412
413 for i in 0..node.child_count() {
414 if let Some(child) = node.child(i as u32) {
415 collect_calls(child, source, symbols, calls);
416 }
417 }
418}
419
420fn is_pascal_case(name: &str) -> bool {
423 name.starts_with(|c: char| c.is_ascii_uppercase())
424}
425
426fn is_ts_builtin_type(name: &str) -> bool {
427 matches!(
428 name,
429 "string"
430 | "number"
431 | "boolean"
432 | "void"
433 | "any"
434 | "unknown"
435 | "never"
436 | "null"
437 | "undefined"
438 | "object"
439 | "symbol"
440 | "bigint"
441 | "Array"
442 | "Promise"
443 | "Map"
444 | "Set"
445 | "Record"
446 | "Partial"
447 | "Required"
448 | "Readonly"
449 | "Pick"
450 | "Omit"
451 | "Exclude"
452 | "Extract"
453 | "ReturnType"
454 | "Parameters"
455 )
456}
457
458fn body_returns_jsx(func_node: &tree_sitter::Node, source: &str) -> bool {
459 if let Some(body) = func_node.child_by_field_name("body") {
460 return contains_jsx_return(&body, source);
461 }
462 false
463}
464
465fn arrow_returns_jsx(arrow: &tree_sitter::Node, source: &str) -> bool {
466 if let Some(body) = arrow.child_by_field_name("body") {
467 if is_jsx_node(&body) {
469 return true;
470 }
471 if body.kind() == "parenthesized_expression" {
473 for i in 0..body.child_count() {
474 if let Some(child) = body.child(i as u32) {
475 if is_jsx_node(&child) {
476 return true;
477 }
478 }
479 }
480 }
481 return contains_jsx_return(&body, source);
483 }
484 false
485}
486
487fn contains_jsx_return(node: &tree_sitter::Node, _source: &str) -> bool {
488 if node.kind() == "return_statement" {
489 for i in 0..node.child_count() {
490 if let Some(child) = node.child(i as u32) {
491 if is_jsx_node(&child) {
492 return true;
493 }
494 if child.kind() == "parenthesized_expression" {
496 for j in 0..child.child_count() {
497 if let Some(grandchild) = child.child(j as u32) {
498 if is_jsx_node(&grandchild) {
499 return true;
500 }
501 }
502 }
503 }
504 }
505 }
506 }
507 for i in 0..node.child_count() {
509 if let Some(child) = node.child(i as u32) {
510 if contains_jsx_return(&child, _source) {
511 return true;
512 }
513 }
514 }
515 false
516}
517
518fn is_jsx_node(node: &tree_sitter::Node) -> bool {
519 matches!(
520 node.kind(),
521 "jsx_element" | "jsx_self_closing_element" | "jsx_fragment"
522 )
523}
524
525fn extends_react_component(class_node: &tree_sitter::Node, source: &str) -> bool {
526 for i in 0..class_node.child_count() {
528 if let Some(child) = class_node.child(i as u32) {
529 if child.kind() == "class_heritage" {
530 let text = node_text(&child, source);
531 return text.contains("Component") || text.contains("PureComponent");
532 }
533 }
534 }
535 false
536}
537
538pub(crate) struct TypeScriptResolver {
544 tsconfig_paths: HashMap<String, Vec<String>>,
547 base_url: Option<String>,
549}
550
551impl TypeScriptResolver {
552 pub fn new(project_root: &Path) -> Self {
553 let (tsconfig_paths, base_url) = load_tsconfig_paths(project_root);
554 Self {
555 tsconfig_paths,
556 base_url,
557 }
558 }
559}
560
561impl LanguageResolver for TypeScriptResolver {
562 fn resolve_import(
563 &self,
564 import_path: &str,
565 source_file: &str,
566 project_root: &Path,
567 ) -> Option<PathBuf> {
568 if import_path.is_empty() {
569 return None;
570 }
571
572 if import_path.starts_with("./") || import_path.starts_with("../") {
574 let source_abs = project_root.join(source_file);
575 let source_dir = source_abs.parent()?;
576 let target = source_dir.join(import_path);
577 return resolve_ts_file(&target);
578 }
579
580 if let Some(resolved) = self.resolve_via_tsconfig(import_path, project_root) {
582 return Some(resolved);
583 }
584
585 None
587 }
588}
589
590impl TypeScriptResolver {
591 fn resolve_via_tsconfig(&self, import_path: &str, project_root: &Path) -> Option<PathBuf> {
592 let base = if let Some(ref base_url) = self.base_url {
593 project_root.join(base_url)
594 } else {
595 project_root.to_path_buf()
596 };
597
598 for (pattern, replacements) in &self.tsconfig_paths {
599 if let Some(matched) = match_tsconfig_pattern(pattern, import_path) {
600 for replacement in replacements {
601 let resolved_path = replacement.replace('*', &matched);
602 let target = base.join(&resolved_path);
603 if let Some(file) = resolve_ts_file(&target) {
604 return Some(file);
605 }
606 }
607 }
608 }
609
610 None
611 }
612}
613
614fn resolve_ts_file(target: &Path) -> Option<PathBuf> {
618 if target.exists() && target.is_file() {
620 return target.canonicalize().ok();
621 }
622
623 let ts = target.with_extension("ts");
625 if ts.exists() {
626 return ts.canonicalize().ok();
627 }
628
629 let tsx = target.with_extension("tsx");
631 if tsx.exists() {
632 return tsx.canonicalize().ok();
633 }
634
635 let index_ts = target.join("index.ts");
637 if index_ts.exists() {
638 return index_ts.canonicalize().ok();
639 }
640
641 let index_tsx = target.join("index.tsx");
643 if index_tsx.exists() {
644 return index_tsx.canonicalize().ok();
645 }
646
647 None
648}
649
650fn match_tsconfig_pattern(pattern: &str, import_path: &str) -> Option<String> {
654 if let Some(prefix) = pattern.strip_suffix('*') {
655 if let Some(rest) = import_path.strip_prefix(prefix) {
656 return Some(rest.to_string());
657 }
658 } else if pattern == import_path {
659 return Some(String::new());
660 }
661 None
662}
663
664fn load_tsconfig_paths(project_root: &Path) -> (HashMap<String, Vec<String>>, Option<String>) {
667 let tsconfig_path = project_root.join("tsconfig.json");
668 if !tsconfig_path.exists() {
669 return (HashMap::new(), None);
670 }
671
672 let content = match std::fs::read_to_string(&tsconfig_path) {
673 Ok(c) => c,
674 Err(e) => {
675 warn!("cannot read tsconfig.json: {}", e);
676 return (HashMap::new(), None);
677 }
678 };
679
680 let json: serde_json::Value = match serde_json::from_str(&content) {
681 Ok(v) => v,
682 Err(e) => {
683 warn!("cannot parse tsconfig.json: {}", e);
684 return (HashMap::new(), None);
685 }
686 };
687
688 let compiler_options = json.get("compilerOptions");
689 let base_url = compiler_options
690 .and_then(|co| co.get("baseUrl"))
691 .and_then(|v| v.as_str())
692 .map(String::from);
693
694 let mut paths = HashMap::new();
695 if let Some(paths_obj) = compiler_options
696 .and_then(|co| co.get("paths"))
697 .and_then(|v| v.as_object())
698 {
699 for (key, value) in paths_obj {
700 if let Some(arr) = value.as_array() {
701 let targets: Vec<String> = arr
702 .iter()
703 .filter_map(|v| v.as_str().map(String::from))
704 .collect();
705 if !targets.is_empty() {
706 paths.insert(key.clone(), targets);
707 }
708 }
709 }
710 }
711
712 (paths, base_url)
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use std::fs;
719 use std::path::PathBuf;
720
721 fn parse_ts_source(source: &str) -> ParseResult {
724 let parser = TypeScriptParser;
725 parser.parse(source, &PathBuf::from("test.ts"))
726 }
727
728 fn parse_tsx_source(source: &str) -> ParseResult {
729 let parser = TypeScriptParser;
730 parser.parse(source, &PathBuf::from("test.tsx"))
731 }
732
733 #[test]
736 fn test_parse_function() {
737 let result =
738 parse_ts_source("function greet(name: string): string { return `Hello ${name}`; }");
739 assert!(!result.has_errors);
740 assert_eq!(result.symbols.len(), 1);
741 assert_eq!(result.symbols[0].name, "greet");
742 assert_eq!(result.symbols[0].kind, SymbolKind::Function);
743 }
744
745 #[test]
746 fn test_parse_class_with_methods() {
747 let result = parse_ts_source(
748 r#"
749class UserService {
750 private name: string;
751 constructor(name: string) {
752 this.name = name;
753 }
754 getName(): string {
755 return this.name;
756 }
757}
758"#,
759 );
760 assert!(!result.has_errors);
761 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
762 assert!(names.contains(&"UserService"));
763 assert!(names.contains(&"constructor"));
764 assert!(names.contains(&"getName"));
765 let class_sym = result
766 .symbols
767 .iter()
768 .find(|s| s.name == "UserService")
769 .unwrap();
770 assert_eq!(class_sym.kind, SymbolKind::Class);
771 }
772
773 #[test]
774 fn test_parse_interface() {
775 let result = parse_ts_source("interface UserProps { name: string; age: number; }");
776 assert!(!result.has_errors);
777 assert_eq!(result.symbols.len(), 1);
778 assert_eq!(result.symbols[0].name, "UserProps");
779 assert_eq!(result.symbols[0].kind, SymbolKind::Interface);
780 }
781
782 #[test]
783 fn test_parse_type_alias() {
784 let result = parse_ts_source(
785 "type Result<T> = { ok: true; value: T } | { ok: false; error: string };",
786 );
787 assert!(!result.has_errors);
788 assert_eq!(result.symbols.len(), 1);
789 assert_eq!(result.symbols[0].name, "Result");
790 assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
791 }
792
793 #[test]
794 fn test_parse_enum() {
795 let result = parse_ts_source("enum Direction { Up, Down, Left, Right }");
796 assert!(!result.has_errors);
797 assert_eq!(result.symbols.len(), 1);
798 assert_eq!(result.symbols[0].name, "Direction");
799 assert_eq!(result.symbols[0].kind, SymbolKind::Enum);
800 }
801
802 #[test]
803 fn test_parse_imports() {
804 let result = parse_ts_source(
805 r#"
806import { useState, useEffect } from 'react';
807import UserService from './services/UserService';
808import type { Config } from '../config';
809"#,
810 );
811 assert_eq!(result.imports.len(), 3);
812 assert_eq!(result.imports[0].path, "react");
813 assert_eq!(result.imports[1].path, "./services/UserService");
814 assert_eq!(result.imports[2].path, "../config");
815 }
816
817 #[test]
818 fn test_parse_exported_arrow_component() {
819 let result = parse_tsx_source(
820 r#"
821export const Button = ({ label }: ButtonProps) => {
822 return <button>{label}</button>;
823};
824"#,
825 );
826 assert!(!result.has_errors, "parse errors");
827 let button = result.symbols.iter().find(|s| s.name == "Button");
828 assert!(button.is_some(), "Button not found in {:?}", result.symbols);
829 assert_eq!(button.unwrap().kind, SymbolKind::Component);
830 }
831
832 #[test]
833 fn test_parse_function_component() {
834 let result = parse_tsx_source(
835 r#"
836export function UserCard({ user }: { user: User }) {
837 return (
838 <div>
839 <span>{user.name}</span>
840 </div>
841 );
842}
843"#,
844 );
845 assert!(!result.has_errors);
846 let card = result.symbols.iter().find(|s| s.name == "UserCard");
847 assert!(card.is_some(), "UserCard not found in {:?}", result.symbols);
848 assert_eq!(card.unwrap().kind, SymbolKind::Component);
849 }
850
851 #[test]
852 fn test_parse_class_component() {
853 let result = parse_tsx_source(
854 r#"
855class Dashboard extends React.Component<Props> {
856 render() {
857 return <div>Dashboard</div>;
858 }
859}
860"#,
861 );
862 assert!(!result.has_errors);
863 let dash = result.symbols.iter().find(|s| s.name == "Dashboard");
864 assert!(
865 dash.is_some(),
866 "Dashboard not found in {:?}",
867 result.symbols
868 );
869 assert_eq!(dash.unwrap().kind, SymbolKind::Component);
870 }
871
872 #[test]
873 fn test_call_extraction() {
874 let result = parse_ts_source(
875 r#"
876function fetchData() { return fetch('/api'); }
877function main() {
878 const data = fetchData();
879 console.log(data);
880}
881"#,
882 );
883 let callee_names: Vec<&str> = result
884 .calls
885 .iter()
886 .map(|c| c.callee_name.as_str())
887 .collect();
888 assert!(
889 callee_names.contains(&"fetchData"),
890 "calls: {:?}",
891 result.calls
892 );
893 }
894
895 #[test]
896 fn test_new_expression() {
897 let result = parse_ts_source(
898 r#"
899class Foo {}
900function create() {
901 const f = new Foo();
902}
903"#,
904 );
905 let callee_names: Vec<&str> = result
906 .calls
907 .iter()
908 .map(|c| c.callee_name.as_str())
909 .collect();
910 assert!(callee_names.contains(&"Foo"), "calls: {:?}", result.calls);
911 }
912
913 #[test]
914 fn test_jsx_component_reference() {
915 let result = parse_tsx_source(
916 r#"
917export function App() {
918 return (
919 <div>
920 <UserAvatar />
921 <ProfileCard name="test" />
922 </div>
923 );
924}
925"#,
926 );
927 let callee_names: Vec<&str> = result
928 .calls
929 .iter()
930 .map(|c| c.callee_name.as_str())
931 .collect();
932 assert!(
933 callee_names.contains(&"UserAvatar"),
934 "calls: {:?}",
935 result.calls
936 );
937 assert!(
938 callee_names.contains(&"ProfileCard"),
939 "calls: {:?}",
940 result.calls
941 );
942 assert!(!callee_names.contains(&"div"));
944 }
945
946 #[test]
947 fn test_type_ref_extraction() {
948 let result = parse_ts_source(
949 r#"
950interface Config { port: number; }
951function createServer(config: Config): void {}
952"#,
953 );
954 let ref_types: Vec<&str> = result
955 .type_refs
956 .iter()
957 .map(|r| r.to_type.as_str())
958 .collect();
959 assert!(
960 ref_types.contains(&"Config"),
961 "type_refs: {:?}",
962 result.type_refs
963 );
964 }
965
966 #[test]
967 fn test_ts_builtin_types_filtered() {
968 let result = parse_ts_source(
969 r#"
970function test(name: string, age: number, active: boolean): void {}
971"#,
972 );
973 assert!(
975 result.type_refs.is_empty(),
976 "should filter builtins: {:?}",
977 result.type_refs
978 );
979 }
980
981 #[test]
982 fn test_empty_ts_file() {
983 let result = parse_ts_source("");
984 assert!(!result.has_errors);
985 assert_eq!(result.symbols.len(), 0);
986 }
987
988 #[test]
989 fn test_malformed_ts_file() {
990 let result = parse_ts_source("function broken( { }");
991 assert!(result.has_errors);
992 }
993
994 #[test]
995 fn test_import_extracts_named_symbols() {
996 let result = parse_ts_source("import { Foo, Bar } from './baz';");
997 assert_eq!(result.imports.len(), 1);
998 assert_eq!(result.imports[0].imported_symbols, vec!["Foo", "Bar"]);
999 }
1000
1001 #[test]
1002 fn test_import_extracts_default_symbol() {
1003 let result = parse_ts_source("import React from 'react';");
1004 assert_eq!(result.imports.len(), 1);
1005 assert_eq!(result.imports[0].imported_symbols, vec!["React"]);
1006 }
1007
1008 #[test]
1009 fn test_import_extracts_aliased_symbol() {
1010 let result = parse_ts_source("import { Foo as Bar } from './baz';");
1011 assert_eq!(result.imports.len(), 1);
1012 assert!(result.imports[0]
1014 .imported_symbols
1015 .contains(&"Bar".to_string()));
1016 }
1017
1018 #[test]
1019 fn test_import_namespace() {
1020 let result = parse_ts_source("import * as Utils from './utils';");
1021 assert_eq!(result.imports.len(), 1);
1022 assert_eq!(result.imports[0].imported_symbols, vec!["Utils"]);
1023 }
1024
1025 #[test]
1026 fn test_export_const_variable() {
1027 let result = parse_ts_source(
1028 r#"
1029import { compile } from 'zod-aot';
1030import { z } from 'zod';
1031
1032export const UserSchema = compile(
1033 z.object({ name: z.string(), age: z.number() }),
1034);
1035"#,
1036 );
1037 assert!(!result.has_errors);
1038 let sym = result.symbols.iter().find(|s| s.name == "UserSchema");
1039 assert!(
1040 sym.is_some(),
1041 "UserSchema not found in {:?}",
1042 result.symbols
1043 );
1044 assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
1045 }
1046
1047 #[test]
1048 fn test_export_const_simple_value() {
1049 let result = parse_ts_source("export const API_URL = 'https://api.example.com';");
1050 assert!(!result.has_errors);
1051 let sym = result.symbols.iter().find(|s| s.name == "API_URL");
1052 assert!(sym.is_some(), "API_URL not found in {:?}", result.symbols);
1053 assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
1054 }
1055
1056 #[test]
1057 fn test_export_const_object() {
1058 let result = parse_ts_source("export const config = { port: 3000, host: 'localhost' };");
1059 assert!(!result.has_errors);
1060 let sym = result.symbols.iter().find(|s| s.name == "config");
1061 assert!(sym.is_some(), "config not found in {:?}", result.symbols);
1062 assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
1063 }
1064
1065 #[test]
1066 fn test_non_exported_const_not_extracted() {
1067 let result = parse_ts_source("const internal = { port: 3000 };");
1068 assert!(!result.has_errors);
1069 let sym = result.symbols.iter().find(|s| s.name == "internal");
1070 assert!(sym.is_none(), "non-exported const should not be extracted");
1071 }
1072
1073 #[test]
1074 fn test_export_const_arrow_still_function() {
1075 let result = parse_ts_source("export const add = (a: number, b: number) => a + b;");
1076 assert!(!result.has_errors);
1077 let sym = result.symbols.iter().find(|s| s.name == "add");
1078 assert!(sym.is_some(), "add not found in {:?}", result.symbols);
1079 assert_eq!(sym.unwrap().kind, SymbolKind::Function);
1080 }
1081
1082 #[test]
1083 fn test_is_pascal_case_positive() {
1084 assert!(is_pascal_case("Button"));
1085 assert!(is_pascal_case("FooBar"));
1086 assert!(is_pascal_case("A"));
1087 assert!(is_pascal_case("UserAvatar"));
1088 }
1089
1090 #[test]
1091 fn test_is_pascal_case_negative() {
1092 assert!(!is_pascal_case("button"));
1093 assert!(!is_pascal_case(""));
1094 assert!(!is_pascal_case("aButton"));
1095 assert!(!is_pascal_case("_Private"));
1096 }
1097
1098 #[test]
1099 fn test_is_ts_builtin_type_positive() {
1100 assert!(is_ts_builtin_type("string"));
1101 assert!(is_ts_builtin_type("number"));
1102 assert!(is_ts_builtin_type("boolean"));
1103 assert!(is_ts_builtin_type("Array"));
1104 assert!(is_ts_builtin_type("Promise"));
1105 assert!(is_ts_builtin_type("Record"));
1106 assert!(is_ts_builtin_type("ReturnType"));
1107 }
1108
1109 #[test]
1110 fn test_is_ts_builtin_type_negative() {
1111 assert!(!is_ts_builtin_type("MyType"));
1112 assert!(!is_ts_builtin_type("Config"));
1113 assert!(!is_ts_builtin_type("UserService"));
1114 assert!(!is_ts_builtin_type(""));
1115 }
1116
1117 #[test]
1118 fn test_extends_react_component_positive() {
1119 let result = parse_tsx_source(
1120 r#"
1121class MyComponent extends React.Component<Props> {
1122 render() { return <div/>; }
1123}
1124"#,
1125 );
1126 let comp = result.symbols.iter().find(|s| s.name == "MyComponent");
1127 assert!(comp.is_some());
1128 assert_eq!(comp.unwrap().kind, SymbolKind::Component);
1129 }
1130
1131 #[test]
1132 fn test_extends_pure_component() {
1133 let result = parse_tsx_source(
1134 r#"
1135class MyPure extends React.PureComponent<Props> {
1136 render() { return <div/>; }
1137}
1138"#,
1139 );
1140 let comp = result.symbols.iter().find(|s| s.name == "MyPure");
1141 assert!(comp.is_some());
1142 assert_eq!(comp.unwrap().kind, SymbolKind::Component);
1143 }
1144
1145 #[test]
1146 fn test_class_without_heritage_is_not_component() {
1147 let result = parse_tsx_source(
1148 r#"
1149class UserService {
1150 fetch() { return null; }
1151}
1152"#,
1153 );
1154 let svc = result.symbols.iter().find(|s| s.name == "UserService");
1155 assert!(svc.is_some());
1156 assert_eq!(svc.unwrap().kind, SymbolKind::Class);
1157 }
1158
1159 #[test]
1160 fn test_export_const_calls_captured() {
1161 let result = parse_ts_source(
1162 r#"
1163export const Schema = compile(z.object({ name: z.string() }));
1164"#,
1165 );
1166 assert!(!result.has_errors);
1167 let sym = result.symbols.iter().find(|s| s.name == "Schema");
1168 assert!(sym.is_some(), "Schema not found");
1169 let compile_calls: Vec<_> = result
1171 .calls
1172 .iter()
1173 .filter(|c| c.callee_name == "compile")
1174 .collect();
1175 assert!(
1176 !compile_calls.is_empty(),
1177 "compile() call should be captured, calls: {:?}",
1178 result.calls
1179 );
1180 assert_eq!(compile_calls[0].caller_name, "Schema");
1181 }
1182
1183 #[test]
1186 fn test_resolve_relative_ts() {
1187 let tmp = tempfile::tempdir().unwrap();
1188 let src = tmp.path().join("src");
1189 fs::create_dir_all(&src).unwrap();
1190 fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();
1191
1192 let resolver = TypeScriptResolver::new(tmp.path());
1193 let result = resolver.resolve_import("./utils", "src/main.ts", tmp.path());
1194 assert!(result.is_some(), "should resolve ./utils");
1195 assert!(result.unwrap().ends_with("utils.ts"));
1196 }
1197
1198 #[test]
1199 fn test_resolve_relative_tsx() {
1200 let tmp = tempfile::tempdir().unwrap();
1201 let src = tmp.path().join("src");
1202 fs::create_dir_all(&src).unwrap();
1203 fs::write(
1204 src.join("Button.tsx"),
1205 "export const Button = () => <button/>;",
1206 )
1207 .unwrap();
1208
1209 let resolver = TypeScriptResolver::new(tmp.path());
1210 let result = resolver.resolve_import("./Button", "src/App.tsx", tmp.path());
1211 assert!(result.is_some(), "should resolve ./Button");
1212 assert!(result.unwrap().ends_with("Button.tsx"));
1213 }
1214
1215 #[test]
1216 fn test_resolve_index_ts() {
1217 let tmp = tempfile::tempdir().unwrap();
1218 let components = tmp.path().join("src").join("components");
1219 fs::create_dir_all(&components).unwrap();
1220 fs::write(components.join("index.ts"), "export * from './Button';").unwrap();
1221
1222 let resolver = TypeScriptResolver::new(tmp.path());
1223 let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
1224 assert!(result.is_some(), "should resolve ./components to index.ts");
1225 assert!(result.unwrap().ends_with("index.ts"));
1226 }
1227
1228 #[test]
1229 fn test_resolve_parent_relative() {
1230 let tmp = tempfile::tempdir().unwrap();
1231 let src = tmp.path().join("src");
1232 fs::create_dir_all(src.join("components")).unwrap();
1233 fs::write(src.join("utils.ts"), "").unwrap();
1234
1235 let resolver = TypeScriptResolver::new(tmp.path());
1236 let result = resolver.resolve_import("../utils", "src/components/Button.tsx", tmp.path());
1237 assert!(result.is_some(), "should resolve ../utils");
1238 assert!(result.unwrap().ends_with("utils.ts"));
1239 }
1240
1241 #[test]
1242 fn test_resolve_tsconfig_paths() {
1243 let tmp = tempfile::tempdir().unwrap();
1244 let src = tmp.path().join("src");
1245 fs::create_dir_all(src.join("components")).unwrap();
1246 fs::write(src.join("components").join("Button.tsx"), "").unwrap();
1247
1248 fs::write(
1249 tmp.path().join("tsconfig.json"),
1250 r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }"#,
1251 )
1252 .unwrap();
1253
1254 let resolver = TypeScriptResolver::new(tmp.path());
1255 let result = resolver.resolve_import("@/components/Button", "src/App.tsx", tmp.path());
1256 assert!(result.is_some(), "should resolve @/components/Button");
1257 assert!(result.unwrap().ends_with("Button.tsx"));
1258 }
1259
1260 #[test]
1261 fn test_resolve_external_package_returns_none() {
1262 let tmp = tempfile::tempdir().unwrap();
1263 let resolver = TypeScriptResolver::new(tmp.path());
1264 assert_eq!(
1265 resolver.resolve_import("react", "src/App.tsx", tmp.path()),
1266 None
1267 );
1268 assert_eq!(
1269 resolver.resolve_import("@tanstack/react-query", "src/App.tsx", tmp.path()),
1270 None
1271 );
1272 }
1273
1274 #[test]
1275 fn test_resolve_no_tsconfig() {
1276 let tmp = tempfile::tempdir().unwrap();
1277 let src = tmp.path().join("src");
1278 fs::create_dir_all(&src).unwrap();
1279 fs::write(src.join("foo.ts"), "").unwrap();
1280
1281 let resolver = TypeScriptResolver::new(tmp.path());
1282 let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
1284 assert!(result.is_some());
1285 let result = resolver.resolve_import("@/foo", "src/main.ts", tmp.path());
1287 assert!(result.is_none());
1288 }
1289
1290 #[test]
1291 fn test_resolve_invalid_tsconfig() {
1292 let tmp = tempfile::tempdir().unwrap();
1293 fs::write(tmp.path().join("tsconfig.json"), "not json!!!").unwrap();
1294 let src = tmp.path().join("src");
1295 fs::create_dir_all(&src).unwrap();
1296 fs::write(src.join("foo.ts"), "").unwrap();
1297
1298 let resolver = TypeScriptResolver::new(tmp.path());
1300 let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
1301 assert!(result.is_some(), "relative import should still work");
1302 }
1303
1304 #[test]
1305 fn test_match_tsconfig_pattern() {
1306 assert_eq!(
1307 match_tsconfig_pattern("@/*", "@/foo/bar"),
1308 Some("foo/bar".to_string())
1309 );
1310 assert_eq!(match_tsconfig_pattern("@/*", "react"), None);
1311 assert_eq!(
1312 match_tsconfig_pattern("utils", "utils"),
1313 Some(String::new())
1314 );
1315 assert_eq!(match_tsconfig_pattern("utils", "utils/foo"), None);
1316 }
1317
1318 #[test]
1321 fn test_parse_arrow_component_expression_body_jsx() {
1322 let result = parse_tsx_source(
1324 r#"
1325export const Icon = () => <svg><path d="M0 0"/></svg>;
1326"#,
1327 );
1328 assert!(!result.has_errors);
1329 let icon = result.symbols.iter().find(|s| s.name == "Icon");
1330 assert!(icon.is_some(), "Icon not found in {:?}", result.symbols);
1331 assert_eq!(icon.unwrap().kind, SymbolKind::Component);
1332 }
1333
1334 #[test]
1335 fn test_parse_arrow_component_parenthesized_jsx() {
1336 let result = parse_tsx_source(
1338 r#"
1339export const Badge = () => (<span className="badge">Hi</span>);
1340"#,
1341 );
1342 assert!(!result.has_errors);
1343 let badge = result.symbols.iter().find(|s| s.name == "Badge");
1344 assert!(badge.is_some(), "Badge not found in {:?}", result.symbols);
1345 assert_eq!(badge.unwrap().kind, SymbolKind::Component);
1346 }
1347
1348 #[test]
1349 fn test_parse_function_component_parenthesized_return() {
1350 let result = parse_tsx_source(
1352 r#"
1353export function Layout() {
1354 return (
1355 <main>
1356 <h1>Title</h1>
1357 </main>
1358 );
1359}
1360"#,
1361 );
1362 assert!(!result.has_errors);
1363 let layout = result.symbols.iter().find(|s| s.name == "Layout");
1364 assert!(layout.is_some(), "Layout not found in {:?}", result.symbols);
1365 assert_eq!(layout.unwrap().kind, SymbolKind::Component);
1366 }
1367
1368 #[test]
1369 fn test_non_component_arrow_lowercase() {
1370 let result = parse_tsx_source(
1372 r#"
1373export const helper = () => <div/>;
1374"#,
1375 );
1376 assert!(!result.has_errors);
1377 let helper = result.symbols.iter().find(|s| s.name == "helper");
1378 assert!(helper.is_some(), "helper not found in {:?}", result.symbols);
1379 assert_eq!(helper.unwrap().kind, SymbolKind::Function);
1381 }
1382
1383 #[test]
1384 fn test_class_extends_non_component() {
1385 let result = parse_tsx_source(
1387 r#"
1388class MyService extends BaseService {
1389 fetch() { return null; }
1390}
1391"#,
1392 );
1393 assert!(!result.has_errors);
1394 let svc = result.symbols.iter().find(|s| s.name == "MyService");
1395 assert!(svc.is_some());
1396 assert_eq!(
1397 svc.unwrap().kind,
1398 SymbolKind::Class,
1399 "should be class, not component"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_resolve_index_tsx() {
1405 let tmp = tempfile::tempdir().unwrap();
1407 let components = tmp.path().join("src").join("components");
1408 fs::create_dir_all(&components).unwrap();
1409 fs::write(
1410 components.join("index.tsx"),
1411 "export const Foo = () => <div/>;",
1412 )
1413 .unwrap();
1414
1415 let resolver = TypeScriptResolver::new(tmp.path());
1416 let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
1417 assert!(result.is_some(), "should resolve ./components to index.tsx");
1418 assert!(result.unwrap().ends_with("index.tsx"));
1419 }
1420
1421 #[test]
1422 fn test_resolve_tsx_extension() {
1423 let tmp = tempfile::tempdir().unwrap();
1425 let src = tmp.path().join("src");
1426 fs::create_dir_all(&src).unwrap();
1427 fs::write(
1428 src.join("Widget.tsx"),
1429 "export const Widget = () => <div/>;",
1430 )
1431 .unwrap();
1432
1433 let resolver = TypeScriptResolver::new(tmp.path());
1434 let result = resolver.resolve_import("./Widget", "src/App.tsx", tmp.path());
1435 assert!(result.is_some(), "should resolve ./Widget to .tsx");
1436 assert!(result.unwrap().ends_with("Widget.tsx"));
1437 }
1438
1439 #[test]
1440 fn test_resolve_empty_import_path() {
1441 let tmp = tempfile::tempdir().unwrap();
1442 let resolver = TypeScriptResolver::new(tmp.path());
1443 let result = resolver.resolve_import("", "src/App.tsx", tmp.path());
1444 assert!(result.is_none(), "empty import path should return None");
1445 }
1446
1447 #[test]
1448 fn test_resolve_tsconfig_with_base_url_only() {
1449 let tmp = tempfile::tempdir().unwrap();
1451 let src = tmp.path().join("src");
1452 fs::create_dir_all(&src).unwrap();
1453 fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();
1454
1455 fs::write(
1456 tmp.path().join("tsconfig.json"),
1457 r#"{ "compilerOptions": { "baseUrl": "." } }"#,
1458 )
1459 .unwrap();
1460
1461 let resolver = TypeScriptResolver::new(tmp.path());
1462 let result = resolver.resolve_import("src/utils", "src/App.tsx", tmp.path());
1464 assert!(result.is_none());
1465 }
1466
1467 #[test]
1468 fn test_resolve_tsconfig_exact_match_pattern() {
1469 let tmp = tempfile::tempdir().unwrap();
1471 let src = tmp.path().join("src");
1472 fs::create_dir_all(&src).unwrap();
1473 fs::write(src.join("config.ts"), "export const config = {};").unwrap();
1474
1475 fs::write(
1476 tmp.path().join("tsconfig.json"),
1477 r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "config": ["./src/config"] } } }"#,
1478 )
1479 .unwrap();
1480
1481 let resolver = TypeScriptResolver::new(tmp.path());
1482 let result = resolver.resolve_import("config", "src/App.tsx", tmp.path());
1483 assert!(result.is_some(), "exact tsconfig pattern should resolve");
1484 assert!(result.unwrap().ends_with("config.ts"));
1485 }
1486
1487 #[test]
1488 fn test_parse_jsx_fragment() {
1489 let result = parse_tsx_source(
1491 r#"
1492export function Wrapper() {
1493 return (
1494 <>
1495 <ChildComp />
1496 </>
1497 );
1498}
1499"#,
1500 );
1501 assert!(!result.has_errors);
1502 let wrapper = result.symbols.iter().find(|s| s.name == "Wrapper");
1503 assert!(wrapper.is_some());
1504 assert_eq!(wrapper.unwrap().kind, SymbolKind::Component);
1505 let callee_names: Vec<&str> = result
1507 .calls
1508 .iter()
1509 .map(|c| c.callee_name.as_str())
1510 .collect();
1511 assert!(
1512 callee_names.contains(&"ChildComp"),
1513 "calls: {:?}",
1514 result.calls
1515 );
1516 }
1517
1518 #[test]
1519 fn test_parse_namespace_import() {
1520 let result = parse_ts_source("import * as Helpers from './helpers';");
1522 assert_eq!(result.imports.len(), 1);
1523 assert_eq!(result.imports[0].imported_symbols, vec!["Helpers"]);
1524 }
1525
1526 #[test]
1527 fn test_parse_default_and_named_imports() {
1528 let result = parse_ts_source("import React, { useState, useEffect } from 'react';");
1529 assert_eq!(result.imports.len(), 1);
1530 let names = &result.imports[0].imported_symbols;
1531 assert!(names.contains(&"React".to_string()), "imports: {:?}", names);
1532 assert!(
1533 names.contains(&"useState".to_string()),
1534 "imports: {:?}",
1535 names
1536 );
1537 assert!(
1538 names.contains(&"useEffect".to_string()),
1539 "imports: {:?}",
1540 names
1541 );
1542 }
1543
1544 #[test]
1545 fn test_load_tsconfig_no_paths_key() {
1546 let tmp = tempfile::tempdir().unwrap();
1548 fs::write(
1549 tmp.path().join("tsconfig.json"),
1550 r#"{ "compilerOptions": { "strict": true } }"#,
1551 )
1552 .unwrap();
1553
1554 let (paths, base_url) = load_tsconfig_paths(tmp.path());
1555 assert!(paths.is_empty());
1556 assert!(base_url.is_none());
1557 }
1558
1559 #[test]
1560 fn test_load_tsconfig_no_compiler_options() {
1561 let tmp = tempfile::tempdir().unwrap();
1563 fs::write(
1564 tmp.path().join("tsconfig.json"),
1565 r#"{ "include": ["src"] }"#,
1566 )
1567 .unwrap();
1568
1569 let (paths, base_url) = load_tsconfig_paths(tmp.path());
1570 assert!(paths.is_empty());
1571 assert!(base_url.is_none());
1572 }
1573
1574 #[test]
1575 fn test_arrow_function_non_exported_not_extracted() {
1576 let result = parse_ts_source("const helper = () => 42;");
1577 assert!(!result.has_errors);
1578 let sym = result.symbols.iter().find(|s| s.name == "helper");
1579 assert!(
1580 sym.is_none(),
1581 "non-exported arrow function should not be extracted"
1582 );
1583 }
1584
1585 #[test]
1586 fn test_lexical_declaration_non_arrow_value() {
1587 let result = parse_ts_source("const x = 42;");
1588 assert!(!result.has_errors);
1589 assert!(
1590 result.symbols.is_empty(),
1591 "non-exported non-arrow const should not be extracted"
1592 );
1593 }
1594
1595 #[test]
1596 fn test_method_in_class_is_method_kind() {
1597 let result = parse_ts_source(
1598 r#"
1599class Service {
1600 async fetch(): Promise<void> {}
1601 private handle(): void {}
1602}
1603"#,
1604 );
1605 assert!(!result.has_errors);
1606 let methods: Vec<&str> = result
1607 .symbols
1608 .iter()
1609 .filter(|s| s.kind == SymbolKind::Method)
1610 .map(|s| s.name.as_str())
1611 .collect();
1612 assert!(methods.contains(&"fetch"), "methods: {:?}", methods);
1613 assert!(methods.contains(&"handle"), "methods: {:?}", methods);
1614 }
1615
1616 #[test]
1617 fn test_body_returns_jsx_no_jsx() {
1618 let result = parse_ts_source("function greet(): string { return 'hello'; }");
1619 assert!(!result.has_errors);
1620 let greet = result.symbols.iter().find(|s| s.name == "greet").unwrap();
1621 assert_eq!(
1622 greet.kind,
1623 SymbolKind::Function,
1624 "non-jsx function should not be component"
1625 );
1626 }
1627
1628 #[test]
1629 fn test_is_jsx_node_fragment() {
1630 let result = parse_tsx_source(
1631 r#"
1632export function Empty() {
1633 return <></>;
1634}
1635"#,
1636 );
1637 assert!(!result.has_errors);
1638 let empty = result.symbols.iter().find(|s| s.name == "Empty");
1639 assert!(empty.is_some());
1640 assert_eq!(empty.unwrap().kind, SymbolKind::Component);
1641 }
1642
1643 #[test]
1644 fn test_arrow_no_jsx_is_function() {
1645 let result = parse_ts_source(
1646 r#"
1647export const compute = (x: number) => {
1648 return x * 2;
1649};
1650"#,
1651 );
1652 assert!(!result.has_errors);
1653 let compute = result.symbols.iter().find(|s| s.name == "compute");
1654 assert!(compute.is_some());
1655 assert_eq!(compute.unwrap().kind, SymbolKind::Function);
1656 }
1657
1658 #[test]
1659 fn test_is_ts_builtin_type_exhaustive() {
1660 for ty in &[
1661 "string",
1662 "number",
1663 "boolean",
1664 "void",
1665 "any",
1666 "unknown",
1667 "never",
1668 "null",
1669 "undefined",
1670 "object",
1671 "symbol",
1672 "bigint",
1673 "Array",
1674 "Promise",
1675 "Map",
1676 "Set",
1677 "Record",
1678 "Partial",
1679 "Required",
1680 "Readonly",
1681 "Pick",
1682 "Omit",
1683 "Exclude",
1684 "Extract",
1685 "ReturnType",
1686 "Parameters",
1687 ] {
1688 assert!(is_ts_builtin_type(ty), "'{}' should be builtin", ty);
1689 }
1690 }
1691
1692 #[test]
1693 fn test_import_side_effect_only() {
1694 let result = parse_ts_source("import './polyfills';");
1695 assert!(!result.has_errors);
1696 assert_eq!(result.imports.len(), 1);
1697 assert_eq!(result.imports[0].path, "./polyfills");
1698 assert!(
1699 result.imports[0].imported_symbols.is_empty(),
1700 "side-effect import should have no imported symbols"
1701 );
1702 }
1703
1704 #[test]
1705 fn test_export_statement_wraps_children() {
1706 let result = parse_ts_source("export function exported() { return 1; }");
1707 assert!(!result.has_errors);
1708 let sym = result.symbols.iter().find(|s| s.name == "exported");
1709 assert!(sym.is_some(), "exported function should be found");
1710 assert_eq!(sym.unwrap().kind, SymbolKind::Function);
1711 }
1712
1713 #[test]
1714 fn test_tsconfig_paths_with_multiple_replacements() {
1715 let tmp = tempfile::tempdir().unwrap();
1716 let src = tmp.path().join("src");
1717 let lib = tmp.path().join("lib");
1718 fs::create_dir_all(&src).unwrap();
1719 fs::create_dir_all(&lib).unwrap();
1720 fs::write(lib.join("utils.ts"), "export function foo() {}").unwrap();
1721
1722 fs::write(
1723 tmp.path().join("tsconfig.json"),
1724 r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*", "./lib/*"] } } }"#,
1725 )
1726 .unwrap();
1727
1728 let resolver = TypeScriptResolver::new(tmp.path());
1729 let result = resolver.resolve_import("@/utils", "src/App.tsx", tmp.path());
1730 assert!(
1731 result.is_some(),
1732 "should resolve via second path replacement"
1733 );
1734 assert!(result.unwrap().ends_with("utils.ts"));
1735 }
1736
1737 #[test]
1738 fn test_resolve_direct_file_with_extension() {
1739 let tmp = tempfile::tempdir().unwrap();
1740 let src = tmp.path().join("src");
1741 fs::create_dir_all(&src).unwrap();
1742 fs::write(src.join("data.json"), r#"{"key": "value"}"#).unwrap();
1743
1744 let target = src.join("data.json");
1745 let result = resolve_ts_file(&target);
1746 assert!(result.is_some(), "direct file should resolve");
1747 }
1748
1749 #[test]
1750 fn test_resolve_ts_file_nonexistent() {
1751 let tmp = tempfile::tempdir().unwrap();
1752 let target = tmp.path().join("nonexistent");
1753 let result = resolve_ts_file(&target);
1754 assert!(result.is_none(), "nonexistent path should return None");
1755 }
1756
1757 #[test]
1760 fn test_parse_getter_setter() {
1761 let result = parse_ts_source(
1762 r#"
1763class Config {
1764 private _name: string = '';
1765 get name(): string { return this._name; }
1766 set name(v: string) { this._name = v; }
1767}
1768"#,
1769 );
1770 assert!(!result.has_errors);
1771 let methods: Vec<&str> = result
1772 .symbols
1773 .iter()
1774 .filter(|s| s.kind == SymbolKind::Method)
1775 .map(|s| s.name.as_str())
1776 .collect();
1777 assert!(
1779 methods.iter().filter(|&&n| n == "name").count() >= 2,
1780 "getter and setter should both appear: {:?}",
1781 methods
1782 );
1783 }
1784
1785 #[test]
1786 fn test_parse_static_method() {
1787 let result = parse_ts_source(
1788 r#"
1789class Factory {
1790 static create(): Factory { return new Factory(); }
1791 build(): void {}
1792}
1793"#,
1794 );
1795 assert!(!result.has_errors);
1796 let methods: Vec<&str> = result
1797 .symbols
1798 .iter()
1799 .filter(|s| s.kind == SymbolKind::Method)
1800 .map(|s| s.name.as_str())
1801 .collect();
1802 assert!(methods.contains(&"create"), "static method: {:?}", methods);
1803 assert!(methods.contains(&"build"), "instance method: {:?}", methods);
1804 }
1805
1806 #[test]
1807 fn test_parse_abstract_class_not_extracted() {
1808 let result = parse_ts_source(
1812 r#"
1813abstract class BaseHandler {
1814 abstract handle(): void;
1815 log(): void { console.log('log'); }
1816}
1817"#,
1818 );
1819 assert!(!result.has_errors);
1820 let class = result.symbols.iter().find(|s| s.name == "BaseHandler");
1821 assert!(
1822 class.is_none(),
1823 "abstract class is not extracted by current parser: {:?}",
1824 result.symbols
1825 );
1826 }
1827
1828 #[test]
1829 fn test_parse_declare_function() {
1830 let result = parse_ts_source("declare function readFile(path: string): string;");
1832 assert!(!result.has_errors);
1833 }
1834
1835 #[test]
1836 fn test_parse_declare_module() {
1837 let result = parse_ts_source(
1838 r#"
1839declare module 'my-lib' {
1840 export function init(): void;
1841}
1842"#,
1843 );
1844 assert!(!result.has_errors);
1845 }
1846
1847 #[test]
1848 fn test_type_ref_union_types() {
1849 let result = parse_ts_source(
1850 r#"
1851interface Config { port: number; }
1852interface Options { timeout: number; }
1853function create(input: Config | Options): void {}
1854"#,
1855 );
1856 let ref_types: Vec<&str> = result
1857 .type_refs
1858 .iter()
1859 .map(|r| r.to_type.as_str())
1860 .collect();
1861 assert!(
1862 ref_types.contains(&"Config"),
1863 "union type Config: {:?}",
1864 ref_types
1865 );
1866 assert!(
1867 ref_types.contains(&"Options"),
1868 "union type Options: {:?}",
1869 ref_types
1870 );
1871 }
1872
1873 #[test]
1874 fn test_type_ref_intersection_types() {
1875 let result = parse_ts_source(
1876 r#"
1877interface Base { id: number; }
1878interface Named { name: string; }
1879function process(item: Base & Named): void {}
1880"#,
1881 );
1882 let ref_types: Vec<&str> = result
1883 .type_refs
1884 .iter()
1885 .map(|r| r.to_type.as_str())
1886 .collect();
1887 assert!(
1888 ref_types.contains(&"Base"),
1889 "intersection type: {:?}",
1890 ref_types
1891 );
1892 assert!(
1893 ref_types.contains(&"Named"),
1894 "intersection type: {:?}",
1895 ref_types
1896 );
1897 }
1898
1899 #[test]
1900 fn test_export_default_function() {
1901 let result = parse_ts_source("export default function handler() { return 1; }");
1902 assert!(!result.has_errors);
1903 let sym = result.symbols.iter().find(|s| s.name == "handler");
1904 assert!(sym.is_some(), "export default function should be extracted");
1905 }
1906
1907 #[test]
1908 fn test_multiple_classes_with_methods() {
1909 let result = parse_ts_source(
1910 r#"
1911class A {
1912 methodA(): void {}
1913}
1914class B {
1915 methodB(): void {}
1916}
1917"#,
1918 );
1919 assert!(!result.has_errors);
1920 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1921 assert!(names.contains(&"A"));
1922 assert!(names.contains(&"methodA"));
1923 assert!(names.contains(&"B"));
1924 assert!(names.contains(&"methodB"));
1925 let a_idx = result.symbols.iter().position(|s| s.name == "A").unwrap();
1927 let method_a = result.symbols.iter().find(|s| s.name == "methodA").unwrap();
1928 assert_eq!(method_a.parent_index, Some(a_idx));
1929 }
1930
1931 #[test]
1932 fn test_conditional_jsx_return() {
1933 let result = parse_tsx_source(
1934 r#"
1935export function Toggle({ on }: { on: boolean }) {
1936 if (on) {
1937 return <span>On</span>;
1938 }
1939 return <span>Off</span>;
1940}
1941"#,
1942 );
1943 assert!(!result.has_errors);
1944 let toggle = result.symbols.iter().find(|s| s.name == "Toggle").unwrap();
1945 assert_eq!(toggle.kind, SymbolKind::Component);
1946 }
1947
1948 #[test]
1949 fn test_arrow_component_with_block_body() {
1950 let result = parse_tsx_source(
1951 r#"
1952export const Card = ({ title }: { title: string }) => {
1953 const formatted = title.toUpperCase();
1954 return <div>{formatted}</div>;
1955};
1956"#,
1957 );
1958 assert!(!result.has_errors);
1959 let card = result.symbols.iter().find(|s| s.name == "Card").unwrap();
1960 assert_eq!(card.kind, SymbolKind::Component);
1961 }
1962
1963 #[test]
1964 fn test_import_type_only() {
1965 let result = parse_ts_source("import type { Config, Options } from './types';");
1966 assert!(!result.has_errors);
1967 assert_eq!(result.imports.len(), 1);
1968 assert_eq!(result.imports[0].path, "./types");
1969 assert!(result.imports[0]
1970 .imported_symbols
1971 .contains(&"Config".to_string()));
1972 }
1973
1974 #[test]
1975 fn test_re_export() {
1976 let result = parse_ts_source("export { default as Button } from './Button';");
1977 assert!(!result.has_errors);
1978 }
1979
1980 #[test]
1981 fn test_language_parser_trait_name() {
1982 let parser = TypeScriptParser;
1983 assert_eq!(parser.language_name(), "typescript");
1984 }
1985
1986 #[test]
1987 fn test_language_parser_tsx_vs_ts() {
1988 let parser = TypeScriptParser;
1989 let ts_result = parser.parse("function foo() { return 1; }", &PathBuf::from("test.ts"));
1991 assert!(!ts_result.has_errors);
1992
1993 let tsx_result = parser.parse(
1995 "export function App() { return <div/>; }",
1996 &PathBuf::from("test.tsx"),
1997 );
1998 assert!(!tsx_result.has_errors);
1999 let app = tsx_result.symbols.iter().find(|s| s.name == "App").unwrap();
2000 assert_eq!(app.kind, SymbolKind::Component);
2001 }
2002
2003 #[test]
2004 fn test_match_tsconfig_pattern_no_wildcard() {
2005 assert_eq!(
2007 match_tsconfig_pattern("lodash", "lodash"),
2008 Some(String::new())
2009 );
2010 assert_eq!(match_tsconfig_pattern("lodash", "lodash/fp"), None);
2011 assert_eq!(match_tsconfig_pattern("lodash", "underscore"), None);
2012 }
2013
2014 #[test]
2015 fn test_calls_in_arrow_function() {
2016 let result = parse_ts_source(
2017 r#"
2018export const init = () => {
2019 setup();
2020 configure();
2021};
2022"#,
2023 );
2024 let callee_names: Vec<&str> = result
2025 .calls
2026 .iter()
2027 .map(|c| c.callee_name.as_str())
2028 .collect();
2029 assert!(callee_names.contains(&"setup"), "calls: {:?}", result.calls);
2030 assert!(
2031 callee_names.contains(&"configure"),
2032 "calls: {:?}",
2033 result.calls
2034 );
2035 }
2036}