Skip to content

Commit 16b3dc2

Browse files
authored
feat(macros): support using #[dynify] on remote items (#17)
1 parent 1474e09 commit 16b3dc2

25 files changed

+200
-45
lines changed

.github/codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ coverage:
88
default:
99
enabled: true
1010
threshold: 1%
11+
patch: false

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ follows <https://www.conventionalcommits.org/en/v1.0.0/> to track changes.
3737

3838
## [Unreleased]
3939

40+
### Added
41+
42+
- Support using `#[dynify]` on remote items ([#17])
43+
44+
[#17]: https://github.com/loichyan/dynify/pull/17
45+
4046
## [0.1.1] - 2025-08-28
4147

4248
The major update since the previous release is the introduction of the

macros/src/dynify.rs

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,52 @@
11
use proc_macro2::TokenStream;
22
use quote::{format_ident, quote, ToTokens};
3-
use syn::{parse_quote_spanned, FnArg, Ident, Lifetime, Result, ReturnType, Token, Type};
3+
use syn::parse::ParseStream;
4+
use syn::{
5+
parse_quote, parse_quote_spanned, FnArg, Ident, Lifetime, LitStr, Result, ReturnType, Token,
6+
Type,
7+
};
48

59
use crate::lifetime::TraitContext;
610
use crate::utils::*;
711

812
pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
9-
let rename = syn::parse2::<Option<Ident>>(attr)?;
13+
let opts = syn::parse2::<Options>(attr)?;
1014
let input_item = syn::parse2::<syn::Item>(input.clone())?;
15+
16+
let is_remote = opts.remote.is_some();
1117
let output = match input_item {
12-
syn::Item::Trait(t) => expand_trait(rename, t)?,
13-
syn::Item::Fn(f) => expand_fn(rename, f)?,
18+
syn::Item::Trait(t) => expand_trait(opts, t)?,
19+
syn::Item::Fn(f) => expand_fn(opts, f)?,
1420
item => {
1521
return Err(syn::Error::new_spanned(
1622
&item,
1723
"expected a `fn` or `trait` item",
1824
))
1925
},
2026
};
21-
Ok(quote!(
22-
#[allow(async_fn_in_trait)]
23-
#input
24-
#output
25-
))
26-
}
2727

28-
fn expand_trait(rename: Option<Ident>, mut dyn_trait: syn::ItemTrait) -> Result<TokenStream> {
29-
let dyn_trait_name = rename.unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident));
30-
let input_trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name);
31-
let dyn_trait_name = &dyn_trait.ident;
28+
let input = (!is_remote).then_some(input);
29+
Ok(quote!(#input #output))
30+
}
3231

33-
let impl_target = format_ident!("{}Implementor", input_trait_name);
34-
let mut trait_impl_items = TokenStream::new();
32+
fn expand_trait(opts: Options, mut dyn_trait: syn::ItemTrait) -> Result<TokenStream> {
33+
let target_trait = if let Some(remote) = opts.remote {
34+
remote
35+
} else {
36+
let dyn_trait_name = opts
37+
.rename
38+
.unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident));
39+
let target_trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name);
40+
parse_quote!(#target_trait_name)
41+
};
3542

