//! Implementation of an [`Into`] derive macro. use std::{ any::{Any, TypeId}, borrow::Cow, iter, slice, }; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens as _}; use syn::{ ext::IdentExt as _, parse::{discouraged::Speculative as _, Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned as _, token, }; use crate::utils::{ attr::{self, ParseMultiple as _}, polyfill, Either, FieldsExt, Spanning, }; /// Expands an [`Into`] derive macro. pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { let attr_name = format_ident!("into"); let data = match &input.data { syn::Data::Struct(data) => Ok(data), syn::Data::Enum(e) => Err(syn::Error::new( e.enum_token.span(), "`Into` cannot be derived for enums", )), syn::Data::Union(u) => Err(syn::Error::new( u.union_token.span(), "`Into` cannot be derived for unions", )), }?; let struct_attr = StructAttribute::parse_attrs_with( &input.attrs, &attr_name, &ConsiderLegacySyntax { fields: &data.fields, }, )? .map(Spanning::into_inner); let fields_data = data .fields .iter() .enumerate() .map(|(i, f)| { let field_attr = FieldAttribute::parse_attrs_with( &f.attrs, &attr_name, &ConsiderLegacySyntax { fields: slice::from_ref(f), }, )? .map(Spanning::into_inner); let skip = field_attr .as_ref() .map(|attr| attr.skip.is_some()) .unwrap_or(false); let convs = field_attr.and_then(|attr| attr.convs); Ok(((i, f, skip), convs)) }) .collect::>>()?; let (fields, fields_convs): (Vec<_>, Vec<_>) = fields_data.into_iter().unzip(); let struct_attr = struct_attr.or_else(|| { fields_convs .iter() .all(Option::is_none) .then(ConversionsAttribute::default) .map(Either::Right) }); let mut expansions: Vec<_> = fields .iter() .zip(fields_convs) .filter_map(|(&(i, field, _), convs)| { convs.map(|convs| Expansion { input_ident: &input.ident, input_generics: &input.generics, fields: vec![(i, field)], convs, }) }) .collect(); if let Some(attr) = struct_attr { expansions.push(Expansion { input_ident: &input.ident, input_generics: &input.generics, fields: fields .into_iter() .filter_map(|(i, f, skip)| (!skip).then_some((i, f))) .collect(), convs: attr.into(), }); } expansions.into_iter().map(Expansion::expand).collect() } /// Expansion of an [`Into`] derive macro, generating [`From`] implementations for a struct. struct Expansion<'a> { /// [`syn::Ident`] of the struct. /// /// [`syn::Ident`]: struct@syn::Ident input_ident: &'a syn::Ident, /// [`syn::Generics`] of the struct. input_generics: &'a syn::Generics, /// Fields to convert from, along with their indices. fields: Vec<(usize, &'a syn::Field)>, /// Conversions to be generated. convs: ConversionsAttribute, } impl Expansion<'_> { fn expand(self) -> syn::Result { let Self { input_ident, input_generics, fields, convs, } = self; let fields_idents: Vec<_> = fields .iter() .map(|(i, f)| { f.ident .as_ref() .map_or_else(|| Either::Left(syn::Index::from(*i)), Either::Right) }) .collect(); let fields_tys: Vec<_> = fields.iter().map(|(_, f)| &f.ty).collect(); let fields_tuple = syn::Type::Tuple(syn::TypeTuple { paren_token: token::Paren::default(), elems: fields_tys.iter().cloned().cloned().collect(), }); [ (&convs.owned, false, false), (&convs.r#ref, true, false), (&convs.ref_mut, true, true), ] .into_iter() .filter(|(conv, _, _)| conv.consider_fields_ty || !conv.tys.is_empty()) .map(|(conv, ref_, mut_)| { let lf = ref_.then(|| syn::Lifetime::new("'__derive_more_into", Span::call_site())); let r = ref_.then(token::And::default); let m = mut_.then(token::Mut::default); let gens = if let Some(lf) = lf.clone() { let mut gens = input_generics.clone(); gens.params.push(syn::LifetimeParam::new(lf).into()); Cow::Owned(gens) } else { Cow::Borrowed(input_generics) }; let (impl_gens, _, where_clause) = gens.split_for_impl(); let (_, ty_gens, _) = input_generics.split_for_impl(); if conv.consider_fields_ty { Either::Left(iter::once(&fields_tuple)) } else { Either::Right(iter::empty()) } .chain(&conv.tys) .map(|out_ty| { let tys: Vec<_> = fields_tys.validate_type(out_ty)?.collect(); Ok(quote! { #[allow(clippy::unused_unit)] #[automatically_derived] impl #impl_gens derive_more::core::convert::From<#r #lf #m #input_ident #ty_gens> for ( #( #r #lf #m #tys ),* ) #where_clause { #[inline] fn from(value: #r #lf #m #input_ident #ty_gens) -> Self { (#( <#r #m #tys as derive_more::core::convert::From<_>>::from( #r #m value. #fields_idents ) ),*) } } }) }) .collect::>() }) .collect() } } /// Representation of an [`Into`] derive macro struct container attribute. /// /// ```rust,ignore /// #[into] /// #[into()] /// #[into(owned(), ref(), ref_mut())] /// ``` type StructAttribute = Either; impl From for ConversionsAttribute { fn from(v: StructAttribute) -> Self { match v { Either::Left(_) => ConversionsAttribute::default(), Either::Right(c) => c, } } } type Untyped = Either>; impl From for FieldAttribute { fn from(v: Untyped) -> Self { match v { Untyped::Left(skip) => Self { skip: Some(skip), convs: None, }, Untyped::Right(c) => Self { skip: None, convs: Some(match c { Either::Left(_empty) => ConversionsAttribute::default(), Either::Right(convs) => convs, }), }, } } } /// Representation of an [`Into`] derive macro field attribute. /// /// ```rust,ignore /// #[into] /// #[into()] /// #[into(owned(), ref(), ref_mut())] /// #[into(skip)] #[into(ignore)] /// ``` #[derive(Clone, Debug)] struct FieldAttribute { skip: Option, convs: Option, } impl Parse for FieldAttribute { fn parse(_: ParseStream<'_>) -> syn::Result { unreachable!("call `attr::ParseMultiple::parse_attr_with()` instead") } } impl attr::ParseMultiple for FieldAttribute { fn parse_attr_with( attr: &syn::Attribute, parser: &P, ) -> syn::Result { Untyped::parse_attr_with(attr, parser).map(Self::from) } fn merge_attrs( prev: Spanning, new: Spanning, name: &syn::Ident, ) -> syn::Result> { let skip = attr::Skip::merge_opt_attrs( prev.clone().map(|v| v.skip).transpose(), new.clone().map(|v| v.skip).transpose(), name, )? .map(Spanning::into_inner); let convs = ConversionsAttribute::merge_opt_attrs( prev.clone().map(|v| v.convs).transpose(), new.clone().map(|v| v.convs).transpose(), name, )? .map(Spanning::into_inner); Ok(Spanning::new( Self { skip, convs }, prev.span.join(new.span).unwrap_or(prev.span), )) } } /// [`Into`] conversions specified by a [`ConversionsAttribute`]. #[derive(Clone, Debug, Default)] struct Conversions { /// Indicator whether these [`Conversions`] should contain a conversion into fields type. consider_fields_ty: bool, /// [`syn::Type`]s explicitly specified in a [`ConversionsAttribute`]. tys: Punctuated, } /// Representation of an [`Into`] derive macro attribute describing specified [`Into`] conversions. /// /// ```rust,ignore /// #[into()] /// #[into(owned(), ref(), ref_mut())] /// ``` #[derive(Clone, Debug)] struct ConversionsAttribute { /// [`syn::Type`]s wrapped into `owned(...)` or simply `#[into(...)]`. owned: Conversions, /// [`syn::Type`]s wrapped into `ref(...)`. r#ref: Conversions, /// [`syn::Type`]s wrapped into `ref_mut(...)`. ref_mut: Conversions, } impl Default for ConversionsAttribute { fn default() -> Self { Self { owned: Conversions { consider_fields_ty: true, tys: Punctuated::new(), }, r#ref: Conversions::default(), ref_mut: Conversions::default(), } } } impl Parse for ConversionsAttribute { fn parse(input: ParseStream<'_>) -> syn::Result { let mut out = Self { owned: Conversions::default(), r#ref: Conversions::default(), ref_mut: Conversions::default(), }; let parse_inner = |ahead, convs: &mut Conversions| { input.advance_to(&ahead); if input.peek(token::Paren) { let inner; syn::parenthesized!(inner in input); convs.tys.extend( inner .parse_terminated(syn::Type::parse, token::Comma)? .into_pairs(), ); } else { convs.consider_fields_ty = true; } if input.peek(token::Comma) { let comma = input.parse::()?; if !convs.tys.empty_or_trailing() { convs.tys.push_punct(comma); } } Ok(()) }; let mut has_wrapped_type = false; let mut top_level_type = None; while !input.is_empty() { let ahead = input.fork(); let res = if ahead.peek(syn::Ident::peek_any) { ahead.call(syn::Ident::parse_any).map(Into::into) } else { ahead.parse::() }; match res { Ok(p) if p.is_ident("owned") => { has_wrapped_type = true; parse_inner(ahead, &mut out.owned)?; } Ok(p) if p.is_ident("ref") => { has_wrapped_type = true; parse_inner(ahead, &mut out.r#ref)?; } Ok(p) if p.is_ident("ref_mut") => { has_wrapped_type = true; parse_inner(ahead, &mut out.ref_mut)?; } _ => { let ty = input.parse::()?; let _ = top_level_type.get_or_insert_with(|| ty.clone()); out.owned.tys.push_value(ty); if input.peek(token::Comma) { out.owned.tys.push_punct(input.parse::()?) } } } } if let Some(ty) = top_level_type.filter(|_| has_wrapped_type) { Err(syn::Error::new( ty.span(), format!( "mixing regular types with wrapped into `owned`/`ref`/`ref_mut` is not \ allowed, try wrapping this type into `owned({ty}), ref({ty}), ref_mut({ty})`", ty = ty.into_token_stream(), ), )) } else { Ok(out) } } } impl attr::ParseMultiple for ConversionsAttribute { fn merge_attrs( prev: Spanning, new: Spanning, _: &syn::Ident, ) -> syn::Result> { let Spanning { span: prev_span, item: mut prev, } = prev; let Spanning { span: new_span, item: new, } = new; prev.owned.tys.extend(new.owned.tys); prev.owned.consider_fields_ty |= new.owned.consider_fields_ty; prev.r#ref.tys.extend(new.r#ref.tys); prev.r#ref.consider_fields_ty |= new.r#ref.consider_fields_ty; prev.ref_mut.tys.extend(new.ref_mut.tys); prev.ref_mut.consider_fields_ty |= new.ref_mut.consider_fields_ty; Ok(Spanning::new( prev, prev_span.join(new_span).unwrap_or(prev_span), )) } } /// [`attr::Parser`] considering legacy syntax and performing [`check_legacy_syntax()`] for a /// [`StructAttribute`] or a [`FieldAttribute`]. struct ConsiderLegacySyntax { /// [`syn::Field`]s the [`StructAttribute`] or [`FieldAttribute`] is parsed for. fields: F, } impl<'a, F> attr::Parser for ConsiderLegacySyntax<&'a F> where F: FieldsExt + ?Sized, &'a F: IntoIterator, { fn parse(&self, input: ParseStream<'_>) -> syn::Result { if TypeId::of::() == TypeId::of::() { check_legacy_syntax(input, self.fields)?; } T::parse(input) } } /// [`Error`]ors for legacy syntax: `#[into(types(i32, "&str"))]`. /// /// [`Error`]: syn::Error fn check_legacy_syntax<'a, F>(tokens: ParseStream<'_>, fields: &'a F) -> syn::Result<()> where F: FieldsExt + ?Sized, &'a F: IntoIterator, { let span = tokens.span(); let tokens = tokens.fork(); let map_ty = |s: String| { if fields.len() > 1 { format!( "({})", (0..fields.len()) .map(|_| s.as_str()) .collect::>() .join(", ") ) } else { s } }; let field = match fields.len() { 0 => None, 1 => Some( fields .into_iter() .next() .unwrap_or_else(|| unreachable!("fields.len() == 1")) .ty .to_token_stream() .to_string(), ), _ => Some(format!( "({})", fields .into_iter() .map(|f| f.ty.to_token_stream().to_string()) .collect::>() .join(", ") )), }; let Ok(metas) = tokens.parse_terminated(polyfill::Meta::parse, token::Comma) else { return Ok(()); }; let parse_list = |list: polyfill::MetaList, attrs: &mut Option>| { if !list.path.is_ident("types") { return None; } for meta in list .parse_args_with(Punctuated::<_, token::Comma>::parse_terminated) .ok()? { attrs.get_or_insert_with(Vec::new).push(match meta { polyfill::NestedMeta::Lit(syn::Lit::Str(str)) => str.value(), polyfill::NestedMeta::Meta(polyfill::Meta::Path(path)) => { path.into_token_stream().to_string() } _ => return None, }) } Some(()) }; let Some((top_level, owned, ref_, ref_mut)) = metas .into_iter() .try_fold( (None, None, None, None), |(mut top_level, mut owned, mut ref_, mut ref_mut), meta| { let is = |name| { matches!(&meta, polyfill::Meta::Path(p) if p.is_ident(name)) || matches!(&meta, polyfill::Meta::List(list) if list.path.is_ident(name)) }; let parse_inner = |meta, attrs: &mut Option<_>| { match meta { polyfill::Meta::Path(_) => { let _ = attrs.get_or_insert_with(Vec::new); Some(()) } polyfill::Meta::List(list) => { if let polyfill::NestedMeta::Meta(polyfill::Meta::List(list)) = list .parse_args_with(Punctuated::<_, token::Comma>::parse_terminated) .ok()? .pop()? .into_value() { parse_list(list, attrs) } else { None } } } }; match meta { meta if is("owned") => parse_inner(meta, &mut owned), meta if is("ref") => parse_inner(meta, &mut ref_), meta if is("ref_mut") => parse_inner(meta, &mut ref_mut), polyfill::Meta::List(list) => parse_list(list, &mut top_level), _ => None, } .map(|_| (top_level, owned, ref_, ref_mut)) }, ) .filter(|(top_level, owned, ref_, ref_mut)| { [top_level, owned, ref_, ref_mut] .into_iter() .any(|l| l.as_ref().is_some_and(|l| !l.is_empty())) }) else { return Ok(()); }; if [&owned, &ref_, &ref_mut].into_iter().any(Option::is_some) { let format = |list: Option>, name: &str| match list { Some(l) if top_level.as_ref().map_or(true, Vec::is_empty) && l.is_empty() => { Some(name.to_owned()) } Some(l) => Some(format!( "{}({})", name, l.into_iter() .chain(top_level.clone().into_iter().flatten()) .map(map_ty) .chain(field.clone()) .collect::>() .join(", "), )), None => None, }; let format = [ format(owned, "owned"), format(ref_, "ref"), format(ref_mut, "ref_mut"), ] .into_iter() .flatten() .collect::>() .join(", "); Err(syn::Error::new( span, format!("legacy syntax, use `{format}` instead"), )) } else { Err(syn::Error::new( span, format!( "legacy syntax, remove `types` and use `{}` instead", top_level.unwrap_or_else(|| unreachable!()).join(", "), ), )) } }