pub mod ast;
mod csharp;
mod dart;
mod go;
mod java;
mod kotlin;
mod php;
mod python;
mod ruby;
mod rust_lang;
mod swift;
mod typescript;
mod yaml;
use crate::types::{ExportLevel, Language, ParseMode};
use std::path::Path;
pub fn get_exported_symbols(file_path: &Path) -> Vec<String> {
get_exported_symbols_full(file_path, ExportLevel::Member, ParseMode::Regex)
}
#[allow(dead_code)]
pub fn get_exported_symbols_with_level(file_path: &Path, level: ExportLevel) -> Vec<String> {
get_exported_symbols_full(file_path, level, ParseMode::Regex)
}
pub fn get_exported_symbols_full(
file_path: &Path,
level: ExportLevel,
parse_mode: ParseMode,
) -> Vec<String> {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let lang = match Language::from_extension(ext) {
Some(l) => l,
None => return Vec::new(),
};
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let symbols = if parse_mode == ParseMode::Ast {
match lang {
Language::TypeScript => {
let base_dir = file_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let resolver = move |import_path: &str| resolve_ts_import(&base_dir, import_path);
let result =
ast::typescript::extract_exports_with_resolver(&content, Some(&resolver));
if result.is_empty() {
let base_dir2 = file_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let resolver2 =
move |import_path: &str| resolve_ts_import(&base_dir2, import_path);
typescript::extract_exports_with_resolver(&content, Some(&resolver2))
} else {
result
}
}
Language::Python => {
let result = ast::python::extract_exports(&content);
if result.is_empty() {
python::extract_exports(&content)
} else {
result
}
}
Language::Rust => {
let result = ast::rust_lang::extract_exports(&content);
if result.is_empty() {
rust_lang::extract_exports(&content)
} else {
result
}
}
_ => extract_with_regex(&content, lang, file_path),
}
} else {
extract_with_regex(&content, lang, file_path)
};
let symbols = if level == ExportLevel::Type {
filter_type_level_exports(&content, &symbols, lang)
} else {
symbols
};
let mut seen = std::collections::HashSet::new();
symbols
.into_iter()
.filter(|s| seen.insert(s.clone()))
.collect()
}
fn extract_with_regex(content: &str, lang: Language, file_path: &Path) -> Vec<String> {
match lang {
Language::TypeScript => {
let base_dir = file_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let resolver = move |import_path: &str| resolve_ts_import(&base_dir, import_path);
typescript::extract_exports_with_resolver(content, Some(&resolver))
}
Language::Rust => rust_lang::extract_exports(content),
Language::Go => go::extract_exports(content),
Language::Python => python::extract_exports(content),
Language::Swift => swift::extract_exports(content),
Language::Kotlin => kotlin::extract_exports(content),
Language::Java => java::extract_exports(content),
Language::CSharp => csharp::extract_exports(content),
Language::Dart => dart::extract_exports(content),
Language::Php => php::extract_exports(content),
Language::Ruby => ruby::extract_exports(content),
Language::Yaml => yaml::extract_exports(content),
}
}
fn filter_type_level_exports(content: &str, symbols: &[String], lang: Language) -> Vec<String> {
use regex::Regex;
let type_pattern = match lang {
Language::TypeScript => {
Regex::new(
r"(?m)export\s+(?:default\s+)?(?:abstract\s+)?(?:class|interface|type|enum)\s+(\w+)",
)
.ok()
}
Language::Rust => {
Regex::new(r"(?m)pub(?:\(crate\))?\s+(?:struct|enum|trait|type|mod)\s+(\w+)").ok()
}
Language::Go => {
Regex::new(r"(?m)^type\s+([A-Z]\w*)\s+(?:struct|interface)").ok()
}
Language::Python => {
Regex::new(r"(?m)^class\s+(\w+)").ok()
}
Language::Swift => {
Regex::new(
r"(?m)(?:public|open)\s+(?:final\s+)?(?:class|struct|enum|protocol|actor)\s+(\w+)",
)
.ok()
}
Language::Kotlin => {
Regex::new(
r"(?m)(?:public\s+|open\s+|abstract\s+|sealed\s+)*(?:class|interface|enum\s+class|object|data\s+class)\s+(\w+)",
)
.ok()
}
Language::Java => {
Regex::new(
r"(?m)(?:public\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)",
)
.ok()
}
Language::CSharp => {
Regex::new(
r"(?m)(?:public\s+)?(?:abstract\s+|sealed\s+|static\s+)?(?:class|interface|enum|struct|record)\s+(\w+)",
)
.ok()
}
Language::Dart => {
Regex::new(r"(?m)(?:abstract\s+)?class\s+(\w+)|(?m)enum\s+(\w+)").ok()
}
Language::Php => {
Regex::new(
r"(?m)(?:abstract\s+|final\s+)?(?:readonly\s+)?(?:class|interface|trait|enum)\s+(\w+)",
)
.ok()
}
Language::Ruby => {
Regex::new(r"(?m)(?:class|module)\s+([A-Z]\w*)").ok()
}
Language::Yaml => {
return symbols.to_vec();
}
};
let type_names: std::collections::HashSet<String> = match type_pattern {
Some(re) => re
.captures_iter(content)
.filter_map(|caps| {
caps.get(1)
.or_else(|| caps.get(2))
.map(|m| m.as_str().to_string())
})
.collect(),
None => return symbols.to_vec(),
};
symbols
.iter()
.filter(|s| type_names.contains(s.as_str()))
.cloned()
.collect()
}
fn resolve_ts_import(base_dir: &Path, import_path: &str) -> Option<String> {
if !import_path.starts_with('.') {
return None;
}
let target = base_dir.join(import_path);
if target.is_file() {
return std::fs::read_to_string(&target).ok();
}
for ext in &[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"] {
let with_ext = target.with_extension(ext.trim_start_matches('.'));
if with_ext.is_file() {
return std::fs::read_to_string(&with_ext).ok();
}
}
for index in &["index.ts", "index.tsx", "index.js", "index.jsx"] {
let index_path = target.join(index);
if index_path.is_file() {
return std::fs::read_to_string(&index_path).ok();
}
}
None
}
const TEST_DIR_NAMES: &[&str] = &[
"tests",
"test",
"__tests__",
"spec",
"specs",
"testing",
"uitests",
"unittests",
"integrationtests",
"testcases",
"fixtures",
"mocks",
"stubs",
"fakes",
];
pub fn is_test_file(file_path: &Path) -> bool {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let lang = match Language::from_extension(ext) {
Some(l) => l,
None => return false,
};
let name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
for pattern in lang.test_patterns() {
if name.ends_with(pattern) || name.starts_with(pattern) {
return true;
}
}
for component in file_path.components() {
if let std::path::Component::Normal(dir) = component {
let dir_lower = dir.to_string_lossy().to_lowercase();
if TEST_DIR_NAMES.contains(&dir_lower.as_str()) {
return true;
}
}
}
false
}
pub fn is_source_file(file_path: &Path) -> bool {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
Language::from_extension(ext).is_some()
}
pub fn has_extension(file_path: &Path, extensions: &[String]) -> bool {
if extensions.is_empty() {
return is_source_file(file_path);
}
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
extensions.iter().any(|e| e == ext)
}