use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::visit_mut::{self, VisitMut};
use syn::{
Attribute, Error, Field, Fields, GenericParam, Ident, Index, Item, ItemEnum, ItemStruct,
Member, Path, Result, Type, Variant,
};
use crate::options::{is_dispatch_attribute, TargetOptions, TryFromForms};
use crate::route::Route;
use crate::shape::{bare_ident, generic_type_args, is_self, last_segment};
#[derive(Clone)]
pub(crate) enum Subject {
Enum(ItemEnum),
Struct(ItemStruct),
}
impl Subject {
pub(crate) fn ident(&self) -> &Ident {
match self {
Subject::Enum(item) => &item.ident,
Subject::Struct(item) => &item.ident,
}
}
pub(crate) fn generics(&self) -> &syn::Generics {
match self {
Subject::Enum(item) => &item.generics,
Subject::Struct(item) => &item.generics,
}
}
pub(crate) fn attrs_mut(&mut self) -> &mut Vec<Attribute> {
match self {
Subject::Enum(item) => &mut item.attrs,
Subject::Struct(item) => &mut item.attrs,
}
}
pub(crate) fn can_mismatch(&self) -> bool {
matches!(self, Subject::Enum(_))
}
pub(crate) fn has_conversions(&self) -> bool {
matches!(self, Subject::Enum(_))
}
pub(crate) fn has_one_target(&self) -> bool {
matches!(self, Subject::Struct(_))
}
pub(crate) fn target_noun(&self) -> &'static str {
match self {
Subject::Enum(_) => "variant",
Subject::Struct(_) => "field",
}
}
pub(crate) fn without_inner_attributes(mut self) -> Self {
match &mut self {
Subject::Enum(item) => {
for variant in &mut item.variants {
variant
.attrs
.retain(|attribute| !is_dispatch_attribute(attribute));
for field in &mut variant.fields {
field
.attrs
.retain(|attribute| !is_dispatch_attribute(attribute));
}
}
}
Subject::Struct(item) => {
for field in &mut item.fields {
field
.attrs
.retain(|attribute| !is_dispatch_attribute(attribute));
}
}
}
self
}
pub(crate) fn entries(&self) -> Result<Vec<Entry>> {
self.entries_for(None)
}
pub(crate) fn entries_for(&self, trait_path: Option<&Path>) -> Result<Vec<Entry>> {
match self {
Subject::Enum(item) => item.variants.iter().map(variant_entry).collect(),
Subject::Struct(item) => Ok(vec![Entry::Target(struct_target(item, trait_path)?)]),
}
}
pub(crate) fn field_traits(&self) -> Vec<(Ident, Path)> {
let Subject::Struct(item) = self else {
return vec![];
};
let mut named = vec![];
for (index, field) in item.fields.iter().enumerate() {
let Ok(options) = TargetOptions::read(&field.attrs, false) else {
continue;
};
let ident = match &field.ident {
Some(ident) => ident.clone(),
None => format_ident!("_{}", index, span = field.ty.span()),
};
named.extend(options.traits.into_iter().map(|path| (ident.clone(), path)));
}
named
}
}
impl Parse for Subject {
fn parse(input: ParseStream) -> Result<Self> {
match input.parse::<Item>()? {
Item::Enum(item) => Ok(Subject::Enum(item)),
Item::Struct(item) => Ok(Subject::Struct(item)),
other => Err(Error::new_spanned(other, "expected an enum or a struct")),
}
}
}
impl ToTokens for Subject {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
Subject::Enum(item) => item.to_tokens(tokens),
Subject::Struct(item) => item.to_tokens(tokens),
}
}
}
#[derive(Clone)]
pub(crate) struct Target {
pub(crate) ident: Ident,
pub(crate) place: Place,
pub(crate) payload: Type,
pub(crate) cfgs: Vec<Attribute>,
pub(crate) route: Route,
}
#[derive(Clone)]
pub(crate) enum Place {
Variant {
conversions: Conversions,
field: Option<Member>,
},
Field { member: Member, rebuildable: bool },
}
#[derive(Clone, Copy)]
pub(crate) struct Conversions {
pub(crate) from: bool,
pub(crate) try_from: Option<TryFromForms>,
}
impl Target {
pub(crate) fn conversions(&self) -> Option<Conversions> {
match &self.place {
Place::Variant {
conversions,
field: None,
} => Some(*conversions),
Place::Variant { .. } | Place::Field { .. } => None,
}
}
pub(crate) fn pattern(&self, prefix: &Path, binding: &Ident) -> TokenStream2 {
let ident = &self.ident;
match &self.place {
Place::Variant { field: None, .. } => quote!(#prefix::#ident(#binding)),
Place::Variant {
field: Some(Member::Unnamed(index)),
..
} => {
let skipped = (0..index.index).map(|_| quote!(_));
quote!(#prefix::#ident(#(#skipped,)* #binding, ..))
}
Place::Variant {
field: Some(member),
..
} => quote!(#prefix::#ident { #member: #binding, .. }),
Place::Field {
member: Member::Unnamed(index),
..
} => {
let skipped = (0..index.index).map(|_| quote!(_));
quote!(#prefix(#(#skipped,)* #binding, ..))
}
Place::Field { member, .. } => quote!(#prefix { #member: #binding, .. }),
}
}
pub(crate) fn rebuild(&self, prefix: &Path, value: TokenStream2) -> Option<TokenStream2> {
let ident = &self.ident;
match &self.place {
Place::Variant { field: None, .. } => Some(quote!(#prefix::#ident(#value))),
Place::Variant { .. } => None,
Place::Field {
member: Member::Unnamed(_),
rebuildable: true,
} => Some(quote!(#prefix(#value))),
Place::Field {
member,
rebuildable: true,
} => Some(quote!(#prefix { #member: #value })),
Place::Field {
rebuildable: false, ..
} => None,
}
}
pub(crate) fn constructor(&self, prefix: &Path) -> Option<TokenStream2> {
let ident = &self.ident;
match &self.place {
Place::Variant { field: None, .. } => Some(quote!(#prefix::#ident)),
Place::Variant { .. } => None,
Place::Field {
member: Member::Unnamed(_),
rebuildable: true,
} => Some(quote!(#prefix)),
Place::Field {
member,
rebuildable: true,
} => {
let value = Ident::new("__value", Span::mixed_site());
Some(quote!(|#value| #prefix { #member: #value }))
}
Place::Field {
rebuildable: false, ..
} => None,
}
}
pub(crate) fn target_type(&self) -> Option<TokenStream2> {
Self::routed_type(&self.payload, &self.route)
}
pub(crate) fn target_type_outside(&self, self_type: &Type) -> Option<TokenStream2> {
let mut payload = self.payload.clone();
SelfReplacer(self_type.clone()).visit_type_mut(&mut payload);
Self::routed_type(&payload, &self.route)
}
fn routed_type(payload: &Type, route: &Route) -> Option<TokenStream2> {
if !route.names_the_target() {
return None;
}
if route.is_deref() {
Some(quote!(<#payload as ::core::ops::Deref>::Target))
} else {
Some(quote!(#payload))
}
}
pub(crate) fn bounded_parameter(&self, generics: &syn::Generics) -> Option<&Ident> {
let candidate = if self.route.is_direct() {
bare_ident(&self.payload)
} else if self.route.is_deref() {
match &self.payload {
Type::Reference(reference) => bare_ident(&reference.elem),
other => match last_segment(other).map(generic_type_args) {
Some(arguments) if arguments.len() == 1 => bare_ident(arguments[0]),
_ => None,
},
}
} else {
None
}?;
generics
.params
.iter()
.any(|param| matches!(param, GenericParam::Type(param) if param.ident == *candidate))
.then_some(candidate)
}
pub(crate) fn holds_box(&self) -> bool {
self.route.is_deref()
&& last_segment(&self.payload).is_some_and(|segment| segment.ident == "Box")
}
}
pub(crate) struct SelfReplacer(pub(crate) Type);
impl VisitMut for SelfReplacer {
fn visit_type_mut(&mut self, ty: &mut Type) {
if is_self(ty) {
*ty = self.0.clone();
return;
}
visit_mut::visit_type_mut(self, ty);
}
}
pub(crate) struct Unforwardable {
pub(crate) ident: Ident,
pub(crate) cfgs: Vec<Attribute>,
pub(crate) reason: String,
pub(crate) error: Error,
}
impl Unforwardable {
pub(crate) fn compile_error(&self) -> TokenStream2 {
let cfgs = &self.cfgs;
let error = self.error.to_compile_error();
quote! { #(#cfgs)* #error }
}
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum Entry {
Target(Target),
Unforwardable(Unforwardable),
}
pub(crate) fn targets(entries: &[Entry]) -> Vec<&Target> {
entries
.iter()
.filter_map(|entry| match entry {
Entry::Target(target) => Some(target),
Entry::Unforwardable(_) => None,
})
.collect()
}
pub(crate) fn shape_errors(entries: &[Entry]) -> TokenStream2 {
entries
.iter()
.filter_map(|entry| match entry {
Entry::Unforwardable(unforwardable) => Some(unforwardable.compile_error()),
Entry::Target(_) => None,
})
.collect()
}
pub(crate) fn variant_entry(variant: &Variant) -> Result<Entry> {
let TargetOptions {
route,
from,
try_from,
traits: _,
} = TargetOptions::read(&variant.attrs, true)?;
let ident = &variant.ident;
let cfgs = cfg_attributes(&variant.attrs);
let marked: Vec<(Member, &Field)> = variant
.fields
.iter()
.enumerate()
.filter(|(_, field)| field.attrs.iter().any(is_dispatch_attribute))
.map(|(index, field)| (member_of(field, index), field))
.collect();
let (member, field) = match (&variant.fields, marked.as_slice()) {
(Fields::Unnamed(fields), []) if fields.unnamed.len() == 1 => (None, &fields.unnamed[0]),
(Fields::Unnamed(fields), [(member, field)]) if fields.unnamed.len() == 1 => {
let _ = member;
(None, *field)
}
(_, [(member, field)]) => (Some(member.clone()), *field),
(_, [_, (_, second), ..]) => {
return Err(Error::new_spanned(
second,
format!("only one field of `{ident}` can be forwarded to; `#[dispatch]` marks it"),
))
}
(fields, []) => {
let reason = match fields {
Fields::Unnamed(fields) if fields.unnamed.is_empty() => {
"holds no payload".to_owned()
}
Fields::Unnamed(fields) => {
format!("holds {} fields, not one payload", fields.unnamed.len())
}
Fields::Named(_) => "has named fields, not one payload".to_owned(),
Fields::Unit => "holds no payload".to_owned(),
};
let marking = match fields {
Fields::Unit => "",
Fields::Unnamed(fields) if fields.unnamed.is_empty() => "",
_ => ", mark the field to forward to with `#[dispatch]`",
};
return Ok(Entry::Unforwardable(Unforwardable {
ident: ident.clone(),
cfgs,
error: Error::new_spanned(
variant,
format!("`{ident}` {reason}, so no trait method can be forwarded to it; write it as `{ident}(Payload)`{marking}, or forward inherent methods instead, whose body answers for it"),
),
reason,
}));
}
};
let field_options = TargetOptions::read(&field.attrs, false)?;
let route = match (route, field_options.route) {
(Some(_), Some(_)) => {
return Err(Error::new_spanned(
field,
format!("`{ident}` and its field both give a route; give one"),
))
}
(route, field_route) => route.or(field_route).unwrap_or_default(),
};
Ok(Entry::Target(Target {
ident: ident.clone(),
place: Place::Variant {
conversions: Conversions { from, try_from },
field: member,
},
payload: field.ty.clone(),
cfgs,
route,
}))
}
fn member_of(field: &Field, index: usize) -> Member {
match &field.ident {
Some(ident) => Member::Named(ident.clone()),
None => Member::Unnamed(Index::from(index)),
}
}
pub(crate) fn struct_target(item: &ItemStruct, trait_path: Option<&Path>) -> Result<Target> {
let fields: Vec<(Member, &Field, TargetOptions)> = item
.fields
.iter()
.enumerate()
.map(|(index, field)| {
let member = match &field.ident {
Some(ident) => Member::Named(ident.clone()),
None => Member::Unnamed(Index::from(index)),
};
Ok((member, field, TargetOptions::read(&field.attrs, false)?))
})
.collect::<Result<_>>()?;
let marked: Vec<&(Member, &Field, TargetOptions)> = fields
.iter()
.filter(|(_, field, _)| field.attrs.iter().any(is_dispatch_attribute))
.collect();
let bare: Vec<&&(Member, &Field, TargetOptions)> = marked
.iter()
.filter(|(_, _, options)| options.traits.is_empty())
.collect();
let same_trait = |path: &Path| {
trait_path.is_some_and(|wanted| {
wanted.segments.last().map(|segment| &segment.ident)
== path.segments.last().map(|segment| &segment.ident)
})
};
let named: Vec<&&(Member, &Field, TargetOptions)> = marked
.iter()
.filter(|(_, _, options)| options.traits.iter().any(same_trait))
.collect();
if let [_, (_, second, _), ..] = bare.as_slice() {
return Err(Error::new_spanned(
second,
"only one field can be the target of every trait not named elsewhere; name the traits on the others",
));
}
if let [_, (_, second, _), ..] = named.as_slice() {
let trait_ident = trait_path
.and_then(|path| path.segments.last())
.expect("named a trait");
return Err(Error::new_spanned(
second,
format!(
"`{}` is named on two fields; one is its target",
trait_ident.ident
),
));
}
let chosen = match (
fields.as_slice(),
named.first(),
bare.first(),
marked.first(),
) {
([], _, _, _) => {
return Err(Error::new_spanned(
&item.ident,
"a dispatch struct needs a field to forward to",
))
}
(_, Some(named), _, _) => **named,
(_, None, Some(bare), _) => **bare,
([one], None, None, _) => one,
(_, None, None, Some(any)) if trait_path.is_none() => *any,
(_, None, None, _) => {
let about = match trait_path.and_then(|path| path.segments.last()) {
Some(segment) => format!(" `{}` to", segment.ident),
None => String::new(),
};
return Err(Error::new_spanned(
&item.ident,
format!(
"`{}` has several fields; mark the one to forward{about} with `#[dispatch]`, or name the trait on it, `#[dispatch(Trait)]`",
item.ident
),
));
}
};
let (member, field, options) = chosen;
let member = member.clone();
let route = options.route.clone();
let ident = match &member {
Member::Named(ident) => ident.clone(),
Member::Unnamed(index) => format_ident!("_{}", index.index, span = index.span),
};
Ok(Target {
ident,
place: Place::Field {
member,
rebuildable: fields.len() == 1,
},
payload: field.ty.clone(),
cfgs: vec![],
route: route.unwrap_or_default(),
})
}
pub(crate) fn cfg_attributes(attributes: &[Attribute]) -> Vec<Attribute> {
attributes
.iter()
.filter(|attribute| attribute.path().is_ident("cfg"))
.cloned()
.collect()
}
pub(crate) fn carried_attributes(attributes: &[Attribute]) -> Vec<Attribute> {
attributes
.iter()
.filter(|attribute| {
let path = attribute.path();
path.is_ident("cfg") || (path.is_ident("allow") && written_by_hand(attribute))
})
.cloned()
.collect()
}
fn written_by_hand(attribute: &Attribute) -> bool {
attribute
.path()
.get_ident()
.and_then(|ident| ident.span().source_text())
.is_some_and(|text| text == "allow")
}