use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::visit::{self, Visit};
use syn::{
Attribute, FnArg, GenericParam, Ident, ItemTrait, Pat, ReturnType, Signature, Token, TraitItem,
TraitItemFn, Type, TypeParamBound, parse_macro_input,
};
struct Args {
shim_name: Ident,
bounds: Punctuated<TypeParamBound, Token![+]>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let shim_name = input.parse()?;
let mut bounds = Punctuated::new();
if input.peek(Token![:]) {
input.parse::<Token![:]>()?;
bounds = Punctuated::parse_separated_nonempty(input)?;
}
Ok(Args { shim_name, bounds })
}
}
#[proc_macro_attribute]
pub fn dyn_shim(attr: TokenStream, item: TokenStream) -> TokenStream {
let Args { shim_name, bounds } = parse_macro_input!(attr as Args);
let input = parse_macro_input!(item as ItemTrait);
if let Some(param) = input.generics.params.first() {
return syn::Error::new_spanned(param, "dyn_shim does not support generic source traits")
.to_compile_error()
.into();
}
for item in &input.items {
let attrs = match item {
TraitItem::Fn(item) => {
for attr in item.attrs.iter().filter(|a| a.path().is_ident("dyn_shim")) {
if let Err(err) = require_skip(attr) {
return err.to_compile_error().into();
}
}
continue;
}
TraitItem::Const(item) => &item.attrs,
TraitItem::Type(item) => &item.attrs,
TraitItem::Macro(item) => &item.attrs,
_ => continue,
};
if let Some(attr) = attrs.iter().find(|a| a.path().is_ident("dyn_shim")) {
return syn::Error::new_spanned(
attr,
"#[dyn_shim] attributes are only supported on methods",
)
.to_compile_error()
.into();
}
}
let src = &input.ident;
let vis = &input.vis;
let mut sigs = Vec::new();
let mut impls = Vec::new();
let mut skipped: Vec<(String, &str)> = Vec::new();
for item in &input.items {
let TraitItem::Fn(method) = item else {
continue;
};
match skip(method) {
Some(reason) => skipped.push((method.sig.ident.to_string(), reason)),
None => {
let (sig, body) = forward(method, src);
sigs.push(sig);
impls.push(body);
}
}
}
let mut clean = input.clone();
for item in &mut clean.items {
if let TraitItem::Fn(method) = item {
method.attrs.retain(|a| !a.path().is_ident("dyn_shim"));
}
}
for line in source_doc(&shim_name) {
clean.attrs.push(syn::parse_quote! { #[doc = #line] });
}
let doc_attrs = shim_doc(src, &skipped)
.into_iter()
.map(|line| quote! { #[doc = #line] });
let supertraits = (!bounds.is_empty()).then(|| quote! { : #bounds });
let impl_bounds = (!bounds.is_empty()).then(|| quote! { + #bounds });
quote! {
#clean
#(#doc_attrs)*
#vis trait #shim_name #supertraits {
#(#sigs)*
}
impl<__T: #src #impl_bounds> #shim_name for __T {
#(#impls)*
}
}
.into()
}
fn forward(method: &TraitItemFn, src: &Ident) -> (TokenStream2, TokenStream2) {
let mut sig = method.sig.clone();
let Some(FnArg::Receiver(recv)) = sig.inputs.first() else {
unreachable!("skip guarantees a receiver")
};
let by_value = recv.reference.is_none()
&& (recv.colon_token.is_none()
|| matches!(&*recv.ty, Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self")));
let self_expr = if by_value {
sig.inputs[0] = syn::parse_quote! { self: ::std::boxed::Box<Self> };
quote! { *self }
} else {
quote! { self }
};
let mut names = Vec::new();
for (i, arg) in sig.inputs.iter_mut().skip(1).enumerate() {
let FnArg::Typed(pat) = arg else { continue };
let id = match &*pat.pat {
Pat::Ident(p) if p.by_ref.is_none() && p.subpat.is_none() => p.ident.clone(),
_ => format_ident!("__a{i}"),
};
*pat.pat = syn::parse_quote! { #id };
names.push(id);
}
let attrs: Vec<&Attribute> = method
.attrs
.iter()
.filter(|a| !a.path().is_ident("dyn_shim"))
.collect();
let cfg_attrs: Vec<&Attribute> = attrs
.iter()
.copied()
.filter(|a| a.path().is_ident("cfg"))
.collect();
let name = &sig.ident;
let shim_sig = quote! {
#(#attrs)*
#sig ;
};
let shim_impl = quote! {
#(#cfg_attrs)*
#[allow(deprecated)]
#sig {
<__T as #src>::#name(#self_expr #(, #names)*)
}
};
(shim_sig, shim_impl)
}
fn source_doc(shim_name: &Ident) -> Vec<String> {
vec![
String::new(),
"# Dyn Compatibility".to_string(),
String::new(),
format!(
"[`{shim_name}`] is a generated dyn-compatible shim for this trait. \
Use `dyn {shim_name}` to hold implementors behind a trait object."
),
]
}
fn shim_doc(src: &Ident, skipped: &[(String, &str)]) -> Vec<String> {
let mut lines = vec![format!("Dyn-compatible shim for [`{src}`].")];
if !skipped.is_empty() {
lines.push(String::new());
lines.push("These methods of the source trait are not dyn-compatible, so they".to_string());
lines.push("are not part of this shim. Call them on the concrete type.".to_string());
lines.push(String::new());
for (name, reason) in skipped {
lines.push(format!("- [`{src}::{name}`] ({reason})"));
}
}
lines
}
fn skip(method: &TraitItemFn) -> Option<&'static str> {
let sig = &method.sig;
if is_opted_out(method) {
Some("opted out with #[dyn_shim(skip)]")
} else if sig.asyncness.is_some() {
Some("async fn")
} else if !has_self_receiver(sig) {
Some("no self receiver")
} else if has_type_or_const_generics(sig) {
Some("generic type or const parameter")
} else if requires_self_sized(sig) {
Some("requires Self: Sized")
} else if signature_mentions_self_or_impl_trait(sig) {
Some("mentions Self or impl Trait")
} else {
None
}
}
fn require_skip(attr: &Attribute) -> syn::Result<()> {
let mut skip = false;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
Ok(())
} else {
Err(meta.error("unsupported dyn_shim argument, expected `skip`"))
}
})?;
if skip {
Ok(())
} else {
Err(syn::Error::new_spanned(attr, "expected #[dyn_shim(skip)]"))
}
}
fn is_opted_out(method: &TraitItemFn) -> bool {
method.attrs.iter().any(|a| a.path().is_ident("dyn_shim"))
}
fn has_self_receiver(sig: &Signature) -> bool {
matches!(sig.inputs.first(), Some(FnArg::Receiver(_)))
}
fn requires_self_sized(sig: &Signature) -> bool {
let Some(where_clause) = &sig.generics.where_clause else {
return false;
};
where_clause.predicates.iter().any(|pred| {
let syn::WherePredicate::Type(pred) = pred else {
return false;
};
let Type::Path(bounded) = &pred.bounded_ty else {
return false;
};
if bounded.qself.is_some() || !bounded.path.is_ident("Self") {
return false;
}
pred.bounds
.iter()
.any(|bound| matches!(bound, syn::TypeParamBound::Trait(t) if t.path.is_ident("Sized")))
})
}
fn has_type_or_const_generics(sig: &Signature) -> bool {
sig.generics
.params
.iter()
.any(|p| !matches!(p, GenericParam::Lifetime(_)))
}
fn signature_mentions_self_or_impl_trait(sig: &Signature) -> bool {
let return_bad =
matches!(&sig.output, ReturnType::Type(_, ty) if mentions_self_or_impl_trait(ty));
let arg_bad = sig
.inputs
.iter()
.skip(1)
.any(|arg| matches!(arg, FnArg::Typed(pat) if mentions_self_or_impl_trait(&pat.ty)));
return_bad || arg_bad
}
fn mentions_self_or_impl_trait(ty: &Type) -> bool {
struct Finder(bool);
impl<'ast> Visit<'ast> for Finder {
fn visit_path(&mut self, path: &'ast syn::Path) {
if path.segments.iter().any(|s| s.ident == "Self") {
self.0 = true;
}
visit::visit_path(self, path);
}
fn visit_type_impl_trait(&mut self, it: &'ast syn::TypeImplTrait) {
self.0 = true;
visit::visit_type_impl_trait(self, it);
}
}
let mut finder = Finder(false);
finder.visit_type(ty);
finder.0
}