Skip to content

Commit

Permalink
feat: Attribute `#[delegate_to(...)]
Browse files Browse the repository at this point in the history
  • Loading branch information
kenoss committed Aug 26, 2024
1 parent 9ac4360 commit 3341315
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 10 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ rust-version = "1.80.0"
[lib]
proc-macro = true

[features]
default = []
unstable_delegate_to = []

[[test]]
name = "tests"
path = "tests/test.rs"
Expand Down
5 changes: 4 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ run *ARGS:
cargo run {{ARGS}}

test *ARGS:
cargo test {{ARGS}}
cargo test --features unstable_delegate_to {{ARGS}}

nextest-run *ARGS:
cargo nextest run --features unstable_delegate_to {{ARGS}}
30 changes: 30 additions & 0 deletions src/delegate_to_arg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use syn::parse::{Parse, ParseStream};

pub(crate) struct DelegateToArg {
pub ident: syn::Ident,
#[allow(unused)]
pub fat_arrow_token: syn::token::FatArrow,
pub expr: syn::Expr,
}

impl Parse for DelegateToArg {
fn parse(input: ParseStream) -> syn::Result<DelegateToArg> {
Ok(DelegateToArg {
ident: input.parse()?,
fat_arrow_token: input.parse()?,
expr: input.parse()?,
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use quote::quote;

#[test]
fn parsable() {
syn::parse2::<DelegateToArg>(quote! { x => &x.0 }).unwrap();
syn::parse2::<DelegateToArg>(quote! { x => x::x(&x.0) }).unwrap();
}
}
34 changes: 34 additions & 0 deletions src/delegate_to_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use syn::visit_mut::VisitMut;

pub(crate) fn check_non_existence(item: &mut syn::Item) -> syn::Result<()> {
let mut visitor = Visitor { error: None };
visitor.visit_item_mut(item);
match visitor.error {
None => Ok(()),
Some(e) => Err(e),
}
}

struct Visitor {
error: Option<syn::Error>,
}

// Use `visit_*_mut()` as we may need to change enum variant when it matches.
impl VisitMut for Visitor {
fn visit_attribute_mut(&mut self, node: &mut syn::Attribute) {
syn::visit_mut::visit_attribute_mut(self, node);

#[allow(clippy::single_match)]
match &node.meta {
syn::Meta::List(meta_list) if meta_list.path.is_ident("delegate_to") => {
self.error.get_or_insert_with(|| {
syn::Error::new_spanned(
node,
"#[delegate_to(...)] requires feature flag `unstable_delegate_to`",
)
});
}
_ => {}
}
}
}
24 changes: 24 additions & 0 deletions src/delegate_to_remover.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use itertools::Itertools;
use syn::visit_mut::VisitMut;

pub(crate) fn remove_delegate_to(item: &mut syn::Item) {
let mut visitor = Visitor;
visitor.visit_item_mut(item);
}

struct Visitor;

// Use `visit_*_mut()` as we may need to change enum variant when it matches.
impl VisitMut for Visitor {
fn visit_variant_mut(&mut self, node: &mut syn::Variant) {
syn::visit_mut::visit_variant_mut(self, node);

// TODO: Use `Vec::extract_if()` once it is stabilized.
if let Some((i, _)) = node.attrs.iter().find_position(|attr|
matches!(&attr.meta, syn::Meta::List(meta_list) if meta_list.path.is_ident("delegate_to"))
) {
// Note that it is already checked that `#[delegate_to(...)]` appears at most once.
node.attrs.remove(i);
}
}
}
89 changes: 89 additions & 0 deletions src/ident_replacer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use syn::visit_mut::VisitMut;

pub(crate) fn replace_ident_in_expr(
orig: syn::Ident,
subst: syn::Ident,
mut target: syn::Expr,
) -> syn::Expr {
let mut visitor = Visitor { orig, subst };
visitor.visit_expr_mut(&mut target);
target
}

struct Visitor {
orig: syn::Ident,
subst: syn::Ident,
}

