use proc_macro2::{Group, Punct, Spacing, TokenStream as TokenStream2, TokenTree};
use quote::{quote, ToTokens};
use syn::{Error, Ident, 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;
use crate::subject::{carried_attributes, cfg_attributes};
const MOST_PREDICATES: usize = 4;
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);
}
let predicates = match predicates(&captured) {
Ok(predicates) => predicates,
Err(error) => {
errors.push(error);
vec![]
}
};
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_tokens = captured.to_token_stream();
let name = naming::trait_carrier(
&trait_ident,
&captured_tokens,
options.name.as_ref(),
line,
column,
&naming::written_at,
);
let remote = options
.remote
.map(|path| rewrite_crate_paths(path.to_token_stream()));
let emitted = remote.is_none().then_some(emitted);
let carriers = (0..1usize << predicates.len()).map(|combination| {
let holds = |index: usize| combination & (1 << index) != 0;
let condition = (!predicates.is_empty()).then(|| {
let terms = predicates.iter().enumerate().map(|(index, predicate)| {
let predicate = &predicate.tokens;
if holds(index) {
quote!(#predicate)
} else {
quote!(not(#predicate))
}
});
quote!(#[cfg(all(#(#terms),*))])
});
let mut selected = captured.clone();
selected
.items
.retain(|item| match predicate_of(item, &predicates) {
Some(index) => holds(index),
None => true,
});
let provided = signatures_only(&mut selected);
let selected = rewrite_crate_paths(selected.to_token_stream());
carrier_macro(
&name,
quote!(#condition #[doc(hidden)] #export),
Implementation::Trait,
CarriedTrait {
tokens: &selected,
remote: remote.as_ref(),
provided: &provided,
},
)
});
let carriers: Vec<TokenStream2> = carriers.collect();
Ok(quote! {
#emitted
#(#carriers)*
#[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 signatures_only(item_trait: &mut ItemTrait) -> Vec<Ident> {
item_trait.attrs.clear();
item_trait.supertraits.clear();
let mut provided = vec![];
for item in &mut item_trait.items {
match item {
TraitItem::Fn(function) => {
if function.default.take().is_some() {
provided.push(function.sig.ident.clone());
function.semi_token = Some(Default::default());
}
function.attrs = kept_attributes(&function.attrs, true);
}
TraitItem::Type(assoc) => {
assoc.default = None;
assoc.bounds.clear();
assoc.colon_token = None;
assoc.attrs = kept_attributes(&assoc.attrs, false);
}
TraitItem::Const(constant) => {
constant.default = None;
constant.attrs = kept_attributes(&constant.attrs, false);
}
_ => {}
}
}
provided
}
fn kept_attributes(attrs: &[syn::Attribute], on_method: bool) -> Vec<syn::Attribute> {
let mut kept: Vec<syn::Attribute> = carried_attributes(attrs)
.into_iter()
.filter(|attribute| !attribute.path().is_ident("cfg"))
.collect();
kept.extend(
attrs
.iter()
.filter(|attribute| {
attribute.path().is_ident("deprecated")
|| (on_method
&& (attribute.path().is_ident("inline")
|| is_dispatch_attribute(attribute)))
})
.cloned(),
);
kept
}
struct Predicate {
key: String,
tokens: TokenStream2,
}
fn predicates(item_trait: &ItemTrait) -> Result<Vec<Predicate>> {
let mut predicates: Vec<Predicate> = vec![];
for item in &item_trait.items {
let Some(tokens) = item_predicate(item) else {
continue;
};
let key = tokens.to_string();
if predicates.iter().any(|predicate| predicate.key == key) {
continue;
}
if predicates.len() == MOST_PREDICATES {
return Err(Error::new_spanned(
item,
format!("this is the {}th distinct `#[cfg]` predicate among the trait's items, and the trait's carrier macro is written once per combination of them; combine the predicates, or move the items under this one to a trait of their own", MOST_PREDICATES + 1),
));
}
predicates.push(Predicate { key, tokens });
}
Ok(predicates)
}
fn predicate_of(item: &TraitItem, predicates: &[Predicate]) -> Option<usize> {
let key = item_predicate(item)?.to_string();
predicates.iter().position(|predicate| predicate.key == key)
}
fn item_predicate(item: &TraitItem) -> Option<TokenStream2> {
let attrs = item_attrs(item)?;
let cfgs = cfg_attributes(attrs);
if cfgs.is_empty() {
return None;
}
let inner: Vec<TokenStream2> = cfgs
.iter()
.map(|attribute| match &attribute.meta {
syn::Meta::List(list) => list.tokens.clone(),
_ => TokenStream2::new(),
})
.collect();
Some(match inner.as_slice() {
[one] => one.clone(),
many => quote!(all(#(#many),*)),
})
}
fn item_attrs(item: &TraitItem) -> Option<&Vec<syn::Attribute>> {
match item {
TraitItem::Fn(function) => Some(&function.attrs),
TraitItem::Type(assoc) => Some(&assoc.attrs),
TraitItem::Const(constant) => Some(&constant.attrs),
_ => None,
}
}
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
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn the_carrier_holds_the_signatures_and_nothing_else() {
let item_trait: ItemTrait = parse_quote!(
pub trait Counted: Clone {
type Tally: Default;
const START: u32 = 7;
#[inline(always)]
#[must_use]
fn count(&self) -> u32;
#[dispatch(mismatch = false)]
fn same(&self, other: &Self) -> bool {
let secret = 41;
secret + 1 == 42
}
}
);
let options: DispatchableOptions = syn::parse_str("name = unit").unwrap();
let expanded = expand_dispatchable(item_trait, options, 1, 1)
.unwrap()
.to_string();
let once = |needle: &str| expanded.matches(needle).count();
assert_eq!(
once("secret"),
2,
"the body is in the trait, not the carrier"
);
assert_eq!(once("doc comment"), 1);
assert_eq!(once("So does"), 1);
assert_eq!(once("must_use"), 1);
assert_eq!(once("Default"), 1, "the bound stays with the trait");
assert_eq!(once("Clone"), 1, "so does the supertrait");
assert_eq!(once("inline"), 2, "the method's own `#[inline]` travels");
assert_eq!(
once("mismatch"),
1,
"the trait's `#[dispatch]` is consumed; the carrier's stays"
);
assert!(expanded.contains("provided { same }"));
}
}