use std::collections::hash_map::Entry as MapEntry;
use std::collections::HashMap;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::visit_mut::VisitMut;
use syn::{
parse_quote_spanned, Attribute, Block, Error, Expr, Generics, Ident, ImplItem, ItemImpl,
ItemTrait, Meta, Path, Result, Signature, Token, TraitItem, TraitItemConst, TraitItemFn,
TraitItemType, Type,
};
use crate::agreement::Agreement;
use crate::forwarding::{
ArgumentNames, Arm, ArmContext, Callee, Forwarding, MethodPlan, SelfArguments,
};
use crate::options::{
closest, is_dispatch_attribute, listed, method_mismatch, ForwardingOptions, Override, TraitSpec,
};
use crate::protocol::ImplementInput;
use crate::selection::{check_body, one_attribute, select_targets, BodylessMethod, Selection};
use crate::shape::{takes_self_argument, ReturnSite};
use crate::subject::{
allow_deprecated, carried_attributes, cfg_attributes, is_deprecated, shape_errors, targets,
Entry, Subject, Target,
};
use crate::substitution::Substitution;
use crate::{at, combined};
pub(crate) fn expand_implement(input: ImplementInput) -> Result<TokenStream2> {
let ImplementInput {
item_trait,
remote,
provided,
subject,
subject_remote,
mut spec,
partial,
} = input;
let Some(spec_path) = spec.path.clone() else {
return Err(Error::new(
Span::call_site(),
"the trait to implement is missing",
));
};
let restated = remote.is_some();
let implemented = implemented_path(remote, &spec_path);
let trait_path = &implemented;
let trait_ident = &trait_path
.segments
.last()
.expect("a path has at least one segment")
.ident;
let header = ImplHeader::new(
partial.as_ref(),
&subject,
subject_remote.as_ref(),
&item_trait,
&mut spec,
)?;
let subject_path = subject_remote
.clone()
.unwrap_or_else(|| Path::from(subject.ident().clone()));
let ImplHeader {
generics,
self_ty,
attrs: impl_attrs,
unsafety,
written,
methods,
user_items,
} = &header;
let site = at(subject.ident().span());
let derived = partial
.is_none()
.then(|| quote_spanned!(site => #[automatically_derived]));
let block_header = {
let (impl_generics, ty_generics, _) = subject.generics().split_for_impl();
spelled(
"e_spanned!(site => impl #impl_generics #spec_path for #subject_path #ty_generics),
)
};
let mut entries = subject.entries_for(Some(&spec_path))?;
if let Some(block) = &partial {
if let Some(mut renaming) = Substitution::of_subject(subject.generics(), &block.self_ty) {
for entry in &mut entries {
if let Entry::Target(target) = entry {
renaming.visit_type_mut(&mut target.payload);
}
}
}
}
let route_errors = apply_routes(&spec, &mut entries, &subject);
let variants = targets(&entries);
let generics = match partial {
Some(_) => (*generics).clone(),
None => with_payload_bounds(generics, &variants, subject.generics(), trait_path),
};
let generics = &generics;
let (impl_generics, _, where_clause) = generics.split_for_impl();
if variants.is_empty() {
if entries.is_empty() {
return Err(Error::new_spanned(
&subject,
"a dispatch enum needs at least one variant",
));
}
return Ok(match &partial {
Some(_) => shape_errors(&entries),
None => TokenStream2::new(),
});
}
let first = variants
.iter()
.find(|target| target.cfgs.is_empty() && target.target_type().is_some())
.copied();
let supplier = Supplier {
first,
subject: subject.ident(),
trait_path,
};
let mut writer = ItemWriter {
context: MethodContext {
entries: &entries,
subject: &subject,
subject_path: &subject_path,
trait_path,
spec: &spec,
block_header: &block_header,
provided: &provided,
},
supplier: &supplier,
written,
methods,
trait_ident,
restated,
substitution: Substitution::of_trait(&item_trait, trait_path, generics)?,
site,
items: TokenStream2::new(),
agreed_types: vec![],
agreed_consts: vec![],
deprecated_items: false,
plain_forwarding: false,
errors: vec![],
};
for item in &item_trait.items {
writer.write(item);
}
let ItemWriter {
items,
agreed_types,
agreed_consts,
deprecated_items,
plain_forwarding,
errors,
..
} = writer;
let mut reported = route_errors;
reported.extend(unknown_names(
&spec,
&item_trait,
trait_ident,
&subject,
partial.is_none(),
));
if let Some(error) = combined(errors) {
return Err(error);
}
let reported = combined(reported).map(|error| error.to_compile_error());
let checks = first.map(|first| {
let first_outside = first
.target_type_outside(self_ty)
.expect("the supplier is chosen among the targets with a type");
let agreement = Agreement {
variants: &variants,
first,
trait_path,
first_payload: &first_outside,
generics,
self_type: self_ty,
site,
deprecated: deprecated_items,
};
let types = agreement.types(&agreed_types);
let consts = agreement.consts(&agreed_consts);
quote_spanned! { site => #types #consts }
});
let shape_errors = match &partial {
Some(_) if plain_forwarding => shape_errors(&entries),
_ => TokenStream2::new(),
};
Ok(quote_spanned! { site =>
#(#impl_attrs)*
#derived
#unsafety impl #impl_generics #trait_path for #self_ty #where_clause {
#items
#(#user_items)*
}
#checks
#shape_errors
#reported
})
}
fn apply_routes(spec: &TraitSpec, entries: &mut [Entry], subject: &Subject) -> Vec<Error> {
let mut errors = vec![];
for (variant, route) in &spec.routes {
let target = entries.iter_mut().find_map(|entry| match entry {
Entry::Target(target) if target.ident == *variant => Some(target),
_ => None,
});
match target {
Some(target) => target.route = route.clone(),
None => errors.push(Error::new(
variant.span(),
format!(
"`{}` has no variant `{variant}` to route through",
subject.ident()
),
)),
}
}
errors
}
fn unknown_names(
spec: &TraitSpec,
item_trait: &ItemTrait,
trait_ident: &Ident,
subject: &Subject,
on_subject: bool,
) -> Vec<Error> {
let mut errors = vec![];
let associated: Vec<String> = item_trait
.items
.iter()
.filter_map(|item| match item {
TraitItem::Type(assoc) => Some(assoc.ident.to_string()),
TraitItem::Const(constant) => Some(constant.ident.to_string()),
_ => None,
})
.collect();
for fixed in &spec.overrides {
let name = fixed.name.to_string();
if associated.contains(&name) {
continue;
}
let hint = unknown_override_hint(&name, &associated, subject, on_subject);
errors.push(Error::new(
fixed.name.span(),
format!("`{trait_ident}` has no associated type or const `{name}`; {hint}"),
));
}
let deciding: Vec<String> = item_trait
.items
.iter()
.filter_map(|item| match item {
TraitItem::Fn(function) if takes_self_argument(&function.sig) => {
Some(function.sig.ident.to_string())
}
_ => None,
})
.collect();
for (method, _) in &spec.mismatch {
let name = method.to_string();
if deciding.contains(&name) {
continue;
}
let hint = match closest(&name, deciding.iter().map(String::as_str)) {
Some(near) => format!("did you mean `{near}`?"),
None if deciding.is_empty() => "none of its methods takes one".to_owned(),
None => format!("its methods taking one are {}", listed(&deciding)),
};
errors.push(Error::new(
method.span(),
format!("`{trait_ident}` has no method `{name}` with a `Self`, `&Self` or `&mut Self` argument, so there is no mismatch to decide; {hint}"),
));
}
errors
}
enum ItemPlan<'a> {
Skip,
Rejected(&'a TraitItemFn, Error),
Type(&'a TraitItemType),
Const(&'a TraitItemConst),
Method {
function: &'a TraitItemFn,
forwarded: Option<&'a Forwarded>,
complement: Option<TokenStream2>,
},
Limit(Error),
}
struct ItemWriter<'a> {
context: MethodContext<'a>,
supplier: &'a Supplier<'a>,
written: &'a HashMap<String, Written>,
methods: &'a HashMap<String, BlockMethod>,
trait_ident: &'a Ident,
restated: bool,
substitution: Substitution,
site: Span,
items: TokenStream2,
agreed_types: Vec<&'a Ident>,
agreed_consts: Vec<(&'a Ident, Type)>,
deprecated_items: bool,
plain_forwarding: bool,
errors: Vec<Error>,
}
impl<'a> ItemWriter<'a> {
fn write(&mut self, item: &'a TraitItem) {
match item {
TraitItem::Type(assoc) => self.deprecated_items |= is_deprecated(&assoc.attrs),
TraitItem::Const(constant) => self.deprecated_items |= is_deprecated(&constant.attrs),
_ => {}
}
let generated = match self.plan(item) {
ItemPlan::Skip => Ok(TokenStream2::new()),
ItemPlan::Rejected(function, error) => {
rejected_method(function, &mut self.substitution, error)
}
ItemPlan::Type(assoc) => associated_type(
assoc,
self.context.spec,
self.supplier,
self.context.block_header,
&mut self.agreed_types,
self.site,
),
ItemPlan::Const(constant) => associated_const(
constant,
self.context.spec,
self.supplier,
&mut self.substitution,
&mut self.agreed_consts,
self.site,
),
ItemPlan::Method {
function,
forwarded,
complement,
} => {
self.plain_forwarding |= forwarded.is_none() && function.sig.receiver().is_some();
let site = self.site;
expand_method(function, &self.context, &mut self.substitution, forwarded)
.map(|forwarded| quote_spanned! { site => #complement #forwarded })
}
ItemPlan::Limit(error) => Err(error),
};
match generated {
Ok(tokens) => self.items.extend(tokens),
Err(error) => self.errors.push(error),
}
}
fn plan(&self, item: &'a TraitItem) -> ItemPlan<'a> {
let name = match item {
TraitItem::Type(assoc) => Some(&assoc.ident),
TraitItem::Const(constant) => Some(&constant.ident),
TraitItem::Fn(function) => Some(&function.sig.ident),
TraitItem::Macro(item) => {
return ItemPlan::Limit(Error::new_spanned(
item,
"macro invocations inside the trait are not supported",
))
}
_ => return ItemPlan::Skip,
};
let written = name.and_then(|name| self.written.get(&name.to_string()));
let (complement, written_at) = match written {
Some(Written::Always) => return ItemPlan::Skip,
Some(Written::Under { conditions, span }) => {
(Some(not_all_of(conditions, at(*span))), Some(*span))
}
None => (None, None),
};
let block_method = name.and_then(|name| self.methods.get(&name.to_string()));
match (item, written_at, block_method) {
(TraitItem::Type(assoc), Some(span), _) => ItemPlan::Limit(Error::new(
span,
format!("`{}` is declared under `#[cfg]`, and an associated type is declared in every build, since the forwarded methods convert into it; put the whole block under the `cfg`, with a second block for the other builds", assoc.ident),
)),
(TraitItem::Const(constant), Some(span), _) => ItemPlan::Limit(Error::new(
span,
format!("`{}` is given under `#[cfg]`, and an associated const is given in every build; put the whole block under the `cfg`, with a second block for the other builds", constant.ident),
)),
(TraitItem::Type(assoc), None, _) => ItemPlan::Type(assoc),
(TraitItem::Const(constant), None, _) => ItemPlan::Const(constant),
(TraitItem::Fn(function), _, Some(BlockMethod::Default(span))) => {
let provided = self.restated
|| function.default.is_some()
|| self.context.provided.contains(&function.sig.ident);
if provided {
return ItemPlan::Skip;
}
ItemPlan::Rejected(
function,
Error::new(
*span,
format!("`{}` has no default body in `{}` to be left to; give it a body, or forward it", function.sig.ident, self.trait_ident),
),
)
}
(TraitItem::Fn(function), _, block_method) => ItemPlan::Method {
function,
forwarded: match block_method {
Some(BlockMethod::Forwarded(forwarded)) => Some(forwarded),
_ => None,
},
complement,
},
_ => ItemPlan::Skip,
}
}
}
fn unknown_override_hint(
name: &str,
associated: &[String],
subject: &Subject,
on_subject: bool,
) -> String {
if let Some(near) = closest(name, associated.iter().map(String::as_str)) {
return format!("did you mean `{near}`?");
}
let switches = if on_subject {
&["from", "try_from"][..]
} else {
&[]
};
if let Some(near) = closest(name, switches.iter().copied()) {
return format!("did you mean the enum's option `{near} = ..`?");
}
if matches!(name, "target" | "to" | "field" | "delegate" | "via") {
return if subject.has_one_target() {
"a struct forwards to its one field, or to the field marked `#[dispatch]`".to_owned()
} else {
"an enum forwards to every variant, and an inherent method names the variants it forwards to".to_owned()
};
}
let options = if on_subject {
"`mismatch(method = expr)`, `from = false` and `try_from = true`"
} else {
"`mismatch(method = expr)`"
};
if associated.is_empty() {
return format!("it has none, and the other options are {options}");
}
format!(
"its associated items are {}, and the other options are {options}",
listed(associated)
)
}
fn with_payload_bounds(
generics: &Generics,
variants: &[&Target],
declared: &Generics,
trait_path: &Path,
) -> Generics {
let mut generics = generics.clone();
let mut bounded: Vec<&Ident> = vec![];
for target in variants {
let Some(parameter) = target.bounded_parameter(declared) else {
continue;
};
if bounded.contains(¶meter) {
continue;
}
bounded.push(parameter);
let span = at(parameter.span());
generics
.make_where_clause()
.predicates
.push(parse_quote_spanned!(span => #parameter: #trait_path));
}
generics
}
fn spelled(tokens: &impl ToTokens) -> String {
tokens
.to_token_stream()
.to_string()
.replace(" :: ", "::")
.replace(" < ", "<")
.replace("< ", "<")
.replace(" >", ">")
.replace(" ,", ",")
.replace("& ", "&")
.replace(" : ", ": ")
}
fn implemented_path(remote: Option<Path>, named: &Path) -> Path {
match remote {
Some(mut remote) => {
if let (Some(last), Some(named)) = (remote.segments.last_mut(), named.segments.last()) {
last.arguments = named.arguments.clone();
}
remote
}
None => named.clone(),
}
}
struct ImplHeader<'a> {
generics: &'a Generics,
self_ty: Type,
attrs: &'a [Attribute],
unsafety: Option<Token![unsafe]>,
written: HashMap<String, Written>,
methods: HashMap<String, BlockMethod>,
user_items: Vec<&'a ImplItem>,
}
#[allow(clippy::large_enum_variant)]
enum BlockMethod {
Default(Span),
Forwarded(Forwarded),
}
struct Forwarded {
attrs: Vec<Attribute>,
sig: Signature,
body: Option<Block>,
forwarding: ForwardingOptions,
}
impl Forwarded {
fn read(
attribute: Attribute,
mut attrs: Vec<Attribute>,
sig: Signature,
body: Option<Block>,
) -> Result<BlockMethod> {
if let Meta::List(list) = &attribute.meta {
if list.tokens.to_string() == "default" {
if let Some(body) = &body {
return Err(Error::new_spanned(
body,
format!("`{}` is left to the trait's default, so it takes no body; end the signature with `;`", sig.ident),
));
}
return Ok(BlockMethod::Default(attribute.span()));
}
}
let forwarding = one_attribute(&mut attrs, &attribute)?;
let inherent_only = [
forwarding.call.as_ref().map(|_| "`call`"),
forwarding.with.as_ref().map(|_| "`with`"),
forwarding.field.as_ref().map(|_| "`field`"),
forwarding.value.then_some("`value`"),
];
if let Some(option) = inherent_only.into_iter().flatten().next() {
return Err(Error::new_spanned(
&attribute,
format!("{option} is an option of an inherent method; a trait method is called by its own name on every variant it names, and takes `via` only"),
));
}
Ok(BlockMethod::Forwarded(Forwarded {
attrs,
sig,
body,
forwarding,
}))
}
}
enum Written {
Always,
Under {
conditions: Vec<Vec<Attribute>>,
span: Span,
},
}
impl Written {
fn new(cfgs: Vec<Attribute>, span: Span) -> Self {
if cfgs.is_empty() {
Written::Always
} else {
Written::Under {
conditions: vec![cfgs],
span,
}
}
}
fn add(&mut self, cfgs: Vec<Attribute>) {
match self {
Written::Always => {}
Written::Under { .. } if cfgs.is_empty() => *self = Written::Always,
Written::Under { conditions, .. } => conditions.push(cfgs),
}
}
}
fn not_all_of(conditions: &[Vec<Attribute>], span: Span) -> TokenStream2 {
let predicates = conditions.iter().map(|cfgs| {
let predicates = cfgs.iter().map(|attribute| match &attribute.meta {
syn::Meta::List(list) => list.tokens.clone(),
_ => TokenStream2::new(),
});
quote_spanned!(span => all(#(#predicates),*))
});
quote_spanned!(span => #[cfg(not(any(#(#predicates),*)))])
}
impl<'a> ImplHeader<'a> {
fn new(
partial: Option<&'a ItemImpl>,
subject: &'a Subject,
remote: Option<&Path>,
item_trait: &ItemTrait,
spec: &mut TraitSpec,
) -> Result<Self> {
let Some(block) = partial else {
let (_, ty_generics, _) = subject.generics().split_for_impl();
let path = remote
.cloned()
.unwrap_or_else(|| Path::from(subject.ident().clone()));
let site = at(subject.ident().span());
return Ok(Self {
generics: subject.generics(),
self_ty: parse_quote_spanned!(site => #path #ty_generics),
attrs: &[],
unsafety: item_trait.unsafety,
written: HashMap::new(),
methods: HashMap::new(),
user_items: vec![],
});
};
let mut written: HashMap<String, Written> = HashMap::new();
let mut methods: HashMap<String, BlockMethod> = HashMap::new();
let mut user_items = vec![];
for item in &block.items {
let (name, attrs) = match item {
ImplItem::Fn(function) if function.attrs.iter().any(is_dispatch_attribute) => {
let mut attrs = function.attrs.clone();
let position = attrs
.iter()
.position(is_dispatch_attribute)
.expect("checked above");
let attribute = attrs.remove(position);
let method = Forwarded::read(
attribute,
attrs,
function.sig.clone(),
Some(function.block.clone()),
)?;
methods.insert(function.sig.ident.to_string(), method);
continue;
}
ImplItem::Verbatim(tokens) => {
let Ok(method) = syn::parse2::<BodylessMethod>(tokens.clone()) else {
user_items.push(item);
continue;
};
let BodylessMethod {
mut attrs,
vis: _,
sig,
} = method;
let Some(position) = attrs.iter().position(is_dispatch_attribute) else {
return Err(Error::new_spanned(
&sig,
format!("`{}` has no body; forward it with `#[dispatch(*)]` or `#[dispatch(Variant, ..)]`, or leave it to the trait's default with `#[dispatch(default)]`", sig.ident),
));
};
let attribute = attrs.remove(position);
let name = sig.ident.to_string();
methods.insert(name, Forwarded::read(attribute, attrs, sig, None)?);
continue;
}
ImplItem::Fn(function) => (&function.sig.ident, &function.attrs),
ImplItem::Type(assoc) => {
spec.overrides
.push(Override::of_type(assoc.ident.clone(), &assoc.ty));
(&assoc.ident, &assoc.attrs)
}
ImplItem::Const(constant) => (&constant.ident, &constant.attrs),
_ => continue,
};
let cfgs = cfg_attributes(attrs);
match written.entry(name.to_string()) {
MapEntry::Occupied(entry) => entry.into_mut().add(cfgs),
MapEntry::Vacant(entry) => {
entry.insert(Written::new(cfgs, item.span()));
}
}
user_items.push(item);
}
Ok(Self {
generics: &block.generics,
self_ty: (*block.self_ty).clone(),
attrs: &block.attrs,
unsafety: block.unsafety,
written,
methods,
user_items,
})
}
}
struct Supplier<'a> {
first: Option<&'a Target>,
subject: &'a Ident,
trait_path: &'a Path,
}
impl Supplier<'_> {
fn item(&self, name: &Ident) -> Result<TokenStream2> {
let Some(first) = self.first else {
return Err(Error::new_spanned(
self.subject,
format!("every variant of `{}` is under `#[cfg]` or reached through a field or a method, so none can supply `{name}` in every build; fix it on the enum with `{name} = ..`", self.subject),
));
};
let payload = first
.target_type()
.expect("the supplier is chosen among the targets with a type");
let trait_path = self.trait_path;
let span = at(first.payload.span());
Ok(quote_spanned!(span => <#payload as #trait_path>::#name))
}
}
fn associated_type<'a>(
assoc: &'a TraitItemType,
spec: &TraitSpec,
supplier: &Supplier,
block_header: &str,
agreed: &mut Vec<&'a Ident>,
site: Span,
) -> Result<TokenStream2> {
let name = &assoc.ident;
if !assoc.generics.params.is_empty() {
let parameters = spelled(&assoc.generics);
return Err(Error::new_spanned(
assoc,
format!("`{name}` is a generic associated type, which only the enum can declare; write `type {name}{parameters} = ..;` in a `#[whichever::dispatch] {block_header}` block"),
));
}
let mut carried = carried_attributes(&assoc.attrs);
if is_deprecated(&assoc.attrs) {
carried.push(allow_deprecated(site));
}
let cfgs = carried
.iter()
.filter(|attribute| attribute.path().is_ident("cfg"));
let ty = match spec.override_for(name) {
Some(fixed) => fixed.as_type()?.into_token_stream(),
None => {
if cfgs.count() == 0 {
agreed.push(name);
}
supplier.item(name)?
}
};
Ok(quote_spanned! { site => #(#carried)* type #name = #ty; })
}
fn associated_const<'a>(
constant: &'a TraitItemConst,
spec: &TraitSpec,
supplier: &Supplier,
substitution: &mut Substitution,
agreed: &mut Vec<(&'a Ident, Type)>,
site: Span,
) -> Result<TokenStream2> {
let name = &constant.ident;
let mut carried = carried_attributes(&constant.attrs);
if is_deprecated(&constant.attrs) {
carried.push(allow_deprecated(site));
}
let cfgs = carried
.iter()
.filter(|attribute| attribute.path().is_ident("cfg"));
let mut ty = constant.ty.clone();
substitution.visit_type_mut(&mut ty);
substitution.check()?;
let value = match spec.override_for(name) {
Some(fixed) => fixed.as_value()?.into_token_stream(),
None => {
if cfgs.count() == 0 {
agreed.push((name, ty.clone()));
}
supplier.item(name)?
}
};
Ok(quote_spanned! { site => #(#carried)* const #name: #ty = #value; })
}
fn expand_method(
function: &TraitItemFn,
context: &MethodContext,
substitution: &mut Substitution,
forwarded: Option<&Forwarded>,
) -> Result<TokenStream2> {
let signature = match forwarded {
Some(forwarded) => forwarded.sig.clone(),
None => {
let mut signature = function.sig.clone();
substitution.visit_signature_mut(&mut signature);
substitution.check()?;
signature
}
};
let written = forward_trait_method(function, signature.clone(), context, forwarded);
Ok(written.unwrap_or_else(|error| {
let carried = match forwarded {
Some(forwarded) => forwarded.attrs.clone(),
None => carried_attributes(&function.attrs),
};
error_body(&signature, carried, error)
}))
}
fn check_trait_objects(
plan: &MethodPlan,
selection: Option<&Selection>,
entries: &[Entry],
function: &TraitItemFn,
block_header: &str,
) -> Result<()> {
if !plan.requires_sized() {
return Ok(());
}
let forwarded_to: Vec<&Target> = match selection {
Some(selection) => selection.targets.iter().collect(),
None => targets(entries),
};
let Some(target) = forwarded_to.iter().find(|target| target.is_trait_object()) else {
return Ok(());
};
let method = &function.sig.ident;
Err(Error::new_spanned(
&function.sig,
format!("`{method}` requires `Self: Sized`, and `{}` holds a trait object, which is not; leave it to the trait's default with `#[dispatch(default)] fn {method}(..);` in a `#[whichever::dispatch] {block_header}` block, or implement the method by hand", target.ident),
))
}
struct Fallback {
arm: Option<Arm>,
allow_unreachable: bool,
}
fn fallback_for(
plan: &MethodPlan,
entries: &[Entry],
subject: &Subject,
forwarded: Option<&Forwarded>,
mismatch: Option<Expr>,
function: &TraitItemFn,
context: &ArmContext,
) -> Result<Fallback> {
let always_present = entries
.iter()
.filter(|entry| matches!(entry, Entry::Target(target) if target.cfgs.is_empty()))
.count();
let conditional = entries.iter().any(|entry| match entry {
Entry::Target(target) => !target.cfgs.is_empty(),
Entry::Unforwardable(_) => true,
});
if let Some(body) = forwarded.and_then(|forwarded| forwarded.body.as_ref()) {
return Ok(Fallback {
arm: Some(Arm::rest(body.to_token_stream(), context.site)),
allow_unreachable: false,
});
}
let needs_fallback =
plan.has_same_variant() && subject.can_mismatch() && (always_present >= 2 || conditional);
if !needs_fallback {
return Ok(Fallback {
arm: None,
allow_unreachable: false,
});
}
let method = &function.sig.ident;
let Some(expr) = mismatch else {
return Err(Error::new_spanned(
&function.sig,
format!("`{method}` takes `Self` arguments; give the result for two different variants with `#[dispatch(mismatch = <expr>)]` on the trait method or `mismatch({method} = <expr>)` on the enum"),
));
};
Ok(Fallback {
arm: Some(plan.fallback(expr.to_token_stream(), context.site)),
allow_unreachable: forwarded.is_none() && always_present < 2,
})
}
fn rejected_method(
function: &TraitItemFn,
substitution: &mut Substitution,
error: Error,
) -> Result<TokenStream2> {
let mut signature = function.sig.clone();
substitution.visit_signature_mut(&mut signature);
substitution.check()?;
let carried = carried_attributes(&function.attrs);
Ok(error_body(&signature, carried, error))
}
fn error_body(signature: &Signature, carried: Vec<Attribute>, error: Error) -> TokenStream2 {
let error = error.to_compile_error();
let span = at(signature.ident.span());
quote_spanned! { span => #(#carried)* #signature { #error } }
}
struct MethodContext<'a> {
entries: &'a [Entry],
subject: &'a Subject,
subject_path: &'a Path,
trait_path: &'a Path,
spec: &'a TraitSpec,
block_header: &'a str,
provided: &'a [Ident],
}
fn forward_trait_method(
function: &TraitItemFn,
mut signature: Signature,
context: &MethodContext,
forwarded: Option<&Forwarded>,
) -> Result<TokenStream2> {
let MethodContext {
entries,
subject,
subject_path,
trait_path,
spec,
block_header,
provided,
} = *context;
let subject_ident = subject.ident();
let method = signature.ident.clone();
if signature.receiver().is_none() {
if function.default.is_some() || provided.contains(&method) {
return Ok(TokenStream2::new());
}
return Err(Error::new_spanned(
&function.sig,
format!("`{method}` has no receiver, so there is no variant to dispatch on; give it a default body, or write it in a `#[whichever::dispatch] {block_header}` block"),
));
}
let context = ArmContext {
callee: Callee::Qualified {
trait_path,
method: &method,
},
method: &method,
subject: subject_ident,
prefix: subject_path.clone(),
lift: None,
site: at(subject_ident.span()),
};
let (mismatch, resolution) = match spec.mismatch_for(&method) {
Some(expr) => (Some(expr.clone()), Some(expr.span())),
None => (method_mismatch(function)?, None),
};
let names = match forwarded {
Some(_) => ArgumentNames::Written,
None => ArgumentNames::WrittenOrGenerated,
};
let plan = MethodPlan::new(
&mut signature,
names,
SelfArguments::Dispatch,
Some(spec),
&ReturnSite {
subject: subject_ident,
one_target: subject.has_one_target(),
by_hand: context.callee.by_hand(),
},
resolution,
)?;
let selection = match forwarded {
Some(forwarded) => {
let selection = select_targets(
&forwarded.forwarding,
entries,
&context.prefix,
&method,
subject,
)?;
check_body(
&selection,
forwarded.body.as_ref(),
&signature,
subject.target_noun(),
)?;
Some(selection)
}
None => None,
};
check_trait_objects(&plan, selection.as_ref(), entries, function, block_header)?;
let mut arms = Vec::with_capacity(entries.len());
match &selection {
Some(selection) => {
for target in &selection.targets {
arms.push(plan.arm(target, &context)?);
}
}
None => {
for entry in entries {
match entry {
Entry::Target(target) => arms.push(plan.arm(target, &context)?),
Entry::Unforwardable(unforwardable) if !plan.has_same_variant() => {
let site = context.site;
arms.push(Arm::unforwardable(
&context.prefix,
unforwardable,
quote_spanned!(site => ::core::unreachable!()),
site,
));
}
Entry::Unforwardable(_) => {}
}
}
}
}
let Fallback {
arm: fallback,
allow_unreachable,
} = fallback_for(
&plan, entries, subject, forwarded, mismatch, function, &context,
)?;
if let Some(selection) = selection {
arms.extend(selection.unforwardable_arms);
}
let site = context.site;
let allow_unreachable =
allow_unreachable.then(|| quote_spanned! { site => #[allow(unreachable_patterns)] });
let own_attrs: &[Attribute] = match forwarded {
Some(forwarded) => &forwarded.attrs,
None => &function.attrs,
};
let inline = own_attrs
.iter()
.find(|attribute| attribute.path().is_ident("inline"))
.map(ToTokens::to_token_stream)
.unwrap_or_else(|| quote_spanned! { site => #[inline] });
let mut carried: Vec<Attribute> = match forwarded {
Some(forwarded) => forwarded
.attrs
.iter()
.filter(|attribute| !attribute.path().is_ident("inline"))
.cloned()
.collect(),
None => carried_attributes(&function.attrs),
};
if is_deprecated(&function.attrs) {
carried.push(allow_deprecated(site));
}
let body = Forwarding {
scrutinee: plan.scrutinee(context.site),
arms,
fallback,
future: plan.future.clone(),
}
.body(at(method.span()));
Ok(quote_spanned! { site =>
#(#carried)*
#inline
#allow_unreachable
#signature #body
})
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::parse_quote;
use super::*;
#[test]
fn the_forwarded_item_takes_the_builds_the_blocks_items_leave() {
let named: Vec<Attribute> = vec![
parse_quote!(#[cfg(feature = "named")]),
parse_quote!(#[cfg(not(test))]),
];
let unix: Vec<Attribute> = vec![parse_quote!(#[cfg(unix)])];
assert_eq!(
not_all_of(&[named.clone(), unix.clone()], Span::call_site()).to_string(),
quote!(#[cfg(not(any(all(feature = "named", not(test)), all(unix))))]).to_string()
);
let mut written = Written::new(named, Span::call_site());
assert!(matches!(&written, Written::Under { conditions, .. } if conditions.len() == 1));
written.add(unix);
assert!(matches!(&written, Written::Under { conditions, .. } if conditions.len() == 2));
written.add(vec![]);
assert!(matches!(written, Written::Always));
assert!(matches!(
Written::new(vec![], Span::call_site()),
Written::Always
));
}
}