use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use smallvec::SmallVec;
use syn::spanned::Spanned;
use syn::{Error, Result};
use crate::parse;
macro_rules! parse_args {
($ts:expr, $mod:ident;
single: $($svar:ident),*;
multi: $($mvar:ident),*;
) => {{
let args = syn::parse2::<parse::$mod::Args>($ts)?;
let single = ($({
#[allow(irrefutable_let_patterns)]
let rep = args.0.iter().filter_map(|arg| {
if let parse::$mod::Arg::$svar(arg) = arg {
Some(arg.clone())
} else {
None
}
}).collect::<SmallVec<[_; 1]>>();
if rep.len() > 1 {
Err(Error::new(rep[1].span(), "The statement is repeated"))?
}
rep.into_iter().next()
},)*);
let multi = ($({
args.0.iter().filter_map(|arg| {
if let parse::$mod::Arg::$mvar(arg) = arg {
Some(arg.clone())
} else {
None
}
}).collect::<Vec<_>>()
},)*);
(single, multi)
}};
}
fn apply_modifier(
modifier: Option<&parse::Modifier>,
ident: syn::Ident,
meta: Option<TokenStream>,
) -> TokenStream {
let meta = &meta.unwrap_or(quote!());
if let Some(modifier) = modifier {
let vis = &modifier.vis;
if let Some(import) = modifier.imports.as_ref() {
quote!(#meta mod #ident; #meta #vis #import #ident::*;)
} else {
quote!(#meta #vis mod #ident;)
}
} else {
quote!(mod #ident;)
}
}
pub fn all(ts: TokenStream) -> Result<TokenStream> {
let ((dv,), (sv, excepts)) = parse_args! {
ts, all;
single: DefaultVis;
multi: SpecialVis, Except;
};
let mut special = HashMap::<String, (Span, Rc<parse::Modifier>)>::new();
for sve in sv {
let modifier = Rc::new(sve.modifier.clone());
let span = sve.modifier.span();
for name_ident in sve.idents {
let name = name_ident.to_string();
if special.contains_key(&name) {
Err(Error::new(
name_ident.span(),
"The module has multiple visibilities",
))?
}
special.insert(name, (span, modifier.clone()));
}
}
let except = excepts
.into_iter()
.flat_map(|except| except.idents.into_iter())
.map(|ident| ident.to_string())
.collect::<HashSet<_>>();
let mods = list_mods()?
.iter()
.map(|name| -> Result<TokenStream> {
if except.contains(name) {
if special.contains_key(name) {
Err(Error::new(
special[name].0,
"The module has a special visibility but is also excluded in `except`",
))
} else {
Ok(quote!())
}
} else {
let ni = syn::Ident::new(name, Span::call_site());
let modifier = special
.get(name)
.map_or(dv.as_ref().map(|dv| &dv.modifier), |(_, modifier)| {
Some(modifier.as_ref())
});
let stmt = apply_modifier(modifier, ni, None);
Ok(stmt)
}
})
.collect::<Result<Vec<TokenStream>>>()?;
let q = quote!(#(#mods)*);
Ok(q)
}
pub fn os(ts: TokenStream) -> Result<TokenStream> {
cfg(ts, "target_os")
}
pub fn family(ts: TokenStream) -> Result<TokenStream> {
cfg(ts, "target_family")
}
pub fn feature(ts: TokenStream) -> Result<TokenStream> {
cfg(ts, "feature")
}
fn cfg(ts: TokenStream, flag_name: &str) -> Result<TokenStream> {
let flag = syn::Ident::new(flag_name, Span::call_site());
let ((arg,), ()) = parse_args! {
ts, cfg;
single: Cfg;
multi: ;
};
let mods = list_mods()?;
let mods_code = mods.iter().map(|name| {
apply_modifier(
arg.as_ref().map(|arg| &arg.modifier),
syn::Ident::new(name, Span::call_site()),
Some(quote! ( #[cfg(#flag = #name)] )),
)
});
let el = if let Some(Some((_, error))) = arg.as_ref().map(|arg| &arg.error) {
let error = error.as_ref().map_or(
format!("{} must be one of \"{}\"", flag, mods.join("\", \"")),
|error| error.value(),
);
quote! {
#[cfg(not(any(#(#flag = #mods),*)))]
compile_error!(#error);
}
} else {
quote!()
};
let ret = quote! {
#(#mods_code)*
#el
};
Ok(ret)
}
fn list_mods() -> Result<Vec<String>> {
fn me<T: std::fmt::Display>(err: T) -> Error {
Error::new(proc_macro2::Span::call_site(), err)
}
macro_rules! mes {
($fmt:literal) => {
|err| me(format!($fmt, err))
};
}
let span = proc_macro::Span::call_site();
let src = span.source_file();
if !src.is_real() {
return Err(me("dirmod can only be invoked directly, not via macros"));
}
let dir = src
.path()
.parent()
.expect("parent directory does not exist")
.read_dir()
.map_err(mes!("error reading parent directory of current file: {}"))?;
let mut ret = vec![];
for entry in dir {
let entry = entry.map_err(mes!("error reading dir entry: {}"))?;
let path = entry.path();
let ft = entry
.file_type()
.map_err(mes!("error checking dir entry file type: {}"))?;
if ft.is_file()
&& path.extension().and_then(|str| str.to_str()) == Some("rs")
&& path.file_name() != src.path().file_name()
{
let name = entry
.file_name()
.into_string()
.map_err(|_| me("Module is not UTF-8 compliant"))?;
ret.push(name[..(name.len() - 3)].to_string());
} else if ft.is_dir() && path.join("mod.rs").is_file() {
let name = entry
.file_name()
.into_string()
.map_err(|_| me("Module is not UTF-8 compliant"))?;
ret.push(name);
}
}
Ok(ret)
}