// Use `visit_*_mut()` as we may need to change enum variant when it matches.
impl VisitMut for Visitor {
fn visit_expr_mut(&mut self, node: &mut syn::Expr) {
syn::visit_mut::visit_expr_mut(self, node);

#[allow(clippy::single_match)]
match node {
syn::Expr::Path(expr_path) => {
if expr_path.path.is_ident(&self.orig) {
let path = syn::Path::from(syn::PathSegment::from(self.subst.clone()));
*node = syn::Expr::Path(syn::ExprPath {
attrs: vec![],
qself: None,
path,
});
}
}
_ => {}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use quote::{quote, ToTokens};

macro_rules! test_replace_ident_in_expr {
(
$test_name:ident,
$orig:expr,
$subst:expr,
$target:expr,
$expected:expr,
) => {
#[test]
fn $test_name() -> Result<(), syn::Error> {
let orig = syn::parse2::<syn::Ident>($orig).unwrap();
let subst = syn::parse2::<syn::Ident>($subst).unwrap();
let target = syn::parse2::<syn::Expr>($target).unwrap();
let expected = syn::parse2::<syn::Expr>($expected).unwrap();

let got = replace_ident_in_expr(orig, subst, target.clone());
assert_eq!(
got,
expected,
"\n got = {},\n expected = {}",
got.to_token_stream(),
expected.to_token_stream(),
);

Ok(())
}
};
}

test_replace_ident_in_expr! {
ref_0,
quote! { x },
quote! { y },
quote! { &x.0 },
quote! { &y.0 },
}

test_replace_ident_in_expr! {
dont_replace_not_is_ident,
quote! { x },
quote! { y },
quote! { x::x(&x.0) },
quote! { x::x(&y.0) },
}
}
94 changes: 85 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
mod delegate_to_arg;
#[cfg(not(feature = "unstable_delegate_to"))]
mod delegate_to_checker;
mod delegate_to_remover;
mod generic_param_replacer;
mod ident_replacer;
mod punctuated_parser;
mod storage;

use crate::delegate_to_arg::DelegateToArg;
use crate::generic_param_replacer::GenericParamReplacer;
use crate::punctuated_parser::PunctuatedParser;
use crate::storage::Storage;
Expand Down Expand Up @@ -197,19 +203,32 @@ pub fn derive_delegate(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as syn::Item);
let input_ = input.clone();
let item = parse_macro_input!(input_ as syn::Item);
let mut storage = Storage::global();

match derive_delegate_aux(&mut storage, args.into(), &item) {
#[cfg(not(feature = "unstable_delegate_to"))]
{
let mut item = item.clone();
match delegate_to_checker::check_non_existence(&mut item) {
Ok(()) => {}
Err(e) => {
return TokenStream::from_iter([e.into_compile_error(), input.into()]).into();
}
}
}

match derive_delegate_aux(&mut storage, args.into(), item) {
Ok(x) => x.into(),
Err(e) => TokenStream::from_iter([e.into_compile_error(), (quote! { #item })]).into(),
Err(e) => TokenStream::from_iter([e.into_compile_error(), input.into()]).into(),
}
}

fn derive_delegate_aux(
storage: &mut Storage,
args: TokenStream,
item: &syn::Item,
// Mutation is allowed only for removing attributes used, e.g. `#[delegate_to(...)]`.
mut item: syn::Item,
) -> syn::Result<TokenStream> {
if args.is_empty() {
return Err(syn::Error::new_spanned(
Expand All @@ -222,9 +241,12 @@ fn derive_delegate_aux(

let impls = paths
.iter()
.map(|path| derive_delegate_aux_1(storage, item, path))
.map(|path| derive_delegate_aux_1(storage, &item, path))
.collect::<syn::Result<Vec<_>>>()?;

// Remove `#[delegate_to(...)]` here, as all calls of `derive_delegate_aux_1()` require the attributes.
delegate_to_remover::remove_delegate_to(&mut item);

Ok(quote! {
#item

Expand Down Expand Up @@ -320,6 +342,24 @@ fn gen_impl_fn_enum(
.variants
.iter()
.map(|variant| {
// Note that we'll remove `#[delegate_to(...)]` attribute by `delegate_to_remover::remove_delegate_to()`.
let mut delegate_to_arg = None;
for attr in &variant.attrs {
match &attr.meta {
syn::Meta::List(meta_list) if meta_list.path.is_ident("delegate_to") => {
if delegate_to_arg.is_some() {
return Err(syn::Error::new_spanned(
attr,
"#[delegate_to(...)] can appear at most once",
));
}

delegate_to_arg = Some(syn::parse2::<DelegateToArg>(meta_list.tokens.clone())?);
}
_ => {},
}
}

let variant_ident = &variant.ident;
match &variant.fields {
syn::Fields::Named(fields) => {
Expand All @@ -331,9 +371,14 @@ fn gen_impl_fn_enum(
}

let ident = fields.named[0].ident.as_ref().unwrap();
let receiver = if let Some(delegate_to_arg) = delegate_to_arg {
ident_replacer::replace_ident_in_expr(delegate_to_arg.ident, ident.clone(), delegate_to_arg.expr).to_token_stream()
} else {
ident.to_token_stream()
};

Ok(quote! {
Self::#variant_ident { #ident } => #trait_path::#method_ident(#ident #(,#args)*)
Self::#variant_ident { #ident } => #trait_path::#method_ident(#receiver #(,#args)*)
})
}
syn::Fields::Unnamed(fields) => {
Expand All @@ -344,8 +389,14 @@ fn gen_impl_fn_enum(
));
}

let ident = syn::Ident::new("x", Span::call_site());
let receiver = if let Some(delegate_to_arg) = delegate_to_arg {
ident_replacer::replace_ident_in_expr(delegate_to_arg.ident, ident.clone(), delegate_to_arg.expr).to_token_stream()
} else {
ident.to_token_stream()
};
Ok(quote! {
Self::#variant_ident(x) => #trait_path::#method_ident(x #(,#args)*)
Self::#variant_ident(x) => #trait_path::#method_ident(#receiver #(,#args)*)
})
}
syn::Fields::Unit => {
Expand Down Expand Up @@ -440,7 +491,7 @@ mod tests {
let args = $derive_delegate_args;
let input = $derive_delegate_input;
compare_result!(
derive_delegate_aux(&mut storage, args, &syn::parse2::<syn::Item>(input)?),
derive_delegate_aux(&mut storage, args, syn::parse2::<syn::Item>(input)?),
Ok($derive_delegate_expected)
);

Expand Down Expand Up @@ -484,7 +535,7 @@ mod tests {
let args = $derive_delegate_args;
let input = $derive_delegate_input;
compare_result!(
derive_delegate_aux(&mut storage, args, &syn::parse2::<syn::Item>(input)?),
derive_delegate_aux(&mut storage, args, syn::parse2::<syn::Item>(input)?),
Ok($derive_delegate_expected)
);

Expand Down Expand Up @@ -972,4 +1023,29 @@ mod tests {
}
},
}

test_as_ref! {
custom_receiver,
// derive_delegate
quote! { AsRef<str> },
quote! {
enum Hoge {
#[delegate_to(x => &x.0)]
A((String, u8)),
}
},
quote! {
enum Hoge {
A((String, u8)),
}

impl AsRef<str> for Hoge {
fn as_ref(&self) -> &str {
match self {
Self::A(x) => AsRef::as_ref(&x.0),
}
}
}
},
}
}
5 changes: 5 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ fn ui_test() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/pass_*.rs");
t.compile_fail("tests/ui/fail*.rs");
#[cfg(feature = "unstable_delegate_to")]
{
t.pass("tests/ui/delegate_to_pass_*.rs");
t.compile_fail("tests/ui/delegate_to_fail*.rs");
}
}
Loading

0 comments on commit 3341315

Please sign in to comment.