use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::warn;
use tree_sitter::Language;
use astmap_core::SymbolKind;
use super::{
collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
LanguageResolver, LanguageSupport, ParseResult,
};
pub(super) fn lang() -> LanguageSupport {
LanguageSupport {
name: "typescript",
extensions: &["ts", "tsx"],
parser: &TypeScriptParser,
resolver_factory: |root| Box::new(TypeScriptResolver::new(root)),
config_files: &["tsconfig.json"],
sibling_fn: |_| None, }
}
pub(crate) struct TypeScriptParser;
impl LanguageParser for TypeScriptParser {
fn parse(&self, source: &str, file_path: &Path) -> ParseResult {
let language = if file_path.extension().is_some_and(|e| e == "tsx") {
tsx_language()
} else {
ts_language()
};
parse_ts(source, language)
}
fn language_name(&self) -> &str {
"typescript"
}
}
fn ts_language() -> Language {
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
}
fn tsx_language() -> Language {
tree_sitter_typescript::LANGUAGE_TSX.into()
}
fn parse_ts(source: &str, language: Language) -> ParseResult {
parse_with_tree_sitter(
source,
&language,
|root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
extract_calls,
|root, src, symbols| {
collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_ts_builtin_type)
},
)
}
fn extract_from_node(
node: tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_index: Option<usize>,
) {
let kind = node.kind();
match kind {
"function_declaration" => {
if let Some(sym) = extract_function_or_component(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"class_declaration" => {
if let Some(sym) = extract_class(&node, source, parent_index) {
let idx = symbols.len();
symbols.push(sym);
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i as u32) {
extract_from_node(child, source, symbols, imports, Some(idx));
}
}
}
return;
}
}
"method_definition" => {
if let Some(sym) = extract_method(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"interface_declaration" => {
if let Some(sym) =
extract_named_symbol(&node, source, SymbolKind::Interface, parent_index)
{
symbols.push(sym);
return;
}
}
"type_alias_declaration" => {
if let Some(sym) =
extract_named_symbol(&node, source, SymbolKind::TypeAlias, parent_index)
{
symbols.push(sym);
return;
}
}
"enum_declaration" => {
if let Some(sym) = extract_named_symbol(&node, source, SymbolKind::Enum, parent_index) {
symbols.push(sym);
return;
}
}
"export_statement" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, parent_index);
}
}
return;
}
"import_statement" => {
if let Some(imp) = extract_import(&node, source) {
imports.push(imp);
}
return;
}
"lexical_declaration" => {
let is_exported = node
.parent()
.is_some_and(|p| p.kind() == "export_statement");
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "variable_declarator" {
if let Some(sym) =
extract_arrow_function_symbol(&child, source, parent_index, is_exported)
{
symbols.push(sym);
} else if is_exported {
if let Some(sym) = extract_variable_symbol(&child, source, parent_index)
{
symbols.push(sym);
}
}
}
}
}
return;
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, parent_index);
}
}
}
fn extract_function_or_component(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let is_component = is_pascal_case(&name) && body_returns_jsx(node, source);
let kind = if parent_index.is_some() {
SymbolKind::Method
} else if is_component {
SymbolKind::Component
} else {
SymbolKind::Function
};
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_class(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let kind = if extends_react_component(node, source) {
SymbolKind::Component
} else {
SymbolKind::Class
};
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_method(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Method,
node,
source,
parent_index,
))
}
fn extract_named_symbol(
node: &tree_sitter::Node,
source: &str,
kind: SymbolKind,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_variable_symbol(
declarator: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = declarator.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let parent = declarator.parent()?;
let top = parent.parent().unwrap_or(parent);
Some(symbol_from_node(
name,
SymbolKind::Variable,
&top,
source,
parent_index,
))
}
fn extract_arrow_function_symbol(
declarator: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
is_exported: bool,
) -> Option<ExtractedSymbol> {
let name_node = declarator.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let value = declarator.child_by_field_name("value")?;
if value.kind() != "arrow_function" {
return None;
}
if !is_exported {
return None;
}
let is_component = is_pascal_case(&name) && arrow_returns_jsx(&value, source);
let kind = if is_component {
SymbolKind::Component
} else {
SymbolKind::Function
};
let parent = declarator.parent()?;
let top = parent.parent().unwrap_or(parent);
Some(symbol_from_node(name, kind, &top, source, parent_index))
}
fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
let src_node = node.child_by_field_name("source")?;
let raw = node_text(&src_node, source);
let path = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
let imported_symbols: Vec<String> = (0..node.child_count())
.filter_map(|i| node.child(i as u32))
.flat_map(|child| match child.kind() {
"import_clause" => collect_import_names(&child, source),
"identifier" => vec![node_text(&child, source)],
_ => Vec::new(),
})
.collect();
Some(ExtractedImport {
path,
imported_symbols,
})
}
fn collect_import_names(node: &tree_sitter::Node, source: &str) -> Vec<String> {
(0..node.child_count())
.filter_map(|i| node.child(i as u32))
.flat_map(|child| match child.kind() {
"identifier" => vec![node_text(&child, source)],
"named_imports" => (0..child.child_count())
.filter_map(|j| child.child(j as u32))
.filter(|spec| spec.kind() == "import_specifier")
.filter_map(|spec| {
let local = spec
.child_by_field_name("alias")
.or_else(|| spec.child_by_field_name("name"))
.unwrap_or(spec);
(local.kind() == "identifier").then(|| node_text(&local, source))
})
.collect(),
"namespace_import" => (0..child.child_count())
.filter_map(|k| child.child(k as u32))
.filter(|ns_child| ns_child.kind() == "identifier")
.map(|ns_child| node_text(&ns_child, source))
.collect(),
_ => collect_import_names(&child, source),
})
.collect()
}
fn extract_calls(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
) -> Vec<ExtractedCall> {
let mut calls = Vec::new();
collect_calls(root, source, symbols, &mut calls);
calls
}
fn collect_calls(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
calls: &mut Vec<ExtractedCall>,
) {
match node.kind() {
"call_expression" => {
if let Some(func_node) = node.child_by_field_name("function") {
let callee_text = node_text(&func_node, source);
let callee_name = callee_text.rsplit('.').next().unwrap_or(&callee_text);
let call_line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name: callee_name.to_string(),
line: call_line,
});
}
}
}
"new_expression" => {
if let Some(constructor) = node.child_by_field_name("constructor") {
let callee_name = node_text(&constructor, source);
let call_line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name: callee_name.to_string(),
line: call_line,
});
}
}
}
"jsx_opening_element" | "jsx_self_closing_element" => {
for ci in 0..node.child_count() {
if let Some(tag) = node.child(ci as u32) {
if tag.kind() == "identifier" || tag.kind() == "member_expression" {
let tag_name = node_text(&tag, source);
if is_pascal_case(&tag_name) {
let line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name: tag_name,
line,
});
}
}
break;
}
}
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_calls(child, source, symbols, calls);
}
}
}
fn is_pascal_case(name: &str) -> bool {
name.starts_with(|c: char| c.is_ascii_uppercase())
}
fn is_ts_builtin_type(name: &str) -> bool {
matches!(
name,
"string"
| "number"
| "boolean"
| "void"
| "any"
| "unknown"
| "never"
| "null"
| "undefined"
| "object"
| "symbol"
| "bigint"
| "Array"
| "Promise"
| "Map"
| "Set"
| "Record"
| "Partial"
| "Required"
| "Readonly"
| "Pick"
| "Omit"
| "Exclude"
| "Extract"
| "ReturnType"
| "Parameters"
)
}
fn body_returns_jsx(func_node: &tree_sitter::Node, source: &str) -> bool {
if let Some(body) = func_node.child_by_field_name("body") {
return contains_jsx_return(&body, source);
}
false
}
fn arrow_returns_jsx(arrow: &tree_sitter::Node, source: &str) -> bool {
if let Some(body) = arrow.child_by_field_name("body") {
if is_jsx_node(&body) {
return true;
}
if body.kind() == "parenthesized_expression" {
for i in 0..body.child_count() {
if let Some(child) = body.child(i as u32) {
if is_jsx_node(&child) {
return true;
}
}
}
}
return contains_jsx_return(&body, source);
}
false
}
fn contains_jsx_return(node: &tree_sitter::Node, _source: &str) -> bool {
if node.kind() == "return_statement" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if is_jsx_node(&child) {
return true;
}
if child.kind() == "parenthesized_expression" {
for j in 0..child.child_count() {
if let Some(grandchild) = child.child(j as u32) {
if is_jsx_node(&grandchild) {
return true;
}
}
}
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if contains_jsx_return(&child, _source) {
return true;
}
}
}
false
}
fn is_jsx_node(node: &tree_sitter::Node) -> bool {
matches!(
node.kind(),
"jsx_element" | "jsx_self_closing_element" | "jsx_fragment"
)
}
fn extends_react_component(class_node: &tree_sitter::Node, source: &str) -> bool {
for i in 0..class_node.child_count() {
if let Some(child) = class_node.child(i as u32) {
if child.kind() == "class_heritage" {
let text = node_text(&child, source);
return text.contains("Component") || text.contains("PureComponent");
}
}
}
false
}
pub(crate) struct TypeScriptResolver {
tsconfig_paths: HashMap<String, Vec<String>>,
base_url: Option<String>,
}
impl TypeScriptResolver {
pub fn new(project_root: &Path) -> Self {
let (tsconfig_paths, base_url) = load_tsconfig_paths(project_root);
Self {
tsconfig_paths,
base_url,
}
}
}
impl LanguageResolver for TypeScriptResolver {
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
project_root: &Path,
) -> Option<PathBuf> {
if import_path.is_empty() {
return None;
}
if import_path.starts_with("./") || import_path.starts_with("../") {
let source_abs = project_root.join(source_file);
let source_dir = source_abs.parent()?;
let target = source_dir.join(import_path);
return resolve_ts_file(&target);
}
if let Some(resolved) = self.resolve_via_tsconfig(import_path, project_root) {
return Some(resolved);
}
None
}
}
impl TypeScriptResolver {
fn resolve_via_tsconfig(&self, import_path: &str, project_root: &Path) -> Option<PathBuf> {
let base = if let Some(ref base_url) = self.base_url {
project_root.join(base_url)
} else {
project_root.to_path_buf()
};
for (pattern, replacements) in &self.tsconfig_paths {
if let Some(matched) = match_tsconfig_pattern(pattern, import_path) {
for replacement in replacements {
let resolved_path = replacement.replace('*', &matched);
let target = base.join(&resolved_path);
if let Some(file) = resolve_ts_file(&target) {
return Some(file);
}
}
}
}
None
}
}
fn resolve_ts_file(target: &Path) -> Option<PathBuf> {
if target.exists() && target.is_file() {
return target.canonicalize().ok();
}
let ts = target.with_extension("ts");
if ts.exists() {
return ts.canonicalize().ok();
}
let tsx = target.with_extension("tsx");
if tsx.exists() {
return tsx.canonicalize().ok();
}
let index_ts = target.join("index.ts");
if index_ts.exists() {
return index_ts.canonicalize().ok();
}
let index_tsx = target.join("index.tsx");
if index_tsx.exists() {
return index_tsx.canonicalize().ok();
}
None
}
fn match_tsconfig_pattern(pattern: &str, import_path: &str) -> Option<String> {
if let Some(prefix) = pattern.strip_suffix('*') {
if let Some(rest) = import_path.strip_prefix(prefix) {
return Some(rest.to_string());
}
} else if pattern == import_path {
return Some(String::new());
}
None
}
fn load_tsconfig_paths(project_root: &Path) -> (HashMap<String, Vec<String>>, Option<String>) {
let tsconfig_path = project_root.join("tsconfig.json");
if !tsconfig_path.exists() {
return (HashMap::new(), None);
}
let content = match std::fs::read_to_string(&tsconfig_path) {
Ok(c) => c,
Err(e) => {
warn!("cannot read tsconfig.json: {}", e);
return (HashMap::new(), None);
}
};
let json: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => {
warn!("cannot parse tsconfig.json: {}", e);
return (HashMap::new(), None);
}
};
let compiler_options = json.get("compilerOptions");
let base_url = compiler_options
.and_then(|co| co.get("baseUrl"))
.and_then(|v| v.as_str())
.map(String::from);
let mut paths = HashMap::new();
if let Some(paths_obj) = compiler_options
.and_then(|co| co.get("paths"))
.and_then(|v| v.as_object())
{
for (key, value) in paths_obj {
if let Some(arr) = value.as_array() {
let targets: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
if !targets.is_empty() {
paths.insert(key.clone(), targets);
}
}
}
}
(paths, base_url)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn parse_ts_source(source: &str) -> ParseResult {
let parser = TypeScriptParser;
parser.parse(source, &PathBuf::from("test.ts"))
}
fn parse_tsx_source(source: &str) -> ParseResult {
let parser = TypeScriptParser;
parser.parse(source, &PathBuf::from("test.tsx"))
}
#[test]
fn test_parse_function() {
let result =
parse_ts_source("function greet(name: string): string { return `Hello ${name}`; }");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "greet");
assert_eq!(result.symbols[0].kind, SymbolKind::Function);
}
#[test]
fn test_parse_class_with_methods() {
let result = parse_ts_source(
r#"
class UserService {
private name: string;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
}
"#,
);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"UserService"));
assert!(names.contains(&"constructor"));
assert!(names.contains(&"getName"));
let class_sym = result
.symbols
.iter()
.find(|s| s.name == "UserService")
.unwrap();
assert_eq!(class_sym.kind, SymbolKind::Class);
}
#[test]
fn test_parse_interface() {
let result = parse_ts_source("interface UserProps { name: string; age: number; }");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "UserProps");
assert_eq!(result.symbols[0].kind, SymbolKind::Interface);
}
#[test]
fn test_parse_type_alias() {
let result = parse_ts_source(
"type Result<T> = { ok: true; value: T } | { ok: false; error: string };",
);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Result");
assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
}
#[test]
fn test_parse_enum() {
let result = parse_ts_source("enum Direction { Up, Down, Left, Right }");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Direction");
assert_eq!(result.symbols[0].kind, SymbolKind::Enum);
}
#[test]
fn test_parse_imports() {
let result = parse_ts_source(
r#"
import { useState, useEffect } from 'react';
import UserService from './services/UserService';
import type { Config } from '../config';
"#,
);
assert_eq!(result.imports.len(), 3);
assert_eq!(result.imports[0].path, "react");
assert_eq!(result.imports[1].path, "./services/UserService");
assert_eq!(result.imports[2].path, "../config");
}
#[test]
fn test_parse_exported_arrow_component() {
let result = parse_tsx_source(
r#"
export const Button = ({ label }: ButtonProps) => {
return <button>{label}</button>;
};
"#,
);
assert!(!result.has_errors, "parse errors");
let button = result.symbols.iter().find(|s| s.name == "Button");
assert!(button.is_some(), "Button not found in {:?}", result.symbols);
assert_eq!(button.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_parse_function_component() {
let result = parse_tsx_source(
r#"
export function UserCard({ user }: { user: User }) {
return (
<div>
<span>{user.name}</span>
</div>
);
}
"#,
);
assert!(!result.has_errors);
let card = result.symbols.iter().find(|s| s.name == "UserCard");
assert!(card.is_some(), "UserCard not found in {:?}", result.symbols);
assert_eq!(card.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_parse_class_component() {
let result = parse_tsx_source(
r#"
class Dashboard extends React.Component<Props> {
render() {
return <div>Dashboard</div>;
}
}
"#,
);
assert!(!result.has_errors);
let dash = result.symbols.iter().find(|s| s.name == "Dashboard");
assert!(
dash.is_some(),
"Dashboard not found in {:?}",
result.symbols
);
assert_eq!(dash.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_call_extraction() {
let result = parse_ts_source(
r#"
function fetchData() { return fetch('/api'); }
function main() {
const data = fetchData();
console.log(data);
}
"#,
);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callee_names.contains(&"fetchData"),
"calls: {:?}",
result.calls
);
}
#[test]
fn test_new_expression() {
let result = parse_ts_source(
r#"
class Foo {}
function create() {
const f = new Foo();
}
"#,
);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"Foo"), "calls: {:?}", result.calls);
}
#[test]
fn test_jsx_component_reference() {
let result = parse_tsx_source(
r#"
export function App() {
return (
<div>
<UserAvatar />
<ProfileCard name="test" />
</div>
);
}
"#,
);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callee_names.contains(&"UserAvatar"),
"calls: {:?}",
result.calls
);
assert!(
callee_names.contains(&"ProfileCard"),
"calls: {:?}",
result.calls
);
assert!(!callee_names.contains(&"div"));
}
#[test]
fn test_type_ref_extraction() {
let result = parse_ts_source(
r#"
interface Config { port: number; }
function createServer(config: Config): void {}
"#,
);
let ref_types: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
ref_types.contains(&"Config"),
"type_refs: {:?}",
result.type_refs
);
}
#[test]
fn test_ts_builtin_types_filtered() {
let result = parse_ts_source(
r#"
function test(name: string, age: number, active: boolean): void {}
"#,
);
assert!(
result.type_refs.is_empty(),
"should filter builtins: {:?}",
result.type_refs
);
}
#[test]
fn test_empty_ts_file() {
let result = parse_ts_source("");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 0);
}
#[test]
fn test_malformed_ts_file() {
let result = parse_ts_source("function broken( { }");
assert!(result.has_errors);
}
#[test]
fn test_import_extracts_named_symbols() {
let result = parse_ts_source("import { Foo, Bar } from './baz';");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["Foo", "Bar"]);
}
#[test]
fn test_import_extracts_default_symbol() {
let result = parse_ts_source("import React from 'react';");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["React"]);
}
#[test]
fn test_import_extracts_aliased_symbol() {
let result = parse_ts_source("import { Foo as Bar } from './baz';");
assert_eq!(result.imports.len(), 1);
assert!(result.imports[0]
.imported_symbols
.contains(&"Bar".to_string()));
}
#[test]
fn test_import_namespace() {
let result = parse_ts_source("import * as Utils from './utils';");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["Utils"]);
}
#[test]
fn test_export_const_variable() {
let result = parse_ts_source(
r#"
import { compile } from 'zod-aot';
import { z } from 'zod';
export const UserSchema = compile(
z.object({ name: z.string(), age: z.number() }),
);
"#,
);
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "UserSchema");
assert!(
sym.is_some(),
"UserSchema not found in {:?}",
result.symbols
);
assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
}
#[test]
fn test_export_const_simple_value() {
let result = parse_ts_source("export const API_URL = 'https://api.example.com';");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "API_URL");
assert!(sym.is_some(), "API_URL not found in {:?}", result.symbols);
assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
}
#[test]
fn test_export_const_object() {
let result = parse_ts_source("export const config = { port: 3000, host: 'localhost' };");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "config");
assert!(sym.is_some(), "config not found in {:?}", result.symbols);
assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
}
#[test]
fn test_non_exported_const_not_extracted() {
let result = parse_ts_source("const internal = { port: 3000 };");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "internal");
assert!(sym.is_none(), "non-exported const should not be extracted");
}
#[test]
fn test_export_const_arrow_still_function() {
let result = parse_ts_source("export const add = (a: number, b: number) => a + b;");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "add");
assert!(sym.is_some(), "add not found in {:?}", result.symbols);
assert_eq!(sym.unwrap().kind, SymbolKind::Function);
}
#[test]
fn test_is_pascal_case_positive() {
assert!(is_pascal_case("Button"));
assert!(is_pascal_case("FooBar"));
assert!(is_pascal_case("A"));
assert!(is_pascal_case("UserAvatar"));
}
#[test]
fn test_is_pascal_case_negative() {
assert!(!is_pascal_case("button"));
assert!(!is_pascal_case(""));
assert!(!is_pascal_case("aButton"));
assert!(!is_pascal_case("_Private"));
}
#[test]
fn test_is_ts_builtin_type_positive() {
assert!(is_ts_builtin_type("string"));
assert!(is_ts_builtin_type("number"));
assert!(is_ts_builtin_type("boolean"));
assert!(is_ts_builtin_type("Array"));
assert!(is_ts_builtin_type("Promise"));
assert!(is_ts_builtin_type("Record"));
assert!(is_ts_builtin_type("ReturnType"));
}
#[test]
fn test_is_ts_builtin_type_negative() {
assert!(!is_ts_builtin_type("MyType"));
assert!(!is_ts_builtin_type("Config"));
assert!(!is_ts_builtin_type("UserService"));
assert!(!is_ts_builtin_type(""));
}
#[test]
fn test_extends_react_component_positive() {
let result = parse_tsx_source(
r#"
class MyComponent extends React.Component<Props> {
render() { return <div/>; }
}
"#,
);
let comp = result.symbols.iter().find(|s| s.name == "MyComponent");
assert!(comp.is_some());
assert_eq!(comp.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_extends_pure_component() {
let result = parse_tsx_source(
r#"
class MyPure extends React.PureComponent<Props> {
render() { return <div/>; }
}
"#,
);
let comp = result.symbols.iter().find(|s| s.name == "MyPure");
assert!(comp.is_some());
assert_eq!(comp.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_class_without_heritage_is_not_component() {
let result = parse_tsx_source(
r#"
class UserService {
fetch() { return null; }
}
"#,
);
let svc = result.symbols.iter().find(|s| s.name == "UserService");
assert!(svc.is_some());
assert_eq!(svc.unwrap().kind, SymbolKind::Class);
}
#[test]
fn test_export_const_calls_captured() {
let result = parse_ts_source(
r#"
export const Schema = compile(z.object({ name: z.string() }));
"#,
);
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "Schema");
assert!(sym.is_some(), "Schema not found");
let compile_calls: Vec<_> = result
.calls
.iter()
.filter(|c| c.callee_name == "compile")
.collect();
assert!(
!compile_calls.is_empty(),
"compile() call should be captured, calls: {:?}",
result.calls
);
assert_eq!(compile_calls[0].caller_name, "Schema");
}
#[test]
fn test_resolve_relative_ts() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./utils", "src/main.ts", tmp.path());
assert!(result.is_some(), "should resolve ./utils");
assert!(result.unwrap().ends_with("utils.ts"));
}
#[test]
fn test_resolve_relative_tsx() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("Button.tsx"),
"export const Button = () => <button/>;",
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./Button", "src/App.tsx", tmp.path());
assert!(result.is_some(), "should resolve ./Button");
assert!(result.unwrap().ends_with("Button.tsx"));
}
#[test]
fn test_resolve_index_ts() {
let tmp = tempfile::tempdir().unwrap();
let components = tmp.path().join("src").join("components");
fs::create_dir_all(&components).unwrap();
fs::write(components.join("index.ts"), "export * from './Button';").unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
assert!(result.is_some(), "should resolve ./components to index.ts");
assert!(result.unwrap().ends_with("index.ts"));
}
#[test]
fn test_resolve_parent_relative() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("components")).unwrap();
fs::write(src.join("utils.ts"), "").unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("../utils", "src/components/Button.tsx", tmp.path());
assert!(result.is_some(), "should resolve ../utils");
assert!(result.unwrap().ends_with("utils.ts"));
}
#[test]
fn test_resolve_tsconfig_paths() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("components")).unwrap();
fs::write(src.join("components").join("Button.tsx"), "").unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }"#,
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("@/components/Button", "src/App.tsx", tmp.path());
assert!(result.is_some(), "should resolve @/components/Button");
assert!(result.unwrap().ends_with("Button.tsx"));
}
#[test]
fn test_resolve_external_package_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
assert_eq!(
resolver.resolve_import("react", "src/App.tsx", tmp.path()),
None
);
assert_eq!(
resolver.resolve_import("@tanstack/react-query", "src/App.tsx", tmp.path()),
None
);
}
#[test]
fn test_resolve_no_tsconfig() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("foo.ts"), "").unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
assert!(result.is_some());
let result = resolver.resolve_import("@/foo", "src/main.ts", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_resolve_invalid_tsconfig() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("tsconfig.json"), "not json!!!").unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("foo.ts"), "").unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
assert!(result.is_some(), "relative import should still work");
}
#[test]
fn test_match_tsconfig_pattern() {
assert_eq!(
match_tsconfig_pattern("@/*", "@/foo/bar"),
Some("foo/bar".to_string())
);
assert_eq!(match_tsconfig_pattern("@/*", "react"), None);
assert_eq!(
match_tsconfig_pattern("utils", "utils"),
Some(String::new())
);
assert_eq!(match_tsconfig_pattern("utils", "utils/foo"), None);
}
#[test]
fn test_parse_arrow_component_expression_body_jsx() {
let result = parse_tsx_source(
r#"
export const Icon = () => <svg><path d="M0 0"/></svg>;
"#,
);
assert!(!result.has_errors);
let icon = result.symbols.iter().find(|s| s.name == "Icon");
assert!(icon.is_some(), "Icon not found in {:?}", result.symbols);
assert_eq!(icon.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_parse_arrow_component_parenthesized_jsx() {
let result = parse_tsx_source(
r#"
export const Badge = () => (<span className="badge">Hi</span>);
"#,
);
assert!(!result.has_errors);
let badge = result.symbols.iter().find(|s| s.name == "Badge");
assert!(badge.is_some(), "Badge not found in {:?}", result.symbols);
assert_eq!(badge.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_parse_function_component_parenthesized_return() {
let result = parse_tsx_source(
r#"
export function Layout() {
return (
<main>
<h1>Title</h1>
</main>
);
}
"#,
);
assert!(!result.has_errors);
let layout = result.symbols.iter().find(|s| s.name == "Layout");
assert!(layout.is_some(), "Layout not found in {:?}", result.symbols);
assert_eq!(layout.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_non_component_arrow_lowercase() {
let result = parse_tsx_source(
r#"
export const helper = () => <div/>;
"#,
);
assert!(!result.has_errors);
let helper = result.symbols.iter().find(|s| s.name == "helper");
assert!(helper.is_some(), "helper not found in {:?}", result.symbols);
assert_eq!(helper.unwrap().kind, SymbolKind::Function);
}
#[test]
fn test_class_extends_non_component() {
let result = parse_tsx_source(
r#"
class MyService extends BaseService {
fetch() { return null; }
}
"#,
);
assert!(!result.has_errors);
let svc = result.symbols.iter().find(|s| s.name == "MyService");
assert!(svc.is_some());
assert_eq!(
svc.unwrap().kind,
SymbolKind::Class,
"should be class, not component"
);
}
#[test]
fn test_resolve_index_tsx() {
let tmp = tempfile::tempdir().unwrap();
let components = tmp.path().join("src").join("components");
fs::create_dir_all(&components).unwrap();
fs::write(
components.join("index.tsx"),
"export const Foo = () => <div/>;",
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
assert!(result.is_some(), "should resolve ./components to index.tsx");
assert!(result.unwrap().ends_with("index.tsx"));
}
#[test]
fn test_resolve_tsx_extension() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("Widget.tsx"),
"export const Widget = () => <div/>;",
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("./Widget", "src/App.tsx", tmp.path());
assert!(result.is_some(), "should resolve ./Widget to .tsx");
assert!(result.unwrap().ends_with("Widget.tsx"));
}
#[test]
fn test_resolve_empty_import_path() {
let tmp = tempfile::tempdir().unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("", "src/App.tsx", tmp.path());
assert!(result.is_none(), "empty import path should return None");
}
#[test]
fn test_resolve_tsconfig_with_base_url_only() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "compilerOptions": { "baseUrl": "." } }"#,
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("src/utils", "src/App.tsx", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_resolve_tsconfig_exact_match_pattern() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("config.ts"), "export const config = {};").unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "config": ["./src/config"] } } }"#,
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("config", "src/App.tsx", tmp.path());
assert!(result.is_some(), "exact tsconfig pattern should resolve");
assert!(result.unwrap().ends_with("config.ts"));
}
#[test]
fn test_parse_jsx_fragment() {
let result = parse_tsx_source(
r#"
export function Wrapper() {
return (
<>
<ChildComp />
</>
);
}
"#,
);
assert!(!result.has_errors);
let wrapper = result.symbols.iter().find(|s| s.name == "Wrapper");
assert!(wrapper.is_some());
assert_eq!(wrapper.unwrap().kind, SymbolKind::Component);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callee_names.contains(&"ChildComp"),
"calls: {:?}",
result.calls
);
}
#[test]
fn test_parse_namespace_import() {
let result = parse_ts_source("import * as Helpers from './helpers';");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["Helpers"]);
}
#[test]
fn test_parse_default_and_named_imports() {
let result = parse_ts_source("import React, { useState, useEffect } from 'react';");
assert_eq!(result.imports.len(), 1);
let names = &result.imports[0].imported_symbols;
assert!(names.contains(&"React".to_string()), "imports: {:?}", names);
assert!(
names.contains(&"useState".to_string()),
"imports: {:?}",
names
);
assert!(
names.contains(&"useEffect".to_string()),
"imports: {:?}",
names
);
}
#[test]
fn test_load_tsconfig_no_paths_key() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "compilerOptions": { "strict": true } }"#,
)
.unwrap();
let (paths, base_url) = load_tsconfig_paths(tmp.path());
assert!(paths.is_empty());
assert!(base_url.is_none());
}
#[test]
fn test_load_tsconfig_no_compiler_options() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "include": ["src"] }"#,
)
.unwrap();
let (paths, base_url) = load_tsconfig_paths(tmp.path());
assert!(paths.is_empty());
assert!(base_url.is_none());
}
#[test]
fn test_arrow_function_non_exported_not_extracted() {
let result = parse_ts_source("const helper = () => 42;");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "helper");
assert!(
sym.is_none(),
"non-exported arrow function should not be extracted"
);
}
#[test]
fn test_lexical_declaration_non_arrow_value() {
let result = parse_ts_source("const x = 42;");
assert!(!result.has_errors);
assert!(
result.symbols.is_empty(),
"non-exported non-arrow const should not be extracted"
);
}
#[test]
fn test_method_in_class_is_method_kind() {
let result = parse_ts_source(
r#"
class Service {
async fetch(): Promise<void> {}
private handle(): void {}
}
"#,
);
assert!(!result.has_errors);
let methods: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.map(|s| s.name.as_str())
.collect();
assert!(methods.contains(&"fetch"), "methods: {:?}", methods);
assert!(methods.contains(&"handle"), "methods: {:?}", methods);
}
#[test]
fn test_body_returns_jsx_no_jsx() {
let result = parse_ts_source("function greet(): string { return 'hello'; }");
assert!(!result.has_errors);
let greet = result.symbols.iter().find(|s| s.name == "greet").unwrap();
assert_eq!(
greet.kind,
SymbolKind::Function,
"non-jsx function should not be component"
);
}
#[test]
fn test_is_jsx_node_fragment() {
let result = parse_tsx_source(
r#"
export function Empty() {
return <></>;
}
"#,
);
assert!(!result.has_errors);
let empty = result.symbols.iter().find(|s| s.name == "Empty");
assert!(empty.is_some());
assert_eq!(empty.unwrap().kind, SymbolKind::Component);
}
#[test]
fn test_arrow_no_jsx_is_function() {
let result = parse_ts_source(
r#"
export const compute = (x: number) => {
return x * 2;
};
"#,
);
assert!(!result.has_errors);
let compute = result.symbols.iter().find(|s| s.name == "compute");
assert!(compute.is_some());
assert_eq!(compute.unwrap().kind, SymbolKind::Function);
}
#[test]
fn test_is_ts_builtin_type_exhaustive() {
for ty in &[
"string",
"number",
"boolean",
"void",
"any",
"unknown",
"never",
"null",
"undefined",
"object",
"symbol",
"bigint",
"Array",
"Promise",
"Map",
"Set",
"Record",
"Partial",
"Required",
"Readonly",
"Pick",
"Omit",
"Exclude",
"Extract",
"ReturnType",
"Parameters",
] {
assert!(is_ts_builtin_type(ty), "'{}' should be builtin", ty);
}
}
#[test]
fn test_import_side_effect_only() {
let result = parse_ts_source("import './polyfills';");
assert!(!result.has_errors);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "./polyfills");
assert!(
result.imports[0].imported_symbols.is_empty(),
"side-effect import should have no imported symbols"
);
}
#[test]
fn test_export_statement_wraps_children() {
let result = parse_ts_source("export function exported() { return 1; }");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "exported");
assert!(sym.is_some(), "exported function should be found");
assert_eq!(sym.unwrap().kind, SymbolKind::Function);
}
#[test]
fn test_tsconfig_paths_with_multiple_replacements() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
let lib = tmp.path().join("lib");
fs::create_dir_all(&src).unwrap();
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("utils.ts"), "export function foo() {}").unwrap();
fs::write(
tmp.path().join("tsconfig.json"),
r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*", "./lib/*"] } } }"#,
)
.unwrap();
let resolver = TypeScriptResolver::new(tmp.path());
let result = resolver.resolve_import("@/utils", "src/App.tsx", tmp.path());
assert!(
result.is_some(),
"should resolve via second path replacement"
);
assert!(result.unwrap().ends_with("utils.ts"));
}
#[test]
fn test_resolve_direct_file_with_extension() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("data.json"), r#"{"key": "value"}"#).unwrap();
let target = src.join("data.json");
let result = resolve_ts_file(&target);
assert!(result.is_some(), "direct file should resolve");
}
#[test]
fn test_resolve_ts_file_nonexistent() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("nonexistent");
let result = resolve_ts_file(&target);
assert!(result.is_none(), "nonexistent path should return None");
}
#[test]
fn test_parse_getter_setter() {
let result = parse_ts_source(
r#"
class Config {
private _name: string = '';
get name(): string { return this._name; }
set name(v: string) { this._name = v; }
}
"#,
);
assert!(!result.has_errors);
let methods: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.map(|s| s.name.as_str())
.collect();
assert!(
methods.iter().filter(|&&n| n == "name").count() >= 2,
"getter and setter should both appear: {:?}",
methods
);
}
#[test]
fn test_parse_static_method() {
let result = parse_ts_source(
r#"
class Factory {
static create(): Factory { return new Factory(); }
build(): void {}
}
"#,
);
assert!(!result.has_errors);
let methods: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.map(|s| s.name.as_str())
.collect();
assert!(methods.contains(&"create"), "static method: {:?}", methods);
assert!(methods.contains(&"build"), "instance method: {:?}", methods);
}
#[test]
fn test_parse_abstract_class_not_extracted() {
let result = parse_ts_source(
r#"
abstract class BaseHandler {
abstract handle(): void;
log(): void { console.log('log'); }
}
"#,
);
assert!(!result.has_errors);
let class = result.symbols.iter().find(|s| s.name == "BaseHandler");
assert!(
class.is_none(),
"abstract class is not extracted by current parser: {:?}",
result.symbols
);
}
#[test]
fn test_parse_declare_function() {
let result = parse_ts_source("declare function readFile(path: string): string;");
assert!(!result.has_errors);
}
#[test]
fn test_parse_declare_module() {
let result = parse_ts_source(
r#"
declare module 'my-lib' {
export function init(): void;
}
"#,
);
assert!(!result.has_errors);
}
#[test]
fn test_type_ref_union_types() {
let result = parse_ts_source(
r#"
interface Config { port: number; }
interface Options { timeout: number; }
function create(input: Config | Options): void {}
"#,
);
let ref_types: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
ref_types.contains(&"Config"),
"union type Config: {:?}",
ref_types
);
assert!(
ref_types.contains(&"Options"),
"union type Options: {:?}",
ref_types
);
}
#[test]
fn test_type_ref_intersection_types() {
let result = parse_ts_source(
r#"
interface Base { id: number; }
interface Named { name: string; }
function process(item: Base & Named): void {}
"#,
);
let ref_types: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
ref_types.contains(&"Base"),
"intersection type: {:?}",
ref_types
);
assert!(
ref_types.contains(&"Named"),
"intersection type: {:?}",
ref_types
);
}
#[test]
fn test_export_default_function() {
let result = parse_ts_source("export default function handler() { return 1; }");
assert!(!result.has_errors);
let sym = result.symbols.iter().find(|s| s.name == "handler");
assert!(sym.is_some(), "export default function should be extracted");
}
#[test]
fn test_multiple_classes_with_methods() {
let result = parse_ts_source(
r#"
class A {
methodA(): void {}
}
class B {
methodB(): void {}
}
"#,
);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"A"));
assert!(names.contains(&"methodA"));
assert!(names.contains(&"B"));
assert!(names.contains(&"methodB"));
let a_idx = result.symbols.iter().position(|s| s.name == "A").unwrap();
let method_a = result.symbols.iter().find(|s| s.name == "methodA").unwrap();
assert_eq!(method_a.parent_index, Some(a_idx));
}
#[test]
fn test_conditional_jsx_return() {
let result = parse_tsx_source(
r#"
export function Toggle({ on }: { on: boolean }) {
if (on) {
return <span>On</span>;
}
return <span>Off</span>;
}
"#,
);
assert!(!result.has_errors);
let toggle = result.symbols.iter().find(|s| s.name == "Toggle").unwrap();
assert_eq!(toggle.kind, SymbolKind::Component);
}
#[test]
fn test_arrow_component_with_block_body() {
let result = parse_tsx_source(
r#"
export const Card = ({ title }: { title: string }) => {
const formatted = title.toUpperCase();
return <div>{formatted}</div>;
};
"#,
);
assert!(!result.has_errors);
let card = result.symbols.iter().find(|s| s.name == "Card").unwrap();
assert_eq!(card.kind, SymbolKind::Component);
}
#[test]
fn test_import_type_only() {
let result = parse_ts_source("import type { Config, Options } from './types';");
assert!(!result.has_errors);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "./types");
assert!(result.imports[0]
.imported_symbols
.contains(&"Config".to_string()));
}
#[test]
fn test_re_export() {
let result = parse_ts_source("export { default as Button } from './Button';");
assert!(!result.has_errors);
}
#[test]
fn test_language_parser_trait_name() {
let parser = TypeScriptParser;
assert_eq!(parser.language_name(), "typescript");
}
#[test]
fn test_language_parser_tsx_vs_ts() {
let parser = TypeScriptParser;
let ts_result = parser.parse("function foo() { return 1; }", &PathBuf::from("test.ts"));
assert!(!ts_result.has_errors);
let tsx_result = parser.parse(
"export function App() { return <div/>; }",
&PathBuf::from("test.tsx"),
);
assert!(!tsx_result.has_errors);
let app = tsx_result.symbols.iter().find(|s| s.name == "App").unwrap();
assert_eq!(app.kind, SymbolKind::Component);
}
#[test]
fn test_match_tsconfig_pattern_no_wildcard() {
assert_eq!(
match_tsconfig_pattern("lodash", "lodash"),
Some(String::new())
);
assert_eq!(match_tsconfig_pattern("lodash", "lodash/fp"), None);
assert_eq!(match_tsconfig_pattern("lodash", "underscore"), None);
}
#[test]
fn test_calls_in_arrow_function() {
let result = parse_ts_source(
r#"
export const init = () => {
setup();
configure();
};
"#,
);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"setup"), "calls: {:?}", result.calls);
assert!(
callee_names.contains(&"configure"),
"calls: {:?}",
result.calls
);
}
}