use {
crate::core::constants::{
configuration,
re_export,
},
proc_macro2::TokenStream,
quote::quote,
std::{
collections::{
HashMap,
HashSet,
},
fs,
path::Path,
},
syn::{
Ident,
Item,
LitStr,
Result,
Token,
Visibility,
braced,
parse::{
Parse,
ParseStream,
},
parse_file,
},
};
pub trait ReExportFormatter {
fn format_item(
&self,
module_name: &Ident,
item_ident: &Ident,
alias: Option<&Ident>,
) -> TokenStream;
fn format_output(
&self,
re_exports: Vec<TokenStream>,
base_path: &syn::Path,
) -> TokenStream;
fn matches_item(
&self,
item: &Item,
) -> Option<String>;
}
pub struct FunctionFormatter;
impl ReExportFormatter for FunctionFormatter {
fn format_item(
&self,
module_name: &Ident,
item_ident: &Ident,
alias: Option<&Ident>,
) -> TokenStream {
if let Some(alias) = alias {
quote! { #module_name::#item_ident as #alias }
} else {
quote! { #module_name::#item_ident }
}
}
fn format_output(
&self,
re_exports: Vec<TokenStream>,
base_path: &syn::Path,
) -> TokenStream {
quote! {
pub use #base_path::{
#(#re_exports),*
};
}
}
fn matches_item(
&self,
item: &Item,
) -> Option<String> {
if let Item::Fn(func) = item { Some(func.sig.ident.to_string()) } else { None }
}
}
pub struct TraitFormatter;
impl ReExportFormatter for TraitFormatter {
fn format_item(
&self,
module_name: &Ident,
item_ident: &Ident,
alias: Option<&Ident>,
) -> TokenStream {
if let Some(alias) = alias {
quote! { pub use #module_name::#item_ident as #alias; }
} else {
quote! { pub use #module_name::#item_ident; }
}
}
fn format_output(
&self,
re_exports: Vec<TokenStream>,
_base_path: &syn::Path,
) -> TokenStream {
quote! {
#(#re_exports)*
}
}
fn matches_item(
&self,
item: &Item,
) -> Option<String> {
if let Item::Trait(trait_item) = item { Some(trait_item.ident.to_string()) } else { None }
}
}
pub struct ReExportInput {
path: LitStr,
aliases: HashMap<String, Ident>,
exclusions: HashSet<String>,
}
impl Parse for ReExportInput {
fn parse(input: ParseStream) -> Result<Self> {
let path: LitStr = input.parse()?;
let mut aliases = HashMap::new();
let mut exclusions = HashSet::new();
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let content;
braced!(content in input);
while !content.is_empty() {
let key_str = if content.peek(LitStr) {
let s: LitStr = content.parse()?;
s.value()
} else {
let i: Ident = content.parse()?;
i.to_string()
};
content.parse::<Token![:]>()?;
let value: Ident = content.parse()?;
aliases.insert(key_str, value);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let exclude_kw: Ident = input.parse()?;
if exclude_kw != "exclude" {
return Err(syn::Error::new(exclude_kw.span(), "expected `exclude`"));
}
let content;
braced!(content in input);
while !content.is_empty() {
let key_str = if content.peek(LitStr) {
let s: LitStr = content.parse()?;
s.value()
} else {
let i: Ident = content.parse()?;
i.to_string()
};
exclusions.insert(key_str);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
}
Ok(ReExportInput {
path,
aliases,
exclusions,
})
}
}
fn detect_re_export_pattern(file: &syn::File) -> Option<String> {
for item in &file.items {
if let Item::Use(use_item) = item
&& matches!(use_item.vis, Visibility::Public(_))
{
if let syn::UseTree::Path(path) = &use_item.tree
&& let syn::UseTree::Glob(_) = &*path.tree
{
return Some(path.ident.to_string());
}
}
}
None
}
fn collect_items<V, F>(
file: &syn::File,
reexport_module: Option<&str>,
mut visibility_filter: V,
mut item_filter: F,
) -> Vec<String>
where
V: FnMut(&Item) -> bool,
F: FnMut(&Item) -> Option<String>, {
if let Some(module_name) = reexport_module {
file.items
.iter()
.filter_map(|item| {
if let Item::Mod(mod_item) = item
&& mod_item.ident == module_name
&& let Some((_, items)) = &mod_item.content
{
return Some(
items
.iter()
.filter(|item| visibility_filter(item))
.filter_map(&mut item_filter)
.collect::<Vec<_>>(),
);
}
None
})
.flatten()
.collect()
} else {
file.items.iter().filter(|item| visibility_filter(item)).filter_map(item_filter).collect()
}
}
fn is_public_item(item: &Item) -> bool {
match item {
Item::Const(i) => matches!(i.vis, Visibility::Public(_)),
Item::Enum(i) => matches!(i.vis, Visibility::Public(_)),
Item::ExternCrate(i) => matches!(i.vis, Visibility::Public(_)),
Item::Fn(i) => matches!(i.vis, Visibility::Public(_)),
Item::ForeignMod(_) => false, Item::Impl(_) => false, Item::Macro(_) => false, Item::Mod(i) => matches!(i.vis, Visibility::Public(_)),
Item::Static(i) => matches!(i.vis, Visibility::Public(_)),
Item::Struct(i) => matches!(i.vis, Visibility::Public(_)),
Item::Trait(i) => matches!(i.vis, Visibility::Public(_)),
Item::TraitAlias(i) => matches!(i.vis, Visibility::Public(_)),
Item::Type(i) => matches!(i.vis, Visibility::Public(_)),
Item::Union(i) => matches!(i.vis, Visibility::Public(_)),
Item::Use(i) => matches!(i.vis, Visibility::Public(_)),
Item::Verbatim(_) => false, _ => false, }
}
fn collect_public_items<F>(
file: &syn::File,
reexport_module: Option<&str>,
item_filter: F,
) -> Vec<String>
where
F: FnMut(&Item) -> Option<String>, {
collect_items(file, reexport_module, is_public_item, item_filter)
}
fn scan_directory_and_collect(
input: &ReExportInput,
formatter: &dyn ReExportFormatter,
) -> Vec<TokenStream> {
#[expect(clippy::expect_used, reason = "CARGO_MANIFEST_DIR is always set by Cargo")]
let manifest_dir =
std::env::var(configuration::CARGO_MANIFEST_DIR).expect("CARGO_MANIFEST_DIR not set");
let base_path = Path::new(&manifest_dir).join(input.path.value());
let mut re_exports = Vec::new();
if let Ok(entries) = fs::read_dir(&base_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some(re_export::RS_EXTENSION) {
let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue; };
if file_stem == re_export::MOD_FILE_STEM {
continue;
}
let Ok(content) = fs::read_to_string(&path) else {
continue; };
if let Ok(file) = parse_file(&content) {
let reexport_module = detect_re_export_pattern(&file);
let items = collect_public_items(&file, reexport_module.as_deref(), |item| {
formatter.matches_item(item)
});
for item_name in items {
let full_name = format!("{file_stem}::{item_name}");
if input.exclusions.contains(&full_name)
|| input.exclusions.contains(&item_name)
{
continue;
}
let item_ident = Ident::new(&item_name, proc_macro2::Span::call_site());
let module_name = Ident::new(file_stem, proc_macro2::Span::call_site());
let alias =
input.aliases.get(&full_name).or_else(|| input.aliases.get(&item_name));
let tokens = formatter.format_item(&module_name, &item_ident, alias);
re_exports.push(tokens);
}
}
}
}
} else {
let path_str = input.path.value();
return vec![quote! {
compile_error!(concat!(
"Failed to read directory for re-export generation: '",
#path_str,
"'. Please ensure the path exists and is accessible."
));
}];
}
re_exports.sort_by_key(|tokens| tokens.to_string());
re_exports
}
pub fn generate_re_exports_worker(
input: &ReExportInput,
formatter: &dyn ReExportFormatter,
) -> TokenStream {
let base_path = parse_base_path_from_input(input);
let re_exports = scan_directory_and_collect(input, formatter);
formatter.format_output(re_exports, &base_path)
}
fn parse_base_path_from_input(input: &ReExportInput) -> syn::Path {
let path_str = input.path.value();
let parts: Vec<&str> =
path_str.split('/').filter(|p| *p != re_export::SRC_DIR && !p.is_empty()).collect();
let mut segments = syn::punctuated::Punctuated::new();
segments.push(syn::PathSegment {
ident: Ident::new(re_export::CRATE_KEYWORD, proc_macro2::Span::call_site()),
arguments: syn::PathArguments::None,
});
for part in parts {
segments.push(syn::PathSegment {
ident: Ident::new(part, proc_macro2::Span::call_site()),
arguments: syn::PathArguments::None,
});
}
syn::Path {
leading_colon: None,
segments,
}
}