43+
let impl_target = {
44+
let target_trait_name = &target_trait.segments.last().unwrap().ident;
45+
format_ident!("{}Implementor", target_trait_name)
46+
};
3647
let (_, ty_generics, where_clause) = dyn_trait.generics.split_for_impl();
48+
49+
let mut trait_impl_items = TokenStream::new();
3750
for item in dyn_trait.items.iter_mut() {
3851
let impl_item = match item {
3952
syn::TraitItem::Const(syn::TraitItemConst {
@@ -71,12 +84,12 @@ fn expand_trait(rename: Option<Ident>, mut dyn_trait: syn::ItemTrait) -> Result<
7184
// TODO: support nested `#[dynify]`
7285
let attrs_outer = attrs.outer();
7386
let attrs_inner = attrs.inner();
74-
let target = quote_with(|tokens| {
87+
let target_fn = quote_with(|tokens| {
7588
impl_target.to_tokens(tokens);
7689
NewToken![::].to_tokens(tokens);
7790
sig.ident.to_tokens(tokens);
7891
});
79-
let impl_body = quote_transformed_body(transformed, &target, sig);
92+
let impl_body = quote_transformed_body(transformed, &target_fn, sig);
8093
quote!(#(#attrs_outer)* #sig { #(#attrs_inner)* #impl_body })
8194
},
8295
_ => continue,
@@ -85,33 +98,42 @@ fn expand_trait(rename: Option<Ident>, mut dyn_trait: syn::ItemTrait) -> Result<
8598
}
8699

87100
let impl_generics = quote_impl_generics(&dyn_trait.generics);
101+
let dyn_trait_name = &dyn_trait.ident;
102+
88103
Ok(quote!(
89104
#[allow(async_fn_in_trait)]
90105
#[allow(clippy::type_complexity)]
91106
#dyn_trait
92107

93108
#[allow(clippy::type_complexity)]
94-
impl<#impl_generics #impl_target: #input_trait_name #ty_generics>
109+
impl<#impl_generics #impl_target: #target_trait #ty_generics>
95110
#dyn_trait_name #ty_generics for #impl_target
96111
#where_clause { #trait_impl_items }
97112
))
98113
}
99114

100-
fn expand_fn(rename: Option<Ident>, mut dyn_fn: syn::ItemFn) -> Result<TokenStream> {
115+
fn expand_fn(opts: Options, mut dyn_fn: syn::ItemFn) -> Result<TokenStream> {
101116
let syn::ItemFn {
102117
vis,
103118
sig,
104119
attrs,
105120
block: _,
106121
} = &mut dyn_fn;
107122

108-
let dyn_fn_name = rename.unwrap_or_else(|| format_ident!("dyn_{}", sig.ident));
109-
let input_fn_name = std::mem::replace(&mut sig.ident, dyn_fn_name);
123+
let target_fn = if let Some(remote) = opts.remote {
124+
remote
125+
} else {
126+
let dyn_fn_name = opts
127+
.rename
128+
.unwrap_or_else(|| format_ident!("dyn_{}", sig.ident));
129+
let target_fn_name = std::mem::replace(&mut sig.ident, dyn_fn_name);
130+
parse_quote!(#target_fn_name)
131+
};
110132

111133
let transformed = transform_fn(None, sig, true)?;
112134
let attrs_outer = attrs.outer();
113135
let attrs_inner = attrs.inner();
114-
let impl_body = quote_transformed_body(transformed, &input_fn_name, sig);
136+
let impl_body = quote_transformed_body(transformed, &target_fn, sig);
115137
Ok(quote!(#(#attrs_outer)* #vis #sig { #(#attrs_inner)* #impl_body }))
116138
}
117139

@@ -259,6 +281,39 @@ fn get_impl_type(ty: &ReturnType) -> Option<(Token![->], &syn::TypeImplTrait)> {
259281
.and_then(|(r, ty)| as_variant!(&**ty, Type::ImplTrait).map(|ty| (*r, ty)))
260282
}
261283

284+
struct Options {
285+
rename: Option<Ident>,
286+
remote: Option<syn::Path>,
287+
}
288+
289+
impl syn::parse::Parse for Options {
290+
fn parse(input: ParseStream) -> Result<Options> {
291+
syn::custom_keyword!(remote);
292+
293+
let mut attrs = Options {
294+
rename: None,
295+
remote: None,
296+
};
297+
298+
// The syntax for specifying arbitrary tokens as the value of an option
299+
// follows those used in [serde](https://github.com/serde-rs/serde).
300+
if input.peek2(Token![=]) {
301+
let remote_token = input.parse::<remote>()?;
302+
let _eq_token = input.parse::<Token![=]>()?;
303+
let remote = input.parse::<LitStr>()?.parse::<syn::Path>()?;
304+
if remote.segments.is_empty() {
305+
return Err(syn::Error::new(remote_token.span, "invalid remote type"));
306+
}
307+
attrs.remote = Some(remote);
308+
} else if !input.is_empty() {
309+
attrs.rename = Some(input.parse()?);
310+
}
311+
input.parse::<syn::parse::Nothing>()?;
312+
313+
Ok(attrs)
314+
}
315+
}
316+
262317
#[cfg(test)]
263318
#[path = "dynify_tests.rs"]
264319
mod tests;

macros/src/dynify_tests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ define_macro_tests!(
9696
quote!(my_dyn_test),
9797
quote!(async fn test(_arg1: &str) -> String { todo!() }),
9898
)]
99+
// == Remote items == //
100+
#[case::remote_trait(
101+
quote!(remote = "dynify::r#priv::TestRemoteTrait"),
102+
quote!(trait DynTestRemoteTrait { async fn test(&self, arg: &str) -> usize; }),
103+
)]
104+
#[case::remote_fn(
105+
quote!(remote = "dynify::r#priv::test_remote_fn"),
106+
quote!(async fn dyn_test_remote_fn(_arg1: &str) -> usize {}),
107+
)]
99108
fn ui(#[case] test_name: &str, #[case] attr: TokenStream, #[case] input: TokenStream) {
100109
let output = expand(attr, input).unwrap();
101110
// Append `fn main() {}` so that they can pass compile tests

macros/src/dynify_tests/fn_renamed.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* This file is @generated for testing purpose */
2-
#[allow(async_fn_in_trait)]
32
async fn test(_arg1: &str) -> String {
43
todo!()
54
}

macros/src/dynify_tests/fn_returning_async.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* This file is @generated for testing purpose */
2-
#[allow(async_fn_in_trait)]
32
async fn test(_arg1: &str) -> String {
43
todo!()
54
}

macros/src/dynify_tests/fn_returning_impl.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* This file is @generated for testing purpose */
2-
#[allow(async_fn_in_trait)]
32
fn test() -> impl core::any::Any {
43
todo!()
54
}

macros/src/dynify_tests/fn_with_vis.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* This file is @generated for testing purpose */
2-
#[allow(async_fn_in_trait)]
32
pub(crate) fn test() -> impl core::any::Any {
43
todo!()
54
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* This file is @generated for testing purpose */
2+
fn dyn_test_remote_fn<'_arg1, 'dynify>(
3+
_arg1: &'_arg1 str,
4+
) -> ::dynify::r#priv::Fn<
5+
(&'_arg1 str,),
6+
dyn 'dynify + ::core::future::Future<Output = usize>,
7+
>
8+
where
9+
'_arg1: 'dynify,
10+
{
11+
::dynify::__from_fn!([] dynify::r#priv::test_remote_fn, _arg1,)
12+
}
13+
fn main() {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* This file is @generated for testing purpose */
2+
#[allow(async_fn_in_trait)]
3+
#[allow(clippy::type_complexity)]
4+
trait DynTestRemoteTrait {
5+
fn test<'this, 'arg, 'dynify>(
6+
&'this self,
7+
arg: &'arg str,
8+
) -> ::dynify::r#priv::Fn<
9+
(::dynify::r#priv::RefSelf, &'arg str),
10+
dyn 'dynify + ::core::future::Future<Output = usize>,
11+
>
12+
where
13+
'this: 'dynify,
14+
'arg: 'dynify,
15+
Self: 'dynify;
16+
}
17+
#[allow(clippy::type_complexity)]
18+
impl<TestRemoteTraitImplementor: dynify::r#priv::TestRemoteTrait> DynTestRemoteTrait
19+
for TestRemoteTraitImplementor {
20+
fn test<'this, 'arg, 'dynify>(
21+
&'this self,
22+
arg: &'arg str,
23+
) -> ::dynify::r#priv::Fn<
24+
(::dynify::r#priv::RefSelf, &'arg str),
25+
dyn 'dynify + ::core::future::Future<Output = usize>,
26+
>
27+
where
28+
'this: 'dynify,
29+
'arg: 'dynify,
30+
Self: 'dynify,
31+
{
32+
::dynify::__from_fn!([self] TestRemoteTraitImplementor::test, self, arg,)
33+
}
34+
}
35+
fn main() {}

0 commit comments

Comments
 (0)