use regex::Regex;
use std::sync::LazyLock;
static COMMENT_SINGLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)//.*$").unwrap());
static COMMENT_MULTI: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap());
static EXPORT_DECL: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"export\s+(?:async\s+)?(?:abstract\s+)?(?:function|class|interface|type|const|enum)\s+(\w+)")
.unwrap()
});
static RE_EXPORT_TYPE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"export\s+type\s*\{([^}]+)\}").unwrap());
static RE_EXPORT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"export\s*\{([^}]+)\}").unwrap());
static WILDCARD_EXPORT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"export\s+\*\s+(?:as\s+(\w+)\s+)?from\s+['"]([^'"]+)['"]"#).unwrap()
});
static EXPORT_DEFAULT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"export\s+default\s+(?:(?:abstract\s+)?(?:function|class)\s+(\w+)|(\w+)\s*[;\n])")
.unwrap()
});
#[cfg_attr(not(test), allow(dead_code))]
pub fn extract_exports(content: &str) -> Vec<String> {
extract_exports_with_resolver(content, None)
}
type ImportResolver<'a> = dyn Fn(&str) -> Option<String> + 'a;
pub fn extract_exports_with_resolver(
content: &str,
resolver: Option<&ImportResolver<'_>>,
) -> Vec<String> {
let stripped = COMMENT_SINGLE.replace_all(content, "");
let stripped = COMMENT_MULTI.replace_all(&stripped, "");
let mut symbols = Vec::new();
for caps in EXPORT_DECL.captures_iter(&stripped) {
if let Some(name) = caps.get(1) {
symbols.push(name.as_str().to_string());
}
}
for caps in EXPORT_DEFAULT.captures_iter(&stripped) {
if let Some(name) = caps.get(1).or_else(|| caps.get(2)) {
let n = name.as_str();
if !["new", "function", "class", "abstract", "async", "true", "false", "null", "undefined"].contains(&n) {
symbols.push(n.to_string());
}
}
}
for caps in RE_EXPORT_TYPE.captures_iter(&stripped) {
if let Some(names) = caps.get(1) {
for name in names.as_str().split(',') {
let name = name.trim();
let final_name = name.split(" as ").last().unwrap_or(name).trim();
if !final_name.is_empty() {
symbols.push(final_name.to_string());
}
}
}
}
for caps in RE_EXPORT.captures_iter(&stripped) {
let full = caps.get(0).unwrap().as_str();
if full.contains("export type") {
continue;
}
if let Some(names) = caps.get(1) {
for name in names.as_str().split(',') {
let name = name.trim();
let name = name.strip_prefix("type ").unwrap_or(name);
let final_name = name.split(" as ").last().unwrap_or(name).trim();
if !final_name.is_empty() {
symbols.push(final_name.to_string());
}
}
}
}
for caps in WILDCARD_EXPORT.captures_iter(&stripped) {
if let Some(alias) = caps.get(1) {
symbols.push(alias.as_str().to_string());
} else if let Some(resolver) = resolver {
let path = caps.get(2).unwrap().as_str();
if let Some(target_content) = resolver(path) {
let target_symbols = extract_exports_with_resolver(&target_content, None);
symbols.extend(target_symbols);
}
}
}
symbols
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_exports() {
let src = r#"
export function createAuth(config: Config): Auth {}
export class AuthService {}
export interface AuthConfig {}
export type TokenType = string;
export const DEFAULT_TTL = 3600;
export enum AuthStatus { Active, Expired }
"#;
let symbols = extract_exports(src);
assert_eq!(
symbols,
vec![
"createAuth",
"AuthService",
"AuthConfig",
"TokenType",
"DEFAULT_TTL",
"AuthStatus"
]
);
}
#[test]
fn test_comments_stripped() {
let src = r#"
// export function notExported() {}
/* export class AlsoNot {} */
export function realExport(): void {}
"#;
let symbols = extract_exports(src);
assert_eq!(symbols, vec!["realExport"]);
}
#[test]
fn test_re_exports() {
let src = r#"
export { Foo, Bar as Baz } from './module';
export type { MyType } from './types';
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"Foo".to_string()));
assert!(symbols.contains(&"Baz".to_string()));
assert!(symbols.contains(&"MyType".to_string()));
}
#[test]
fn test_wildcard_namespace_export() {
let src = r#"
export * as Utils from './utils';
export * as Types from './types';
"#;
let symbols = extract_exports(src);
assert_eq!(symbols, vec!["Utils", "Types"]);
}
#[test]
fn test_wildcard_export_with_resolver() {
let src = r#"
export * from './helpers';
export function main() {}
"#;
let helper_content = r#"
export function helperA() {}
export function helperB() {}
export const HELPER_CONST = 42;
"#;
let resolver = |path: &str| -> Option<String> {
if path == "./helpers" {
Some(helper_content.to_string())
} else {
None
}
};
let symbols = extract_exports_with_resolver(src, Some(&resolver));
assert!(symbols.contains(&"main".to_string()));
assert!(symbols.contains(&"helperA".to_string()));
assert!(symbols.contains(&"helperB".to_string()));
assert!(symbols.contains(&"HELPER_CONST".to_string()));
}
#[test]
fn test_wildcard_export_without_resolver() {
let src = r#"
export * from './helpers';
export function main() {}
"#;
let symbols = extract_exports(src);
assert_eq!(symbols, vec!["main"]);
}
#[test]
fn test_default_export_class() {
let src = r#"
export default class MyApp {}
export function helper() {}
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"MyApp".to_string()));
assert!(symbols.contains(&"helper".to_string()));
}
#[test]
fn test_async_and_abstract_exports() {
let src = r#"
export async function fetchData() {}
export abstract class BaseService {}
"#;
let symbols = extract_exports(src);
assert!(symbols.contains(&"fetchData".to_string()));
assert!(symbols.contains(&"BaseService".to_string()));
}
}