use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{
parse_quote, FnArg, Ident, ImplItem, Item, ItemEnum, ItemFn, ItemImpl, ItemStruct, ItemType,
LitStr, Pat, Signature, Visibility,
};
pub(crate) fn attribute(attr: TokenStream, item: TokenStream) -> TokenStream {
let item_ts2 = TokenStream2::from(item.clone());
if let Ok(item_fn) = syn::parse::<ItemFn>(item.clone()) {
let args = match syn::parse::<FnAttrArgs>(attr) {
Ok(a) => a,
Err(e) => return e.to_compile_error().into(),
};
return expand_fn(args, item_fn).into();
}
if let Ok(parsed_item) = syn::parse::<Item>(item) {
if let Err(e) = parse_empty_attr(attr) {
return e.to_compile_error().into();
}
match parsed_item {
Item::Enum(item_enum) => return expand_enum(item_enum).into(),
Item::Struct(item_struct) => return expand_struct(item_struct).into(),
Item::Type(item_type) => return expand_type_alias(item_type).into(),
Item::Impl(item_impl) => return expand_impl_block(item_impl).into(),
other => {
return syn::Error::new_spanned(
other,
"`#[expose_pub]` supports `fn`, `enum`, `struct`, `type`, and `impl` blocks; \
other item kinds are not supported",
)
.to_compile_error()
.into();
}
}
}
syn::Error::new_spanned(
item_ts2,
"`#[expose_pub]` couldn't parse the annotated item; \
expected `fn`, `enum`, `struct`, `type`, or `impl`",
)
.to_compile_error()
.into()
}
fn parse_empty_attr(attr: TokenStream) -> syn::Result<()> {
let ts2 = TokenStream2::from(attr);
if ts2.is_empty() {
return Ok(());
}
Err(syn::Error::new_spanned(
ts2,
"`#[expose_pub]` on a type / impl-block does not accept arguments \
(renaming a type breaks every reference; the macro raises visibility in place)",
))
}
struct FnAttrArgs {
wrapper_name: LitStr,
}
impl Parse for FnAttrArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Err(input.error(
"`#[expose_pub]` on a function requires `as = \"<wrapper_name>\"` \
(pick a distinct name so the wrapper doesn't collide with the original)",
));
}
let key = Ident::parse_any(input)?;
if key != "as" {
return Err(syn::Error::new(
key.span(),
format!(
"unknown `expose_pub` argument `{key}`; expected `as = \"<wrapper_name>\"`"
),
));
}
input.parse::<syn::Token![=]>()?;
let wrapper_name: LitStr = input.parse()?;
if !input.is_empty() {
return Err(input
.error("`#[expose_pub(as = \"...\")]` takes one argument; nothing else expected"));
}
Ok(FnAttrArgs { wrapper_name })
}
}
fn expand_fn(args: FnAttrArgs, item_fn: ItemFn) -> TokenStream2 {
let orig_ident = item_fn.sig.ident.clone();
let wrapper_ident = format_ident!("{}", args.wrapper_name.value());
let mut wrapper_sig = item_fn.sig.clone();
wrapper_sig.ident = wrapper_ident;
let call_args = forward_args(&item_fn.sig);
let call_expr = build_call_expr(&item_fn, &orig_ident, &call_args);
quote! {
#item_fn
#[doc(hidden)]
pub #wrapper_sig {
#call_expr
}
}
}
fn build_call_expr(item_fn: &ItemFn, orig: &Ident, args: &[TokenStream2]) -> TokenStream2 {
let has_receiver = matches!(item_fn.sig.inputs.first(), Some(FnArg::Receiver(_)));
if has_receiver {
return quote!(self.#orig(#(#args),*));
}
if item_mentions_self_type(item_fn) {
return quote!(Self::#orig(#(#args),*));
}
quote!(#orig(#(#args),*))
}
fn item_mentions_self_type(item_fn: &ItemFn) -> bool {
let mut tokens = TokenStream2::new();
item_fn.sig.to_tokens(&mut tokens);
item_fn.block.to_tokens(&mut tokens);
tokens.to_string().contains("Self")
}
fn expand_enum(mut item: ItemEnum) -> TokenStream2 {
item.vis = pub_visibility();
quote! {
#[doc(hidden)]
#item
}
}
fn expand_struct(mut item: ItemStruct) -> TokenStream2 {
item.vis = pub_visibility();
quote! {
#[doc(hidden)]
#item
}
}
fn expand_type_alias(mut item: ItemType) -> TokenStream2 {
item.vis = pub_visibility();
quote! {
#[doc(hidden)]
#item
}
}
fn expand_impl_block(mut item: ItemImpl) -> TokenStream2 {
for impl_item in item.items.iter_mut() {
if let ImplItem::Fn(fn_item) = impl_item {
fn_item.vis = pub_visibility();
fn_item.attrs.insert(0, parse_quote!(#[doc(hidden)]));
}
}
quote! { #item }
}
fn pub_visibility() -> Visibility {
parse_quote!(pub)
}
fn forward_args(sig: &Signature) -> Vec<TokenStream2> {
sig.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => match &*pat_type.pat {
Pat::Ident(pi) => {
let ident = &pi.ident;
Some(quote!(#ident))
}
_ => {
let span = pat_type.span();
Some(quote_spanned! { span =>
compile_error!(
"`#[expose_pub]` v1 supports plain identifier args (`fn foo(a: T)`); \
destructuring patterns are not yet supported"
)
})
}
},
})
.collect()
}