use proc_macro2::{Group, Punct, Spacing, TokenStream as TokenStream2, TokenTree};
use quote::{quote, ToTokens};
use syn::{Error, ItemTrait, Result, TraitItem, TraitItemFn, Visibility};
use crate::combined;
use crate::naming;
use crate::options::{is_dispatch_attribute, method_mismatch, DispatchableOptions};
use crate::protocol::{carrier_macro, CarriedTrait, Implementation};
use crate::shape::takes_self_argument;
pub(crate) fn expand_dispatchable(
item_trait: ItemTrait,
options: DispatchableOptions,
line: usize,
column: usize,
) -> Result<TokenStream2> {
let captured = item_trait.clone();
let mut emitted = item_trait;
let mut errors = vec![];
if let Err(error) = check_attribute_order(&emitted) {
errors.push(error);
}
for item in &mut emitted.items {
if let TraitItem::Fn(function) = item {
if let Err(error) = check_method_attributes(function) {
errors.push(error);
}
function
.attrs
.retain(|attribute| !is_dispatch_attribute(attribute));
}
}
if let Some(error) = combined(errors) {
let error = error.to_compile_error();
let emitted = options.remote.is_none().then_some(&emitted);
return Ok(quote! { #emitted #error });
}
let trait_ident = emitted.ident.clone();
let vis = emitted.vis.clone();
let export = if matches!(vis, Visibility::Public(_)) {
quote!(#[macro_export])
} else {
quote!(#[allow(unused_macros)])
};
let captured = captured.to_token_stream();
let name = naming::trait_carrier(&trait_ident, &captured, options.name.as_ref(), line, column);
let captured = rewrite_crate_paths(captured);
let remote = options
.remote
.map(|path| rewrite_crate_paths(path.to_token_stream()));
let emitted = remote.is_none().then_some(emitted);
let carrier = carrier_macro(
&name,
quote!(#[doc(hidden)] #export),
Implementation::Trait,
CarriedTrait {
tokens: &captured,
remote: remote.as_ref(),
},
);
Ok(quote! {
#emitted
#carrier
#[doc(hidden)]
#[allow(unused_imports)]
#vis use #name as #trait_ident;
})
}
fn check_attribute_order(item_trait: &ItemTrait) -> Result<()> {
if let Some(attribute) = item_trait.attrs.iter().find(|attribute| {
attribute
.path()
.segments
.last()
.is_some_and(|segment| segment.ident == "async_trait")
}) {
return Err(Error::new_spanned(
attribute,
"`#[async_trait]` rewrites the signatures after `whichever` has read them; put `#[whichever::dispatchable]` below it",
));
}
let hidden_method = |name: &str| {
item_trait.items.iter().any(|item| {
matches!(item, TraitItem::Fn(function) if function.sig.ident == name
&& function.attrs.iter().any(|attribute| attribute.path().is_ident("doc")))
})
};
if hidden_method("typetag_name") && hidden_method("typetag_deserialize") {
return Err(Error::new_spanned(
&item_trait.ident,
"`#[typetag::serde]` has added its per-impl methods to the trait, which a forwarder would answer with the payload's tag; put `#[whichever::dispatchable]` above it, and `#[typetag::serde(name = \"..\")]` on the enum's partial impl block",
));
}
Ok(())
}
fn check_method_attributes(function: &TraitItemFn) -> Result<()> {
let mut attributes = function
.attrs
.iter()
.filter(|attribute| is_dispatch_attribute(attribute));
let Some(first) = attributes.next() else {
return Ok(());
};
if let Some(second) = attributes.next() {
return Err(Error::new_spanned(
second,
"one `#[dispatch]` attribute per method",
));
}
if method_mismatch(function)?.is_some() && !takes_self_argument(&function.sig) {
return Err(Error::new_spanned(
first,
format!(
"`mismatch` decides between two different variants, and `{}` takes no `Self`, `&Self` or `&mut Self` argument",
function.sig.ident
),
));
}
Ok(())
}
fn rewrite_crate_paths(tokens: TokenStream2) -> TokenStream2 {
let mut output = TokenStream2::new();
let mut iter = tokens.into_iter().peekable();
while let Some(token) = iter.next() {
match token {
TokenTree::Group(group) => {
let mut rewritten =
Group::new(group.delimiter(), rewrite_crate_paths(group.stream()));
rewritten.set_span(group.span());
output.extend([TokenTree::Group(rewritten)]);
}
TokenTree::Ident(ident)
if ident == "crate"
&& matches!(iter.peek(), Some(TokenTree::Punct(punct)) if punct.as_char() == ':') =>
{
output.extend([
TokenTree::Punct(Punct::new('$', Spacing::Alone)),
TokenTree::Ident(ident),
]);
}
other => output.extend([other]),
}
}
output
}