diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b92cc7e8..02c19b06 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -37,6 +37,7 @@ use syn::{ parse_quote, punctuated::Punctuated, token::Comma, + AngleBracketedGenericArguments, Data, DataEnum, DataStruct, @@ -45,7 +46,11 @@ use syn::{ ExprLit, Field, Fields, + GenericArgument, + Ident, Lit, + PathArguments, + TypePath, Variant, }; @@ -73,20 +78,24 @@ fn generate_type(input: TokenStream2) -> Result { let ident = &ast.ident; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - let generic_type_ids = ast.generics.type_params().map(|ty| { - let ty_ident = &ty.ident; - quote! { - ::scale_info::meta_type::<#ty_ident>() - } - }); let ast: DeriveInput = syn::parse2(input.clone())?; - let build_type = match &ast.data { + let (build_type, phantom_params) = match &ast.data { Data::Struct(ref s) => generate_composite_type(s), Data::Enum(ref e) => generate_variant_type(e), Data::Union(_) => return Err(Error::new_spanned(input, "Unions not supported")), }; + let generic_type_ids = ast.generics.type_params().fold(Vec::new(), |mut acc, ty| { + let ty_ident = &ty.ident; + if !phantom_params.contains(ty_ident) { + acc.push(quote! { + ::scale_info::meta_type::<#ty_ident>() + }); + } + acc + }); + let type_info_impl = quote! { impl #impl_generics ::scale_info::TypeInfo for #ident #ty_generics #where_clause { type Identity = Self; @@ -105,24 +114,121 @@ fn generate_type(input: TokenStream2) -> Result { type FieldsList = Punctuated; -fn generate_fields(fields: &FieldsList) -> Vec { - fields +/// Given a type `Path`, find all generics params that are used in a +/// `PhantomData` and return them as a `Vec` of `Ident`. +/// E.g. given `utils::stuff::Thing, Q, PhantomData>`, return +/// `vec!["P", "R"]` +fn find_phantoms_in_path(path: &syn::Path) -> Vec { + path.segments .iter() - .map(|f| { - let (ty, ident) = (&f.ty, &f.ident); - let type_name = clean_type_string("e!(#ty).to_string()); + .filter(|seg| seg.ident == "PhantomData") + .fold(Vec::new(), |mut acc, seg| { + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args, + .. + }) = &seg.arguments + { + for arg in args { + if let GenericArgument::Type(syn::Type::Path(TypePath { + path, .. + })) = arg + { + for segment in path.segments.iter() { + acc.push(segment.ident.clone()) + } + } + } + } + acc + }) +} - if let Some(i) = ident { - quote! { - .field_of::<#ty>(stringify!(#i), #type_name) +/// Given a list of tuple elements, removes all `PhantomData` and returns a new +/// tuple along with any type parameters used in `PhantomData`. +fn scrub_phantoms_from_tuple( + tuple_elements: &Punctuated, + paren_token: syn::token::Paren, +) -> (syn::Type, Vec) { + let mut phantom_params = Vec::new(); + let mut punctuated: Punctuated<_, Comma> = Punctuated::new(); + for tuple_element in tuple_elements { + match tuple_element { + syn::Type::Path(syn::TypePath { path, .. }) => { + let phantoms = find_phantoms_in_path(path); + if phantoms.is_empty() { + punctuated.push(tuple_element.clone()); + } else { + phantom_params.extend(phantoms); } - } else { - quote! { - .field_of::<#ty>(#type_name) + } + syn::Type::Tuple(syn::TypeTuple { elems, paren_token }) => { + let (sub_tuple, phantoms) = + scrub_phantoms_from_tuple(elems, *paren_token); + punctuated.push(sub_tuple); + phantom_params.extend(phantoms); + } + // REVIEW: (dp) can there be anything but types and tuples in a tuple? + _ => unreachable!("Only types and tuples can appear in tuples"), + } + } + let tuple = syn::Type::Tuple(syn::TypeTuple { + elems: punctuated, + paren_token, + }); + + (tuple, phantom_params) +} + +/// Generate code for each field of a struct (named or unnamed). +/// Filters out `PhantomData` fields and returns the type parameters used as a +/// `Vec` of `Ident` so that other code can match up the type parameters in the +/// generics section to `PhantomData` (and omit them). +fn generate_fields(fields: &FieldsList) -> (Vec, Vec) { + // Collect all type params used with `PhantomData` anywhere in the type. + let mut phantom_params = Vec::new(); + let field_tokens = fields.iter().fold(Vec::new(), |mut acc, field| { + let (ty, ident) = (&field.ty, &field.ident); + let type_name = clean_type_string("e!(#ty).to_string()); + match ty { + // Regular types, e.g. `struct A { a: PhantomData, b: u8 }` + // Check for phantom types and skip them; record any + // PhantomData type params. + syn::Type::Path(syn::TypePath { path, .. }) => { + let phantoms = find_phantoms_in_path(path); + if phantoms.is_empty() { + let tokens = if let Some(i) = ident { + quote! {.field_of::<#ty>(stringify!(#i), #type_name) } + } else { + quote! {.field_of::<#ty>(#type_name) } + }; + acc.push(tokens); + } else { + phantom_params.extend(phantoms); } } - }) - .collect() + // If the type is a tuple, check it for any `PhantomData` elements and rebuild a new tuple without them. + syn::Type::Tuple(syn::TypeTuple { elems, paren_token }) => { + let (tuple, phantoms) = scrub_phantoms_from_tuple(elems, *paren_token); + let tokens = if let Some(i) = ident { + quote! { .field_of::<#tuple>(stringify!(#i), #type_name) } + } else { + quote! { .field_of::<#tuple>(#type_name)} + }; + phantom_params.extend(phantoms); + acc.push(tokens) + } + _ => { + let tokens = if let Some(i) = ident { + quote! { .field_of::<#ty>(stringify!(#i), #type_name) } + } else { + quote! { .field_of::<#ty>(#type_name)} + }; + acc.push(tokens) + } + } + acc + }); + (field_tokens, phantom_params) } fn clean_type_string(input: &str) -> String { @@ -135,6 +241,8 @@ fn clean_type_string(input: &str) -> String { .replace("[ ", "[") .replace(" ]", "]") .replace(" (", "(") + // put back a space so that `a: u8, (bool, u8)` isn't turned into `a: u8,(bool, u8)` + .replace(",(", ", (") .replace("( ", "(") .replace(" )", ")") .replace(" <", "<") @@ -142,25 +250,24 @@ fn clean_type_string(input: &str) -> String { .replace(" >", ">") } -fn generate_composite_type(data_struct: &DataStruct) -> TokenStream2 { - let fields = match data_struct.fields { +/// Generate code for a composite type. Returns the token stream and all the +/// generic types used in `PhantomData` members. +fn generate_composite_type(data_struct: &DataStruct) -> (TokenStream2, Vec) { + let (fields, phantom_params) = match data_struct.fields { Fields::Named(ref fs) => { - let fields = generate_fields(&fs.named); - quote! { named()#( #fields )* } + let (fields, phantoms) = generate_fields(&fs.named); + (quote! { named()#( #fields )* }, phantoms) } Fields::Unnamed(ref fs) => { - let fields = generate_fields(&fs.unnamed); - quote! { unnamed()#( #fields )* } - } - Fields::Unit => { - quote! { - unit() - } + let (fields, phantoms) = generate_fields(&fs.unnamed); + (quote! { unnamed()#( #fields )* }, phantoms) } + Fields::Unit => (quote! { unit() }, Vec::new()), }; - quote! { - composite(::scale_info::build::Fields::#fields) - } + ( + quote! { composite(::scale_info::build::Fields::#fields) }, + phantom_params, + ) } type VariantList = Punctuated; @@ -205,19 +312,22 @@ fn is_c_like_enum(variants: &VariantList) -> bool { }) } -fn generate_variant_type(data_enum: &DataEnum) -> TokenStream2 { +/// Build enum variants while keeping track of any `PhantomData` members so we can skip them later. +fn generate_variant_type(data_enum: &DataEnum) -> (TokenStream2, Vec) { let variants = &data_enum.variants; if is_c_like_enum(&variants) { - return generate_c_like_enum_def(variants) + return (generate_c_like_enum_def(variants), Vec::new()) } + let mut phantom_params = Vec::new(); let variants = variants.into_iter().map(|v| { let ident = &v.ident; let v_name = quote! {stringify!(#ident) }; match v.fields { Fields::Named(ref fs) => { - let fields = generate_fields(&fs.named); + let (fields, phantoms) = generate_fields(&fs.named); + phantom_params.extend(phantoms); quote! { .variant( #v_name, @@ -227,7 +337,8 @@ fn generate_variant_type(data_enum: &DataEnum) -> TokenStream2 { } } Fields::Unnamed(ref fs) => { - let fields = generate_fields(&fs.unnamed); + let (fields, phantoms) = generate_fields(&fs.unnamed); + phantom_params.extend(phantoms); quote! { .variant( #v_name, @@ -243,10 +354,13 @@ fn generate_variant_type(data_enum: &DataEnum) -> TokenStream2 { } } }); - quote! { - variant( - ::scale_info::build::Variants::with_fields() - #( #variants)* - ) - } + ( + quote! { + variant( + ::scale_info::build::Variants::with_fields() + #( #variants)* + ) + }, + phantom_params, + ) } diff --git a/src/impls.rs b/src/impls.rs index 354efc9d..de42e56f 100644 --- a/src/impls.rs +++ b/src/impls.rs @@ -17,13 +17,11 @@ use crate::prelude::{ collections::BTreeMap, marker::PhantomData, string::String, - vec, vec::Vec, }; use crate::{ build::*, - meta_type, MetaType, Path, Type, @@ -243,9 +241,6 @@ where type Identity = Self; fn type_info() -> Type { - Type::builder() - .path(Path::prelude("PhantomData")) - .type_params(vec![meta_type::()]) - .composite(Fields::unit()) + Type::new(Path::voldemort(), Vec::new(), TypeDefTuple::unit()) } } diff --git a/src/tests.rs b/src/tests.rs index 1552490c..99c27f68 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -74,13 +74,7 @@ fn prelude_items() { .variant("Err", Fields::unnamed().field_of::("E")) ) ); - assert_type!( - PhantomData, - Type::builder() - .path(Path::prelude("PhantomData")) - .type_params(tuple_meta_type!(i32)) - .composite(Fields::unit()) - ); + assert_type!(PhantomData, TypeDefTuple::new(tuple_meta_type!())); } #[test] @@ -154,3 +148,34 @@ fn struct_with_generics() { .composite(Fields::named().field_of::>>("data", "T")); assert_type!(SelfTyped, expected_type); } + +#[test] +fn basic_struct_with_phantoms() { + #[allow(unused)] + struct SomeStruct { + a: u8, + marker: PhantomData, + } + + impl TypeInfo for SomeStruct + where + T: TypeInfo + 'static, + { + type Identity = Self; + + fn type_info() -> Type { + Type::builder() + .path(Path::new("SomeStruct", module_path!())) + .type_params(tuple_meta_type!(T)) + .composite(Fields::named().field_of::("a", "T")) + } + } + + // Normal struct + let struct_bool_type_info = Type::builder() + .path(Path::from_segments(vec!["scale_info", "tests", "SomeStruct"]).unwrap()) + .type_params(tuple_meta_type!(bool)) + .composite(Fields::named().field_of::("a", "T")); + + assert_type!(SomeStruct, struct_bool_type_info); +} diff --git a/test_suite/tests/derive.rs b/test_suite/tests/derive.rs index d7893880..d6fe2023 100644 --- a/test_suite/tests/derive.rs +++ b/test_suite/tests/derive.rs @@ -11,10 +11,12 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - #![cfg_attr(not(feature = "std"), no_std)] -use scale_info::prelude::boxed::Box; +use scale_info::prelude::{ + boxed::Box, + marker::PhantomData, +}; use pretty_assertions::assert_eq; use scale_info::{ @@ -74,6 +76,157 @@ fn struct_derive() { assert_type!(SelfTyped, self_typed_type); } +#[test] +fn no_phantom_types_are_derived_in_structs() { + #[allow(unused)] + #[derive(TypeInfo)] + struct P { + pub a: u8, + pub marker: PhantomData, + } + + let ty = Type::builder() + .path(Path::new("P", "derive")) + .composite(Fields::named().field_of::("a", "u8")); + + assert_type!(P, ty); +} + +#[test] +fn no_phantom_types_are_derived_in_tuple_structs() { + #[allow(unused)] + #[derive(TypeInfo)] + struct Tuppy(u8, PhantomData); + + let tuppy = Type::builder() + .path(Path::new("Tuppy", "derive")) + .composite(Fields::unnamed().field_of::("u8")); + + assert_type!(Tuppy<()>, tuppy); +} + +#[test] +fn no_phantoms_are_derived_in_struct_with_tuple_members() { + #[allow(unused)] + #[derive(TypeInfo)] + struct WithTuples { + a: (u8, PhantomData, u32, PhantomData), + } + + let ty = + Type::builder() + .path(Path::new("WithTuples", "derive")) + .composite(Fields::named().field_of::<(u8, u32)>( + "a", + "(u8, PhantomData, u32, PhantomData)", + )); + + assert_type!(WithTuples, ty); +} + +#[test] +fn no_phantom_types_are_derived_in_enums() { + #[allow(unused)] + #[derive(TypeInfo)] + struct Chocolate { + flavour: PhantomData, + } + #[allow(unused)] + #[derive(TypeInfo)] + enum Choices { + Nutella, + RealThing(Chocolate), + Marshmallow(PhantomData), + }; + + let ty = Type::builder() + .path(Path::new("Choices", "derive")) + .variant( + Variants::with_fields() + .variant_unit("Nutella") + .variant( + "RealThing", + Fields::unnamed().field_of::>("Chocolate"), + ) + .variant_unit("Marshmallow"), + ); + + assert_type!(Choices, ty); +} + +#[test] +fn complex_enum_with_phantoms() { + #[allow(unused)] + #[derive(TypeInfo)] + enum Cake { + A((PhantomData, u8, PhantomData, Filling)), + B, + } + + let ty = Type::builder() + .path(Path::new("Cake", "derive")) + .type_params(tuple_meta_type!(u16)) + .variant( + Variants::with_fields() + .variant( + "A", + Fields::unnamed().field_of::<(u8, u16)>( + "(PhantomData, u8, PhantomData, Filling)", + ), + ) + .variant_unit("B"), + ); + assert_type!(Cake, ty); +} + +#[test] +fn no_nested_phantom_types_are_derived_structs() { + #[allow(unused)] + #[derive(TypeInfo)] + struct Door { + size: PhantomData, + b: u16, + }; + #[allow(unused)] + #[derive(TypeInfo)] + struct House { + a: u8, + door: TDoor, + }; + + let house = Type::builder() + .path(Path::new("House", "derive")) + .type_params(tuple_meta_type!(Door)) + .composite( + Fields::named() + .field_of::("a", "u8") + .field_of::>("door", "TDoor"), + ); + + assert_type!(House>, house); +} + +#[test] +fn no_phantoms_in_nested_tuples() { + #[allow(unused)] + #[derive(TypeInfo)] + struct A { + is_a: bool, + teeth: (u8, u16, (u32, (PhantomData, bool))), + } + + let ty = Type::builder().path(Path::new("A", "derive")).composite( + Fields::named() + .field_of::("is_a", "bool") + .field_of::<(u8, u16, (u32, (bool)))>( + "teeth", + "(u8, u16, (u32, (PhantomData, bool)))", + ), + ); + + assert_type!(A, ty); +} + #[test] fn tuple_struct_derive() { #[allow(unused)] diff --git a/test_suite/tests/json.rs b/test_suite/tests/json.rs index 5fd84585..5d23393d 100644 --- a/test_suite/tests/json.rs +++ b/test_suite/tests/json.rs @@ -133,10 +133,8 @@ fn test_builtins() { assert_json_for_type::(json!({ "def": { "primitive": "str" } })); // PhantomData assert_json_for_type::>(json!({ - "path": ["PhantomData"], - "params": [1], "def": { - "composite": {}, + "tuple": [], } })) }