use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
use syn::{
FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input,
};
use crate::get_global_set;
fn extract_previous_info(sig: &Signature) -> syn::Result<(Pat, TypePath)> {
if sig.inputs.len() != 1 {
return Err(syn::Error::new(
sig.inputs.span(),
"Help function must have exactly one parameter (the entry type)",
));
}
let arg = &sig.inputs[0];
match arg {
FnArg::Typed(PatType { pat, ty, .. }) => {
let param_pat = (**pat).clone();
match &**ty {
Type::Path(type_path) => Ok((param_pat, type_path.clone())),
_ => Err(syn::Error::new(
ty.span(),
"Parameter type must be a type path",
)),
}
}
FnArg::Receiver(_) => Err(syn::Error::new(
arg.span(),
"Help function cannot have self parameter",
)),
}
}
fn validate_return_type(sig: &Signature) -> syn::Result<()> {
match &sig.output {
ReturnType::Type(_, ty) => match &**ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()),
_ => Err(syn::Error::new(
ty.span(),
"Help function must return () or have no return type",
)),
},
ReturnType::Default => Ok(()),
}
}
pub fn help_attr(item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
if input_fn.sig.asyncness.is_some() {
return syn::Error::new(input_fn.sig.span(), "Help function cannot be async")
.to_compile_error()
.into();
}
let (prev_param, entry_type) = match extract_previous_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
if let Some(err_tokens) = crate::check_single_segment_type(&entry_type, "#[help]") {
return err_tokens.into();
}
if let Err(e) = validate_return_type(&input_fn.sig) {
return e.to_compile_error().into();
}
let fn_body = &input_fn.block;
let mut fn_attrs = input_fn.attrs.clone();
fn_attrs.retain(|attr| !attr.path().is_ident("help"));
let vis = &input_fn.vis;
let fn_name = &input_fn.sig.ident;
let internal_name = format!(
"__internal_help_{}",
just_fmt::snake_case!(fn_name.to_string())
);
let struct_name = Ident::new(&internal_name, fn_name.span());
let help_entry = build_help_entry(&struct_name, &entry_type);
let entry_str = help_entry.to_string();
get_global_set(&crate::HELP_REQUESTS)
.lock()
.unwrap()
.insert(entry_str);
let expanded = quote! {
#(#fn_attrs)*
#[doc(hidden)]
#[allow(non_camel_case_types)]
#vis struct #struct_name;
impl ::mingling::HelpRequest for #struct_name {
type Entry = #entry_type;
fn render_help(#prev_param: Self::Entry, __renderer_inner_result: &mut ::mingling::RenderResult) {
#[allow(non_snake_case)]
fn help_wrapper(#prev_param: #entry_type, __renderer_inner_result: &mut ::mingling::RenderResult) {
#fn_body
}
help_wrapper(#prev_param, __renderer_inner_result);
}
}
::mingling::macros::register_help!(#entry_type, #struct_name);
#(#fn_attrs)*
#vis fn #fn_name(#prev_param: #entry_type) {
let mut dummy_r = ::mingling::RenderResult::default();
let __renderer_inner_result = &mut dummy_r;
#fn_body
}
};
expanded.into()
}
fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::TokenStream {
let enum_variant = &entry_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => {
let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() };
<#struct_name as ::mingling::HelpRequest>::render_help(value, __renderer_inner_result);
}
}
}
pub fn register_help(input: TokenStream) -> TokenStream {
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
if input_parsed.len() != 2 {
return syn::Error::new(
input_parsed.span(),
"Expected exactly two comma-separated arguments: `EntryType, StructName`",
)
.to_compile_error()
.into();
}
let entry_type_expr = &input_parsed[0];
let struct_name_expr = &input_parsed[1];
let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) {
Ok(ty) => ty,
Err(e) => return e.to_compile_error().into(),
};
let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
Ok(ident) => ident,
Err(e) => return e.to_compile_error().into(),
};
let help_entry = build_help_entry(&struct_name, &entry_type);
let entry_str = help_entry.to_string();
get_global_set(&crate::HELP_REQUESTS)
.lock()
.unwrap()
.insert(entry_str);
quote! {}.into()
}