diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 9ffef3ee..91a6e8d3 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -18,6 +18,7 @@ extern crate alloc; extern crate proc_macro; mod trait_bounds; +mod utils; use alloc::{ string::{ @@ -41,21 +42,14 @@ use syn::{ punctuated::Punctuated, token::Comma, visit_mut::VisitMut, - AttrStyle, Data, DataEnum, DataStruct, DeriveInput, - Expr, - ExprLit, Field, Fields, Ident, Lifetime, - Lit, - Meta, - MetaList, - NestedMeta, Variant, }; @@ -140,6 +134,7 @@ type FieldsList = Punctuated; fn generate_fields(fields: &FieldsList) -> Vec { fields .iter() + .filter(|f| !utils::should_skip(&f.attrs)) .map(|f| { let (ty, ident) = (&f.ty, &f.ident); // Replace any field lifetime params with `static to prevent "unnecessary lifetime parameter" @@ -154,7 +149,7 @@ fn generate_fields(fields: &FieldsList) -> Vec { StaticLifetimesReplace.visit_type_mut(&mut ty); let type_name = clean_type_string("e!(#ty).to_string()); - let method_call = if is_compact(f) { + let method_call = if utils::is_compact(f) { quote!(.compact_of::<#ty>) } else { quote!(.field_of::<#ty>) @@ -168,23 +163,6 @@ fn generate_fields(fields: &FieldsList) -> Vec { .collect() } -/// Look for a `#[codec(compact)]` outer attribute. -fn is_compact(f: &Field) -> bool { - f.attrs.iter().any(|attr| { - let mut is_compact = false; - if attr.style == AttrStyle::Outer && attr.path.is_ident("codec") { - if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() { - if let Some(NestedMeta::Meta(Meta::Path(path))) = nested.iter().next() { - if path.is_ident("compact") { - is_compact = true; - } - } - } - } - is_compact - }) -} - fn clean_type_string(input: &str) -> String { input .replace(" ::", "::") @@ -229,27 +207,17 @@ fn generate_composite_type(data_struct: &DataStruct, scale_info: &Ident) -> Toke type VariantList = Punctuated; fn generate_c_like_enum_def(variants: &VariantList, scale_info: &Ident) -> TokenStream2 { - let variants = variants.into_iter().enumerate().map(|(i, v)| { - let name = &v.ident; - let discriminant = if let Some(( - _, - Expr::Lit(ExprLit { - lit: Lit::Int(lit_int), - .. - }), - )) = &v.discriminant - { - match lit_int.base10_parse::() { - Ok(i) => i, - Err(err) => return err.to_compile_error(), + let variants = variants + .into_iter() + .enumerate() + .filter(|(_, v)| !utils::should_skip(&v.attrs)) + .map(|(i, v)| { + let name = &v.ident; + let discriminant = utils::variant_index(v, i); + quote! { + .variant(stringify!(#name), #discriminant as u64) } - } else { - i as u64 - }; - quote! { - .variant(stringify!(#name), #discriminant) - } - }); + }); quote! { variant( :: #scale_info ::build::Variants::fieldless() @@ -259,9 +227,9 @@ fn generate_c_like_enum_def(variants: &VariantList, scale_info: &Ident) -> Token } fn is_c_like_enum(variants: &VariantList) -> bool { - // any variant has an explicit discriminant + // One of the variants has an explicit discriminant, or… variants.iter().any(|v| v.discriminant.is_some()) || - // all variants are unit + // …all variants are unit variants.iter().all(|v| matches!(v.fields, Fields::Unit)) } @@ -272,37 +240,40 @@ fn generate_variant_type(data_enum: &DataEnum, scale_info: &Ident) -> TokenStrea return generate_c_like_enum_def(variants, scale_info) } - 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); - quote! { - .variant( - #v_name, - :: #scale_info ::build::Fields::named() - #( #fields)* - ) + let variants = variants + .into_iter() + .filter(|v| !utils::should_skip(&v.attrs)) + .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); + quote! { + .variant( + #v_name, + :: #scale_info::build::Fields::named() + #( #fields)* + ) + } } - } - Fields::Unnamed(ref fs) => { - let fields = generate_fields(&fs.unnamed); - quote! { - .variant( - #v_name, - :: #scale_info ::build::Fields::unnamed() - #( #fields)* - ) + Fields::Unnamed(ref fs) => { + let fields = generate_fields(&fs.unnamed); + quote! { + .variant( + #v_name, + :: #scale_info::build::Fields::unnamed() + #( #fields)* + ) + } } - } - Fields::Unit => { - quote! { - .variant_unit(#v_name) + Fields::Unit => { + quote! { + .variant_unit(#v_name) + } } } - } - }); + }); quote! { variant( :: #scale_info ::build::Variants::with_fields() diff --git a/derive/src/trait_bounds.rs b/derive/src/trait_bounds.rs index 1e79e7d0..65da4257 100644 --- a/derive/src/trait_bounds.rs +++ b/derive/src/trait_bounds.rs @@ -29,6 +29,8 @@ use syn::{ WhereClause, }; +use crate::utils; + /// Generates a where clause for a `TypeInfo` impl, adding `TypeInfo + 'static` bounds to all /// relevant generic types including associated types (e.g. `T::A: TypeInfo`), correctly dealing /// with self-referential types. @@ -164,7 +166,7 @@ fn collect_types_to_bind( // to not have them in the where clause. !type_or_sub_type_path_starts_with_ident(&field.ty, &input_ident) }) - .map(|f| (f.ty.clone(), super::is_compact(f))) + .map(|f| (f.ty.clone(), utils::is_compact(f))) .collect() }; diff --git a/derive/src/utils.rs b/derive/src/utils.rs new file mode 100644 index 00000000..f3911c98 --- /dev/null +++ b/derive/src/utils.rs @@ -0,0 +1,109 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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. + +//! Utility methods to work with `SCALE` attributes relevant for the `TypeInfo` derive.. +//! +//! NOTE: The code here is copied verbatim from `parity-scale-codec-derive`. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + spanned::Spanned, + AttrStyle, + Attribute, + Lit, + Meta, + NestedMeta, + Variant, +}; + +/// Look for a `#[codec(index = $int)]` attribute on a variant. If no attribute +/// is found, fall back to the discriminant or just the variant index. +pub fn variant_index(v: &Variant, i: usize) -> TokenStream { + // first look for an attribute + let index = find_meta_item(v.attrs.iter(), |meta| { + if let NestedMeta::Meta(Meta::NameValue(ref nv)) = meta { + if nv.path.is_ident("index") { + if let Lit::Int(ref v) = nv.lit { + let byte = v + .base10_parse::() + .expect("Internal error. `#[codec(index = …)]` attribute syntax must be checked in `parity-scale-codec`. This is a bug."); + return Some(byte) + } + } + } + + None + }); + + // then fallback to discriminant or just index + index.map(|i| quote! { #i }).unwrap_or_else(|| { + v.discriminant + .as_ref() + .map(|&(_, ref expr)| quote! { #expr }) + .unwrap_or_else(|| quote! { #i }) + }) +} + +/// Look for a `#[codec(compact)]` outer attribute on the given `Field`. +pub fn is_compact(field: &syn::Field) -> bool { + let outer_attrs = field + .attrs + .iter() + .filter(|attr| attr.style == AttrStyle::Outer); + find_meta_item(outer_attrs, |meta| { + if let NestedMeta::Meta(Meta::Path(ref path)) = meta { + if path.is_ident("compact") { + return Some(()) + } + } + + None + }) + .is_some() +} + +/// Look for a `#[codec(skip)]` in the given attributes. +pub fn should_skip(attrs: &[Attribute]) -> bool { + find_meta_item(attrs.iter(), |meta| { + if let NestedMeta::Meta(Meta::Path(ref path)) = meta { + if path.is_ident("skip") { + return Some(path.span()) + } + } + + None + }) + .is_some() +} + +fn find_meta_item<'a, F, R, I>(itr: I, pred: F) -> Option +where + F: Fn(&NestedMeta) -> Option + Clone, + I: Iterator, +{ + itr.filter_map(|attr| { + if attr.path.is_ident("codec") { + if let Meta::List(ref meta_list) = attr + .parse_meta() + .expect("scale-info: Bad index in `#[codec(index = …)]`, see `parity-scale-codec` error") + { + return meta_list.nested.iter().filter_map(pred.clone()).next() + } + } + + None + }) + .next() +} diff --git a/src/ty/variant.rs b/src/ty/variant.rs index b201fc66..dcf2db09 100644 --- a/src/ty/variant.rs +++ b/src/ty/variant.rs @@ -60,7 +60,7 @@ use serde::{ /// Monday, /// Tuesday, /// Wednesday, -/// Thursday = 42, // Also allows to manually set the discriminant! +/// Thursday = 42, // Allows setting the discriminant explicitly /// Friday, /// Saturday, /// Sunday, diff --git a/test_suite/tests/derive.rs b/test_suite/tests/derive.rs index 02a90a35..febae6f3 100644 --- a/test_suite/tests/derive.rs +++ b/test_suite/tests/derive.rs @@ -142,6 +142,32 @@ fn c_like_enum_derive() { assert_type!(E, ty); } +#[test] +fn c_like_enum_derive_with_scale_index_set() { + #[allow(unused)] + #[derive(TypeInfo, Encode)] + enum E { + A, + B = 10, + #[codec(index = 13)] + C, + D, + #[codec(index = 14)] + E = 15, + } + + let ty = Type::builder().path(Path::new("E", "derive")).variant( + Variants::fieldless() + .variant("A", 0) + .variant("B", 10) + .variant("C", 13) + .variant("D", 3) + .variant("E", 14), + ); + + assert_type!(E, ty); +} + #[test] fn enum_derive() { #[allow(unused)] @@ -315,6 +341,67 @@ fn scale_compact_types_work_in_enums() { assert_type!(MutilatedMultiAddress, ty); } +#[test] +fn struct_fields_marked_scale_skip_are_skipped() { + #[allow(unused)] + #[derive(TypeInfo, Encode)] + struct Skippy { + a: u8, + #[codec(skip)] + b: u16, + c: u32, + } + + let ty = Type::builder() + .path(Path::new("Skippy", "derive")) + .composite( + Fields::named() + .field_of::("a", "u8") + .field_of::("c", "u32"), + ); + assert_type!(Skippy, ty); +} + +#[test] +fn enum_variants_marked_scale_skip_are_skipped() { + #[allow(unused)] + #[derive(TypeInfo, Encode)] + enum Skippy { + A, + #[codec(skip)] + B, + C, + } + + let ty = Type::builder() + .path(Path::new("Skippy", "derive")) + .variant(Variants::fieldless().variant("A", 0).variant("C", 2)); + assert_type!(Skippy, ty); +} + +#[test] +fn enum_variants_with_fields_marked_scale_skip_are_skipped() { + #[allow(unused)] + #[derive(TypeInfo, Encode)] + enum Skippy { + #[codec(skip)] + Apa, + Bajs { + #[codec(skip)] + a: u8, + b: bool, + }, + Coo(bool), + } + + let ty = Type::builder().path(Path::new("Skippy", "derive")).variant( + Variants::with_fields() + .variant("Bajs", Fields::named().field_of::("b", "bool")) + .variant("Coo", Fields::unnamed().field_of::("bool")), + ); + assert_type!(Skippy, ty); +} + #[test] fn type_parameters_with_default_bound_works() { trait Formy { diff --git a/test_suite/tests/json.rs b/test_suite/tests/json.rs index 393a670e..9ece5fc3 100644 --- a/test_suite/tests/json.rs +++ b/test_suite/tests/json.rs @@ -243,6 +243,7 @@ fn test_struct_with_some_fields_marked_as_compact() { }]); } +#[test] fn test_struct_with_phantom() { use scale_info::prelude::marker::PhantomData; #[derive(TypeInfo)] diff --git a/test_suite/tests/ui/fail_with_invalid_codec_attrs.rs b/test_suite/tests/ui/fail_with_invalid_codec_attrs.rs index bb542312..a475b3fb 100644 --- a/test_suite/tests/ui/fail_with_invalid_codec_attrs.rs +++ b/test_suite/tests/ui/fail_with_invalid_codec_attrs.rs @@ -2,22 +2,27 @@ use scale_info::TypeInfo; use scale::Encode; #[derive(TypeInfo, Encode)] -struct AttrValidation { +struct NoMultipleAttrs { a: u8, #[codec(skip, compact)] b: u16, } -#[derive(TypeInfo, Encode)] -enum EnumsAttrValidation { - Thing(#[codec(index = 3, compact)] u32), - #[codec(encode_as = u8, compact)] - Thong(bool), - Theng(AttrValidation), +#[derive(Encode, TypeInfo)] +enum NoIndexOnVariantFields { + Thing(#[codec(index = 3)] u32), } -fn assert_type_info() {} +#[derive(Encode, TypeInfo)] +enum IndexMustBeNumber { + #[codec(index = a)] + Thing(u32), +} -fn main() { - assert_type_info::(); +#[derive(Encode, TypeInfo)] +enum EncodeAsAttrMustBeLiteral { + #[codec(encode_as = u8, compact)] + Thong(bool), } + +fn main() {} diff --git a/test_suite/tests/ui/fail_with_invalid_codec_attrs.stderr b/test_suite/tests/ui/fail_with_invalid_codec_attrs.stderr index 0d469171..2fa7f609 100644 --- a/test_suite/tests/ui/fail_with_invalid_codec_attrs.stderr +++ b/test_suite/tests/ui/fail_with_invalid_codec_attrs.stderr @@ -5,7 +5,35 @@ error: Invalid attribute on field, only `#[codec(skip)]`, `#[codec(compact)]` an | ^^^^^^^^^^^^^^^^^^^^ error: Invalid attribute on field, only `#[codec(skip)]`, `#[codec(compact)]` and `#[codec(encoded_as = "$EncodeAs")]` are accepted. - --> $DIR/fail_with_invalid_codec_attrs.rs:13:13 + --> $DIR/fail_with_invalid_codec_attrs.rs:13:19 | -13 | Thing(#[codec(index = 3, compact)] u32), - | ^^^^^^^^^^^^^^^^^^^^^^^^^ +13 | Thing(#[codec(index = 3)] u32), + | ^^^^^^^^^ + +error: proc-macro derive panicked + --> $DIR/fail_with_invalid_codec_attrs.rs:16:18 + | +16 | #[derive(Encode, TypeInfo)] + | ^^^^^^^^ + | + = help: message: scale-info: Bad index in `#[codec(index = …)]`, see `parity-scale-codec` error: Error("expected literal") + +error: expected literal + --> $DIR/fail_with_invalid_codec_attrs.rs:18:21 + | +18 | #[codec(index = a)] + | ^ + +error: proc-macro derive panicked + --> $DIR/fail_with_invalid_codec_attrs.rs:22:18 + | +22 | #[derive(Encode, TypeInfo)] + | ^^^^^^^^ + | + = help: message: scale-info: Bad index in `#[codec(index = …)]`, see `parity-scale-codec` error: Error("expected literal") + +error: expected literal + --> $DIR/fail_with_invalid_codec_attrs.rs:24:25 + | +24 | #[codec(encode_as = u8, compact)] + | ^^