diff --git a/derive/src/attr.rs b/derive/src/attr.rs new file mode 100644 index 00000000..79ff63c1 --- /dev/null +++ b/derive/src/attr.rs @@ -0,0 +1,202 @@ +// 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. + +use syn::{ + parse::{ + Parse, + ParseBuffer, + }, + punctuated::Punctuated, + spanned::Spanned, + Token, +}; + +const SCALE_INFO: &str = "scale_info"; + +mod keywords { + syn::custom_keyword!(scale_info); + syn::custom_keyword!(bounds); + syn::custom_keyword!(skip_type_params); +} + +/// Parsed and validated set of `#[scale_info(...)]` attributes for an item. +pub struct Attributes { + bounds: Option, + skip_type_params: Option, +} + +impl Attributes { + /// Extract out `#[scale_info(...)]` attributes from an item. + pub fn from_ast(item: &syn::DeriveInput) -> syn::Result { + let mut bounds = None; + let mut skip_type_params = None; + + let attributes_parser = |input: &ParseBuffer| { + let attrs: Punctuated = + input.parse_terminated(ScaleInfoAttr::parse)?; + Ok(attrs) + }; + + for attr in &item.attrs { + if !attr.path.is_ident(SCALE_INFO) { + continue + } + let scale_info_attrs = attr.parse_args_with(attributes_parser)?; + + for scale_info_attr in scale_info_attrs { + // check for duplicates + match scale_info_attr { + ScaleInfoAttr::Bounds(parsed_bounds) => { + if bounds.is_some() { + return Err(syn::Error::new( + attr.span(), + "Duplicate `bounds` attributes", + )) + } + bounds = Some(parsed_bounds); + } + ScaleInfoAttr::SkipTypeParams(parsed_skip_type_params) => { + if skip_type_params.is_some() { + return Err(syn::Error::new( + attr.span(), + "Duplicate `skip_type_params` attributes", + )) + } + skip_type_params = Some(parsed_skip_type_params); + } + } + } + } + + // validate type params which do not appear in custom bounds but are not skipped. + if let Some(ref bounds) = bounds { + for type_param in item.generics.type_params() { + if !bounds.contains_type_param(type_param) { + let type_param_skipped = skip_type_params + .as_ref() + .map(|skip| skip.skip(type_param)) + .unwrap_or(false); + if !type_param_skipped { + let msg = format!( + "Type parameter requires a `TypeInfo` bound, so either: \n \ + - add it to `#[scale_info(bounds({}: TypeInfo))]` \n \ + - skip it with `#[scale_info(skip_type_params({}))]`", + type_param.ident, type_param.ident + ); + return Err(syn::Error::new(type_param.span(), msg)) + } + } + } + } + + Ok(Self { + bounds, + skip_type_params, + }) + } + + /// Get the `#[scale_info(bounds(...))]` attribute, if present. + pub fn bounds(&self) -> Option<&BoundsAttr> { + self.bounds.as_ref() + } + + /// Get the `#[scale_info(skip_type_params(...))]` attribute, if present. + pub fn skip_type_params(&self) -> Option<&SkipTypeParamsAttr> { + self.skip_type_params.as_ref() + } +} + +/// Parsed representation of the `#[scale_info(bounds(...))]` attribute. +#[derive(Clone)] +pub struct BoundsAttr { + predicates: Punctuated, +} + +impl Parse for BoundsAttr { + fn parse(input: &ParseBuffer) -> syn::Result { + input.parse::()?; + let content; + syn::parenthesized!(content in input); + let predicates = content.parse_terminated(syn::WherePredicate::parse)?; + Ok(Self { predicates }) + } +} + +impl BoundsAttr { + /// Add the predicates defined in this attribute to the given `where` clause. + pub fn extend_where_clause(&self, where_clause: &mut syn::WhereClause) { + where_clause.predicates.extend(self.predicates.clone()); + } + + /// Returns true if the given type parameter appears in the custom bounds attribute. + pub fn contains_type_param(&self, type_param: &syn::TypeParam) -> bool { + self.predicates.iter().any(|p| { + if let syn::WherePredicate::Type(ty) = p { + if let syn::Type::Path(ref path) = ty.bounded_ty { + path.path.get_ident() == Some(&type_param.ident) + } else { + false + } + } else { + false + } + }) + } +} + +/// Parsed representation of the `#[scale_info(skip_type_params(...))]` attribute. +#[derive(Clone)] +pub struct SkipTypeParamsAttr { + type_params: Punctuated, +} + +impl Parse for SkipTypeParamsAttr { + fn parse(input: &ParseBuffer) -> syn::Result { + input.parse::()?; + let content; + syn::parenthesized!(content in input); + let type_params = content.parse_terminated(syn::TypeParam::parse)?; + Ok(Self { type_params }) + } +} + +impl SkipTypeParamsAttr { + /// Returns `true` if the given type parameter should be skipped. + pub fn skip(&self, type_param: &syn::TypeParam) -> bool { + self.type_params + .iter() + .any(|tp| tp.ident == type_param.ident) + } +} + +/// Parsed representation of one of the `#[scale_info(..)]` attributes. +pub enum ScaleInfoAttr { + Bounds(BoundsAttr), + SkipTypeParams(SkipTypeParamsAttr), +} + +impl Parse for ScaleInfoAttr { + fn parse(input: &ParseBuffer) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(keywords::bounds) { + let bounds = input.parse()?; + Ok(Self::Bounds(bounds)) + } else if lookahead.peek(keywords::skip_type_params) { + let skip_type_params = input.parse()?; + Ok(Self::SkipTypeParams(skip_type_params)) + } else { + Err(input.error("Expected either `bounds` or `skip_type_params`")) + } + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index af46507b..d6a6d32d 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -15,16 +15,10 @@ extern crate alloc; extern crate proc_macro; +mod attr; mod trait_bounds; mod utils; -use alloc::{ - string::{ - String, - ToString, - }, - vec::Vec, -}; use proc_macro::TokenStream; use proc_macro2::{ Span, @@ -66,36 +60,35 @@ fn generate(input: TokenStream2) -> Result { } fn generate_type(input: TokenStream2) -> Result { - let mut ast: DeriveInput = syn::parse2(input.clone())?; + let ast: DeriveInput = syn::parse2(input.clone())?; - utils::check_attributes(&ast)?; + let attrs = attr::Attributes::from_ast(&ast)?; let scale_info = crate_name_ident("scale-info")?; let parity_scale_codec = crate_name_ident("parity-scale-codec")?; let ident = &ast.ident; - let where_clause = if let Some(custom_bounds) = utils::custom_trait_bounds(&ast.attrs) - { - let where_clause = ast.generics.make_where_clause(); - where_clause.predicates.extend(custom_bounds); - where_clause.clone() - } else { - trait_bounds::make_where_clause( - ident, - &ast.generics, - &ast.data, - &scale_info, - &parity_scale_codec, - )? - }; + let where_clause = trait_bounds::make_where_clause( + &attrs, + ident, + &ast.generics, + &ast.data, + &scale_info, + &parity_scale_codec, + )?; let (impl_generics, ty_generics, _) = ast.generics.split_for_impl(); - let generic_type_ids = ast.generics.type_params().map(|ty| { - let ty_ident = &ty.ident; + let type_params = ast.generics.type_params().map(|tp| { + let ty_ident = &tp.ident; + let ty = if attrs.skip_type_params().map_or(true, |skip| !skip.skip(tp)) { + quote! { Some(:: #scale_info ::meta_type::<#ty_ident>()) } + } else { + quote! { None } + }; quote! { - :: #scale_info ::meta_type::<#ty_ident>() + :: #scale_info ::TypeParameter::new(::core::stringify!(#ty_ident), #ty) } }); @@ -112,7 +105,7 @@ fn generate_type(input: TokenStream2) -> Result { fn type_info() -> :: #scale_info ::Type { :: #scale_info ::Type::builder() .path(:: #scale_info ::Path::new(::core::stringify!(#ident), ::core::module_path!())) - .type_params(:: #scale_info ::prelude::vec![ #( #generic_type_ids ),* ]) + .type_params(:: #scale_info ::prelude::vec![ #( #type_params ),* ]) #docs .#build_type } diff --git a/derive/src/trait_bounds.rs b/derive/src/trait_bounds.rs index 96cd6e55..0edfd230 100644 --- a/derive/src/trait_bounds.rs +++ b/derive/src/trait_bounds.rs @@ -29,12 +29,26 @@ use syn::{ WhereClause, }; -use crate::utils; +use crate::{ + attr::Attributes, + 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. +/// +/// # Effect of attributes +/// +/// `#[scale_info(skip_type_params(..))]` +/// +/// Will not add `TypeInfo` bounds for any type parameters skipped via this attribute. +/// +/// `#[scale_info(bounds(..))]` +/// +/// Replaces *all* auto-generated trait bounds with the user-defined ones. pub fn make_where_clause<'a>( + attrs: &'a Attributes, input_ident: &'a Ident, generics: &'a Generics, data: &'a syn::Data, @@ -47,14 +61,29 @@ pub fn make_where_clause<'a>( predicates: Punctuated::new(), } }); + + // Use custom bounds as where clause. + if let Some(custom_bounds) = attrs.bounds() { + custom_bounds.extend_where_clause(&mut where_clause); + + // `'static` lifetime bounds are always required for type parameters, because of the + // requirement on `std::any::TypeId::of` for any field type constructor. + for type_param in generics.type_params() { + let ident = &type_param.ident; + where_clause.predicates.push(parse_quote!(#ident: 'static)) + } + + return Ok(where_clause) + } + for lifetime in generics.lifetimes() { where_clause .predicates .push(parse_quote!(#lifetime: 'static)) } - let type_params = generics.type_params(); - let ty_params_ids = type_params + let ty_params_ids = generics + .type_params() .map(|type_param| type_param.ident.clone()) .collect::>(); @@ -79,7 +108,12 @@ pub fn make_where_clause<'a>( generics.type_params().into_iter().for_each(|type_param| { let ident = type_param.ident.clone(); let mut bounds = type_param.bounds.clone(); - bounds.push(parse_quote!(:: #scale_info ::TypeInfo)); + if attrs + .skip_type_params() + .map_or(true, |skip| !skip.skip(type_param)) + { + bounds.push(parse_quote!(:: #scale_info ::TypeInfo)); + } bounds.push(parse_quote!('static)); where_clause .predicates diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 2c6ea551..15e130d8 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -20,53 +20,15 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{ parse::Parse, - punctuated::Punctuated, spanned::Spanned, - token, AttrStyle, Attribute, - DeriveInput, Lit, Meta, NestedMeta, Variant, }; -/// Trait bounds. -pub type TraitBounds = Punctuated; - -/// Parse `name(T: Bound, N: Bound)` as a custom trait bound. -struct CustomTraitBound { - _name: N, - _paren_token: token::Paren, - bounds: TraitBounds, -} - -impl Parse for CustomTraitBound { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let content; - let _name = input.parse()?; - let _paren_token = syn::parenthesized!(content in input); - let bounds = content.parse_terminated(syn::WherePredicate::parse)?; - Ok(Self { - _name, - _paren_token, - bounds, - }) - } -} - -syn::custom_keyword!(bounds); - -/// Look for a `#[scale_info(bounds(…))]`in the given attributes. -/// -/// If found, use the given trait bounds when deriving the `TypeInfo` trait. -pub fn custom_trait_bounds(attrs: &[Attribute]) -> Option { - scale_info_meta_item(attrs.iter(), |meta: CustomTraitBound| { - Some(meta.bounds) - }) -} - /// 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 { @@ -146,15 +108,6 @@ where find_meta_item("codec", itr, pred) } -fn scale_info_meta_item<'a, F, R, I, M>(itr: I, pred: F) -> Option -where - F: FnMut(M) -> Option + Clone, - I: Iterator, - M: Parse, -{ - find_meta_item("scale_info", itr, pred) -} - fn find_meta_item<'a, F, R, I, M>(kind: &str, mut itr: I, mut pred: F) -> Option where F: FnMut(M) -> Option + Clone, @@ -168,26 +121,3 @@ where .flatten() }) } - -/// Ensure attributes are correctly applied. This *must* be called before using -/// any of the attribute finder methods or the macro may panic if it encounters -/// misapplied attributes. -/// `#[scale_info(bounds())]` is the only accepted attribute. -pub fn check_attributes(input: &DeriveInput) -> syn::Result<()> { - for attr in &input.attrs { - check_top_attribute(attr)?; - } - Ok(()) -} - -// Only `#[scale_info(bounds())]` is a valid top attribute. -fn check_top_attribute(attr: &Attribute) -> syn::Result<()> { - if attr.path.is_ident("scale_info") { - match attr.parse_args::>() { - Ok(_) => Ok(()), - Err(e) => Err(syn::Error::new(attr.span(), format!("Invalid attribute: {:?}. Only `#[scale_info(bounds(…))]` is a valid top attribute", e))) - } - } else { - Ok(()) - } -} diff --git a/src/build.rs b/src/build.rs index 2a44e1a8..9cff2a93 100644 --- a/src/build.rs +++ b/src/build.rs @@ -22,7 +22,7 @@ //! //! ## Generic struct //! ``` -//! # use scale_info::{build::Fields, MetaType, Path, Type, TypeInfo}; +//! # use scale_info::{build::Fields, type_params, MetaType, Path, Type, TypeInfo}; //! struct Foo { //! bar: T, //! data: u64, @@ -37,7 +37,7 @@ //! fn type_info() -> Type { //! Type::builder() //! .path(Path::new("Foo", module_path!())) -//! .type_params(vec![MetaType::new::()]) +//! .type_params(type_params!(T)) //! .composite(Fields::named() //! .field(|f| f.ty::().name("bar").type_name("T")) //! .field(|f| f.ty::().name("data").type_name("u64")) @@ -65,7 +65,7 @@ //! ``` //! ## Enum with fields //! ``` -//! # use scale_info::{build::{Fields, Variants}, MetaType, Path, Type, TypeInfo, Variant}; +//! # use scale_info::{build::{Fields, Variants}, type_params, MetaType, Path, Type, TypeInfo, Variant}; //! enum Foo{ //! A(T), //! B { f: u32 }, @@ -81,7 +81,7 @@ //! fn type_info() -> Type { //! Type::builder() //! .path(Path::new("Foo", module_path!())) -//! .type_params(vec![MetaType::new::()]) +//! .type_params(type_params!(T)) //! .variant( //! Variants::new() //! .variant("A", |v| v.fields(Fields::unnamed().field(|f| f.ty::().type_name("T")))) @@ -131,6 +131,7 @@ use crate::{ TypeDefComposite, TypeDefVariant, TypeInfo, + TypeParameter, Variant, }; @@ -145,7 +146,7 @@ pub mod state { /// Builds a [`Type`](`crate::Type`) pub struct TypeBuilder { path: Option, - type_params: Vec, + type_params: Vec, docs: Vec<&'static str>, marker: PhantomData S>, } @@ -197,7 +198,7 @@ impl TypeBuilder { /// Set the type parameters if it's a generic type pub fn type_params(mut self, type_params: I) -> Self where - I: IntoIterator, + I: IntoIterator, { self.type_params = type_params.into_iter().collect(); self diff --git a/src/impls.rs b/src/impls.rs index e576197e..94753340 100644 --- a/src/impls.rs +++ b/src/impls.rs @@ -187,7 +187,7 @@ where fn type_info() -> Type { Type::builder() .path(Path::prelude("Option")) - .type_params(tuple_meta_type![T]) + .type_params(type_params![T]) .variant(Variants::new().variant("None", |v| v).variant("Some", |v| { v.fields(Fields::unnamed().field(|f| f.ty::())) })) @@ -204,7 +204,7 @@ where fn type_info() -> Type { Type::builder() .path(Path::prelude("Result")) - .type_params(tuple_meta_type!(T, E)) + .type_params(type_params!(T, E)) .variant( Variants::new() .variant("Ok", |v| v.fields(Fields::unnamed().field(|f| f.ty::()))) @@ -224,7 +224,7 @@ where fn type_info() -> Type { Type::builder() .path(Path::prelude("Cow")) - .type_params(tuple_meta_type!(T)) + .type_params(type_params!(T)) .composite(Fields::unnamed().field(|f| f.ty::())) } } @@ -239,7 +239,7 @@ where fn type_info() -> Type { Type::builder() .path(Path::prelude("BTreeMap")) - .type_params(tuple_meta_type![(K, V)]) + .type_params(type_params![K, V]) .composite(Fields::unnamed().field(|f| f.ty::<[(K, V)]>())) } } @@ -253,7 +253,7 @@ where fn type_info() -> Type { Type::builder() .path(Path::prelude("BTreeSet")) - .type_params(tuple_meta_type![T]) + .type_params(type_params![T]) .composite(Fields::unnamed().field(|f| f.ty::<[T]>())) } } @@ -318,14 +318,11 @@ impl TypeInfo for String { } } -impl TypeInfo for PhantomData -where - T: TypeInfo + ?Sized + 'static, -{ - type Identity = Self; +impl TypeInfo for PhantomData { + type Identity = PhantomData<()>; fn type_info() -> Type { - TypeDefPhantom::new(MetaType::new::()).into() + TypeDefPhantom.into() } } diff --git a/src/lib.rs b/src/lib.rs index fb753784..08436429 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,160 @@ //! `scale-info` provides implementations for all commonly used Rust standard //! types and a derive macro for implementing of custom types. //! +//! ## Deriving `TypeInfo` +//! +//! Enable the `derive` feature of this crate: +//! +//! ```toml +//! scale-info = { version = "0.6.0", features = ["derive"] } +//! ``` +//! +//! ```ignore +//! use scale_info::TypeInfo; +//! +//! #[derive(TypeInfo)] +//! struct MyStruct { +//! a: u32, +//! b: MyEnum, +//! } +//! +//! #[derive(TypeInfo)] +//! enum MyEnum { +//! A(bool), +//! B { f: Vec }, +//! C, +//! } +//! ``` +//! +//! ### Attributes +//! +//! #### `#[scale_info(bounds(..))]` +//! +//! Replace the auto-generated `where` clause bounds for the derived `TypeInfo` implementation. +//! +//! ```ignore +//! #[derive(TypeInfo)] +//! #[scale_info(bounds(T: TypeInfo + 'static))] +//! struct MyStruct { +//! a: Vec, +//! } +//! ``` +//! +//! The derive macro automatically adds `TypeInfo` bounds for all type parameters, and all field +//! types containing type parameters or associated types. +//! +//! This is naive and sometimes adds unnecessary bounds, since on a syntactical level there is no +//! way to differentiate between a generic type constructor and a type alias with a generic argument +//! e.g. +//! +//! ```ignore +//! trait MyTrait { +//! type A; +//! } +//! +//! type MyAlias = ::A; +//! +//! #[derive(TypeInfo)] +//! struct MyStruct { +//! a: MyAlias, +//! b: Vec, +//! } +//! ``` +//! +//! So for the above, since a `MyAlias: TypeInfo` bound is required, and we can't distinguish +//! between `MyAlias` and `Vec`, then the `TypeInfo` bound is simply added for all +//! fields which contain any type param. In this case the redundant `Vec: TypeInfo` +//! would be added. +//! +//! This is usually okay, but in some circumstances can cause problems, for example with the +//! [`overflow evaluating the requirement`] error [here](https://github.com/paritytech/scale-info/blob/master/test_suite/tests/ui/pass_custom_bounds_fix_overflow.rs). +//! +//! The `bounds` attribute provides an ["escape hatch"](https://serde.rs/attr-bound.html) to allow +//! the programmer control of the `where` clause on the generated `impl`, to solve this and other +//! issues that can't be foreseen by the auto-generated bounds heuristic. +//! +//! #### `#[scale_info(skip_type_params(..))]` +//! +//! Remove the requirement for the specified type params to implement `TypeInfo`. +//! +//! Consider a simple example of a type parameter which is used for associated types, but the type +//! itself does not carry any type information. Consider this common pattern: +//! +//! ```ignore +//! trait Config { +//! type Balance; +//! } +//! +//! struct Runtime; // doesn't implement `TypeInfo` +//! +//! impl Config for Runtime { +//! type Balance = u64; +//! } +//! +//! #[allow(unused)] +//! #[derive(TypeInfo)] +//! #[scale_info(skip_type_params(T))] +//! struct A { +//! balance: T::Balance, +//! marker: core::marker::PhantomData, +//! } +//! +//! fn assert_type_info() {} +//! +//! fn main() { +//! // without the `skip_type_params` attribute this will fail. +//! assert_type_info::>(); +//! } +//! ``` +//! +//! By default, the derived `TypeInfo` implementation will add the type of all type parameters to +//! the `TypeParameter` specification e.g. +//! +//! `type_params(vec![TypeParameter::new("T", Some(MetaType::new::()))])` +//! +//! In the example above, this will cause a compiler error because `Runtime` is the concrete tyoe +//! for `T`, which does not satisfy the `TypeInfo` requirement of `MetaType::new::()`. +//! +//! Simply adding a `TypeInfo` derive to `Runtime` is one way of solving this, but that could be +//! misleading (why does it need `TypeInfo` if a value of that type is never encoded?), and can +//! sometimes require adding `TypeInfo` bounds in other impl blocks. +//! +//! The `skip_type_params` attribute solves this, providing an additional "escape hatch" which +//! prevents the given type parameter's type information being required: +//! +//! `type_params(vec![TypeParameter::new("T", None)])` +//! +//! The generated type params do not now require `T` to implement `TypeInfo`, so the auto-generated +//! bound is not added to the generated `TypeInfo` `where` clause. +//! +//! #### Combining `bounds` and `skip_type_params` +//! +//! These two attributes can complement one another, particularly in the case where using `bounds` +//! would still require manually adding a `TypeInfo` bound for the type parameter: +//! +//! ```ignore +//! #[derive(TypeInfo)] +//! #[scale_info(bounds(), skip_type_params(T))] +//! struct A { +//! marker: core::marker::PhantomData, +//! } +//! ``` +//! +//! Without `skip_type_params` in the example above, it would require the `TypeInfo` bounds for `T` +//! to be added manually e.g. `#[scale_info(bounds(T: TypeInfo + 'static))]`. Since the intention of +//! the empty bounds is to **remove** all type bounds, then the addition of `skip_type_params` +//! allows this to compile successfully. +//! +//! ##### Precedence +//! +//! When used independently, both attributes modify the `where` clause of the derived `TypeInfo` +//! impl. When used together, the predicates supplied in the `bounds` attribute replaces *all* +//! auto-generated bounds, and `skip_type_params` will have no effect on the resulting `where` +//! clause. +//! +//! **Note:** When using `bounds` without `skip_type_params`, it is therefore required to manually +//! add a `TypeInfo` bound for any non skipped type parameters. The compiler will let you know. +//! //! # Forms //! //! To bridge between compile-time type information and runtime the @@ -70,7 +224,7 @@ /// [`MetaType`](`crate::MetaType`) instances. /// /// This is useful for places that require inputs of iterators over [`MetaType`](`crate::MetaType`) -/// instances and provide a way out of code bloat in these scenarious. +/// instances and provide a way out of code bloat in these scenarios. /// /// # Example /// @@ -101,6 +255,45 @@ macro_rules! tuple_meta_type { } } +/// Construct a vector of `TypeParameter`s from pairs of the name and the concrete type. +/// +/// # Example +/// +/// ``` +/// # use scale_info::{named_type_params, MetaType, TypeParameter}; +/// assert_eq!( +/// named_type_params![(T, u8), (U, u32)], +/// vec! [ +/// TypeParameter::new("T", Some(MetaType::new::())), +/// TypeParameter::new("U", Some(MetaType::new::())), +/// ] +/// ); +/// ``` +#[macro_export] +macro_rules! named_type_params { + ( $(($tp:ty, $ty:ty)),* ) => { + { + $crate::prelude::vec![ + $( + $crate::TypeParameter::<$crate::form::MetaForm>::new( + ::core::stringify!($tp), + Some($crate::MetaType::new::<$ty>()) + ), + )* + ] + } + } +} + +/// Construct a vector of [`TypeParameter`] instances with the name of the type parameter, +/// together with its concrete [`MetaType`]. +#[macro_export] +macro_rules! type_params { + ( $($ty:ty),* ) => { + $crate::named_type_params!{ $( ($ty, $ty) ),* } + } +} + pub mod prelude; pub mod build; diff --git a/src/tests.rs b/src/tests.rs index a9501a20..4b54ee48 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -61,7 +61,7 @@ fn prelude_items() { Option, Type::builder() .path(Path::prelude("Option")) - .type_params(tuple_meta_type!(u128)) + .type_params(named_type_params![(T, u128)]) .variant(Variants::new().variant("None", |v| v).variant("Some", |v| { v.fields(Fields::unnamed().field(|f| f.ty::())) })) @@ -70,7 +70,7 @@ fn prelude_items() { Result, Type::builder() .path(Path::prelude("Result")) - .type_params(tuple_meta_type!(bool, String)) + .type_params(named_type_params![(T, bool), (E, String)]) .variant( Variants::new() .variant( @@ -83,12 +83,12 @@ fn prelude_items() { ) ) ); - assert_type!(PhantomData, TypeDefPhantom::new(meta_type::())); + assert_type!(PhantomData, TypeDefPhantom); assert_type!( Cow, Type::builder() .path(Path::prelude("Cow")) - .type_params(tuple_meta_type!(u128)) + .type_params(named_type_params![(T, u128)]) .composite(Fields::unnamed().field(|f| f.ty::())) ); @@ -106,7 +106,7 @@ fn collections() { BTreeMap, Type::builder() .path(Path::prelude("BTreeMap")) - .type_params(tuple_meta_type![(String, u32)]) + .type_params(named_type_params![(K, String), (V, u32)]) .composite(Fields::unnamed().field(|f| f.ty::<[(String, u32)]>())) ); @@ -114,7 +114,7 @@ fn collections() { BTreeSet, Type::builder() .path(Path::prelude("BTreeSet")) - .type_params(tuple_meta_type![String]) + .type_params(named_type_params![(T, String)]) .composite(Fields::unnamed().field(|f| f.ty::<[String]>())) ); @@ -205,7 +205,7 @@ fn struct_with_generics() { fn type_info() -> Type { Type::builder() .path(Path::new("MyStruct", module_path!())) - .type_params(tuple_meta_type!(T)) + .type_params(type_params!(T)) .composite( Fields::named().field(|f| f.ty::().name("data").type_name("T")), ) @@ -215,7 +215,7 @@ fn struct_with_generics() { // Normal struct let struct_bool_type_info = Type::builder() .path(Path::from_segments(vec!["scale_info", "tests", "MyStruct"]).unwrap()) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params![(T, bool)]) .composite(Fields::named().field(|f| f.ty::().name("data").type_name("T"))); assert_type!(MyStruct, struct_bool_type_info); @@ -224,7 +224,7 @@ fn struct_with_generics() { type SelfTyped = MyStruct>>; let expected_type = Type::builder() .path(Path::new("MyStruct", "scale_info::tests")) - .type_params(tuple_meta_type!(Box>)) + .type_params(named_type_params![(T, Box>)]) .composite( Fields::named() .field(|f| f.ty::>>().name("data").type_name("T")), @@ -249,7 +249,7 @@ fn basic_struct_with_phantoms() { fn type_info() -> Type { Type::builder() .path(Path::new("SomeStruct", module_path!())) - .type_params(tuple_meta_type!(T)) + .type_params(type_params!(T)) .composite( Fields::named().field(|f| f.ty::().name("a").type_name("u8")), ) @@ -258,7 +258,7 @@ fn basic_struct_with_phantoms() { let struct_bool_type_info = Type::builder() .path(Path::from_segments(vec!["scale_info", "tests", "SomeStruct"]).unwrap()) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params![(T, bool)]) .composite(Fields::named().field(|f| f.ty::().name("a").type_name("u8"))); assert_type!(SomeStruct, struct_bool_type_info); diff --git a/src/ty/mod.rs b/src/ty/mod.rs index dde761fe..69f50080 100644 --- a/src/ty/mod.rs +++ b/src/ty/mod.rs @@ -74,7 +74,7 @@ pub struct Type { feature = "serde", serde(rename = "params", skip_serializing_if = "Vec::is_empty", default) )] - type_params: Vec, + type_params: Vec>, /// The actual type definition #[cfg_attr(feature = "serde", serde(rename = "def"))] type_def: TypeDef, @@ -92,7 +92,7 @@ impl IntoPortable for Type { fn into_portable(self, registry: &mut Registry) -> Self::Output { Type { path: self.path.into_portable(registry), - type_params: registry.register_types(self.type_params), + type_params: registry.map_into_portable(self.type_params), type_def: self.type_def.into_portable(registry), docs: registry.map_into_portable(self.docs), } @@ -132,7 +132,7 @@ impl Type { docs: Vec<&'static str>, ) -> Self where - I: IntoIterator, + I: IntoIterator, D: Into, { Self { @@ -154,7 +154,7 @@ where } /// Returns the generic type parameters of the type - pub fn type_params(&self) -> &[T::Type] { + pub fn type_params(&self) -> &[TypeParameter] { &self.type_params } @@ -169,6 +169,48 @@ where } } +/// A generic type parameter. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde", + serde(bound( + serialize = "T::Type: Serialize, T::String: Serialize", + deserialize = "T::Type: DeserializeOwned, T::String: DeserializeOwned", + )) +)] +#[cfg_attr(any(feature = "std", feature = "decode"), derive(scale::Decode))] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, From, Debug, Encode)] +pub struct TypeParameter { + /// The name of the generic type parameter e.g. "T". + name: T::String, + /// The concrete type for the type parameter. + /// + /// `None` if the type parameter is skipped. + #[cfg_attr(feature = "serde", serde(rename = "type"))] + ty: Option, +} + +impl IntoPortable for TypeParameter { + type Output = TypeParameter; + + fn into_portable(self, registry: &mut Registry) -> Self::Output { + TypeParameter { + name: self.name.into_portable(registry), + ty: self.ty.map(|ty| registry.register_type(&ty)), + } + } +} + +impl TypeParameter +where + T: Form, +{ + /// Create a new [`TypeParameter`]. + pub fn new(name: T::String, ty: Option) -> Self { + Self { name, ty } + } +} + /// The possible types a SCALE encodable Rust value could have. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr( @@ -197,7 +239,7 @@ pub enum TypeDef { /// A type using the [`Compact`] encoding Compact(TypeDefCompact), /// A PhantomData type. - Phantom(TypeDefPhantom), + Phantom(TypeDefPhantom), /// A type representing a sequence of bits. BitSequence(TypeDefBitSequence), } @@ -214,7 +256,7 @@ impl IntoPortable for TypeDef { TypeDef::Tuple(tuple) => tuple.into_portable(registry).into(), TypeDef::Primitive(primitive) => primitive.into(), TypeDef::Compact(compact) => compact.into_portable(registry).into(), - TypeDef::Phantom(phantom) => phantom.into_portable(registry).into(), + TypeDef::Phantom(phantom) => phantom.into(), TypeDef::BitSequence(bitseq) => bitseq.into_portable(registry).into(), } } @@ -457,38 +499,7 @@ where #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(any(feature = "std", feature = "decode"), derive(scale::Decode))] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Debug)] -pub struct TypeDefPhantom { - /// The PhantomData type parameter - #[cfg_attr(feature = "serde", serde(rename = "type"))] - type_param: T::Type, -} - -impl IntoPortable for TypeDefPhantom { - type Output = TypeDefPhantom; - - fn into_portable(self, registry: &mut Registry) -> Self::Output { - TypeDefPhantom { - type_param: registry.register_type(&self.type_param), - } - } -} - -impl TypeDefPhantom { - /// Creates a new phantom type definition. - pub fn new(type_param: MetaType) -> Self { - Self { type_param } - } -} - -impl TypeDefPhantom -where - T: Form, -{ - /// Returns the type parameter type of the phantom type. - pub fn type_param(&self) -> &T::Type { - &self.type_param - } -} +pub struct TypeDefPhantom; /// Type describing a [`bitvec::vec::BitVec`]. /// diff --git a/test_suite/tests/derive.rs b/test_suite/tests/derive.rs index c2ea7a6c..7693bc75 100644 --- a/test_suite/tests/derive.rs +++ b/test_suite/tests/derive.rs @@ -18,15 +18,18 @@ use pretty_assertions::assert_eq; use scale::Encode; use scale_info::{ build::*, + named_type_params, prelude::{ boxed::Box, marker::PhantomData, + vec, vec::Vec, }, - tuple_meta_type, + MetaType, Path, Type, TypeInfo, + TypeParameter, }; fn assert_type(expected: E) @@ -57,7 +60,7 @@ fn struct_derive() { let struct_type = Type::builder() .path(Path::new("S", "derive")) - .type_params(tuple_meta_type!(bool, u8)) + .type_params(named_type_params![(T, bool), (U, u8)]) .docs(&["Type docs.", "Multiline."]) .composite( Fields::named() @@ -78,7 +81,7 @@ fn struct_derive() { let self_typed_type = Type::builder() .path(Path::new("S", "derive")) - .type_params(tuple_meta_type!(Box>, bool)) + .type_params(named_type_params!((T, Box>), (U, bool))) .docs(&["Type docs.", "Multiline."]) .composite( Fields::named() @@ -104,7 +107,7 @@ fn phantom_data_is_part_of_the_type_info() { let ty = Type::builder() .path(Path::new("P", "derive")) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params!((T, bool))) .composite( Fields::named() .field(|f| f.ty::().name("a").type_name("u8")) @@ -130,7 +133,7 @@ fn tuple_struct_derive() { let ty = Type::builder() .path(Path::new("S", "derive")) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params!((T, bool))) .docs(&["Type docs."]) .composite( Fields::unnamed() @@ -227,7 +230,7 @@ fn enum_derive() { let ty = Type::builder() .path(Path::new("E", "derive")) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params!((T, bool))) .docs(&["Enum docs."]) .variant( Variants::new() @@ -267,7 +270,7 @@ fn enum_derive_with_codec_index() { let ty = Type::builder() .path(Path::new("E", "derive")) - .type_params(tuple_meta_type!(bool)) + .type_params(named_type_params!((T, bool))) .variant( Variants::new() .variant("A", |v| { @@ -357,7 +360,7 @@ fn associated_types_derive_without_bounds() { let struct_type = Type::builder() .path(Path::new("Assoc", "derive")) - .type_params(tuple_meta_type!(ConcreteTypes)) + .type_params(named_type_params![(T, ConcreteTypes)]) .composite( Fields::named() .field(|f| f.ty::().name("a").type_name("T::A")) @@ -389,7 +392,7 @@ fn associated_types_named_like_the_derived_type_works() { let struct_type = Type::builder() .path(Path::new("Assoc", "derive")) - .type_params(tuple_meta_type!(ConcreteTypes)) + .type_params(named_type_params![(T, ConcreteTypes)]) .composite( Fields::named() .field(|f| f.ty::>().name("a").type_name("Vec")) @@ -433,7 +436,7 @@ fn scale_compact_types_work_in_enums() { let ty = Type::builder() .path(Path::new("MutilatedMultiAddress", "derive")) - .type_params(tuple_meta_type!(u8, u16)) + .type_params(named_type_params![(AccountId, u8), (AccountIndex, u16)]) .variant( Variants::new() .variant("Id", |v| { @@ -546,7 +549,7 @@ fn type_parameters_with_default_bound_works() { let ty = Type::builder() .path(Path::new("Bat", "derive")) - .type_params(tuple_meta_type!(MetaFormy)) + .type_params(named_type_params![(TTT, MetaFormy)]) .composite( Fields::named().field(|f| f.ty::().name("one").type_name("TTT")), ); @@ -609,6 +612,152 @@ fn doc_capture_works() { assert_type!(S, ty); } +#[test] +fn skip_type_params_nested() { + #[allow(unused)] + #[derive(TypeInfo)] + #[scale_info(skip_type_params(T))] + struct SkipTypeParamsNested { + a: Nested, + b: U, + } + + #[derive(TypeInfo)] + #[scale_info(skip_type_params(T))] + struct Nested { + marker: PhantomData, + } + + struct NoScaleInfoImpl; + + let ty = Type::builder() + .path(Path::new("SkipTypeParamsNested", "derive")) + .type_params(vec![ + TypeParameter::new("T", None), + TypeParameter::new("U", Some(MetaType::new::())), + ]) + .composite( + Fields::named() + .field(|f| { + f.ty::>() + .name("a") + .type_name("Nested") + }) + .field(|f| f.ty::().name("b").type_name("U")), + ); + + assert_type!(SkipTypeParamsNested, ty); +} + +#[test] +fn skip_all_type_params() { + #[allow(unused)] + #[derive(TypeInfo)] + #[scale_info(skip_type_params(T, U))] + struct SkipAllTypeParams { + a: PhantomData, + b: PhantomData, + } + + struct NoScaleInfoImpl; + + let ty = Type::builder() + .path(Path::new("SkipAllTypeParams", "derive")) + .type_params(vec![ + TypeParameter::new("T", None), + TypeParameter::new("U", None), + ]) + .composite( + Fields::named() + .field(|f| { + f.ty::>() + .name("a") + .type_name("PhantomData") + }) + .field(|f| { + f.ty::>() + .name("b") + .type_name("PhantomData") + }), + ); + + assert_type!(SkipAllTypeParams, ty); +} + +#[test] +fn skip_type_params_with_associated_types() { + trait Trait { + type A; + } + + #[allow(unused)] + #[derive(TypeInfo)] + #[scale_info(skip_type_params(T))] + struct SkipTypeParamsForTraitImpl + where + T: Trait, + { + a: PhantomData, + b: T::A, + } + + struct NoScaleInfoImpl; + + impl Trait for NoScaleInfoImpl { + type A = u32; + } + + let ty = Type::builder() + .path(Path::new("SkipTypeParamsForTraitImpl", "derive")) + .type_params(vec![TypeParameter::new("T", None)]) + .composite( + Fields::named() + .field(|f| { + f.ty::>() + .name("a") + .type_name("PhantomData") + }) + .field(|f| f.ty::().name("b").type_name("T::A")), + ); + + assert_type!(SkipTypeParamsForTraitImpl, ty); +} + +#[test] +fn skip_type_params_with_defaults() { + #[allow(unused)] + #[derive(TypeInfo)] + #[scale_info(skip_type_params(T, U))] + struct SkipAllTypeParamsWithDefaults { + a: PhantomData, + b: PhantomData, + } + + struct NoScaleInfoImpl; + + let ty = Type::builder() + .path(Path::new("SkipAllTypeParamsWithDefaults", "derive")) + .type_params(vec![ + TypeParameter::new("T", None), + TypeParameter::new("U", None), + ]) + .composite( + Fields::named() + .field(|f| { + f.ty::>() + .name("a") + .type_name("PhantomData") + }) + .field(|f| { + f.ty::>() + .name("b") + .type_name("PhantomData") + }), + ); + + assert_type!(SkipAllTypeParamsWithDefaults, ty); +} + #[rustversion::nightly] #[test] fn ui_tests() { diff --git a/test_suite/tests/json.rs b/test_suite/tests/json.rs index ca047cce..7b70993c 100644 --- a/test_suite/tests/json.rs +++ b/test_suite/tests/json.rs @@ -118,7 +118,9 @@ fn test_builtins() { // complex types assert_json_for_type::>(json!({ "path": ["Option"], - "params": [0], + "params": [ + { "name": "T", "type": 0 } + ], "def": { "variant": { "variants": [ @@ -135,7 +137,10 @@ fn test_builtins() { })); assert_json_for_type::>(json!({ "path": ["Result"], - "params": [0, 1], + "params": [ + { "name": "T", "type": 0 }, + { "name": "E", "type": 1 } + ], "def": { "variant": { "variants": [ @@ -159,9 +164,7 @@ fn test_builtins() { assert_json_for_type::(json!({ "def": { "primitive": "str" } })); assert_json_for_type::(json!({ "def": { "primitive": "str" } })); // PhantomData - assert_json_for_type::>( - json!({ "def": { "phantom": { "type": 0 } }, }), - ) + assert_json_for_type::>(json!({ "def": { "phantom": null }, })) } #[test] @@ -278,7 +281,9 @@ fn test_struct_with_phantom() { assert_json_for_type::>(json!({ "path": ["json", "Struct"], - "params": [0], + "params": [ + { "name": "T", "type": 0 } + ], "def": { "composite": { "fields": [ diff --git a/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.rs b/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.rs new file mode 100644 index 00000000..7682007e --- /dev/null +++ b/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.rs @@ -0,0 +1,10 @@ +use scale_info::TypeInfo; +use core::marker::PhantomData; + +#[derive(TypeInfo)] +#[scale_info(bounds())] +struct A { + marker: PhantomData, +} + +fn main() {} \ No newline at end of file diff --git a/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.stderr b/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.stderr new file mode 100644 index 00000000..ead7b32f --- /dev/null +++ b/test_suite/tests/ui/fail_custom_bounds_missing_skip_type_params.stderr @@ -0,0 +1,7 @@ +error: Type parameter requires a `TypeInfo` bound, so either: + - add it to `#[scale_info(bounds(T: TypeInfo))]` + - skip it with `#[scale_info(skip_type_params(T))]` + --> $DIR/fail_custom_bounds_missing_skip_type_params.rs:6:10 + | +6 | struct A { + | ^ diff --git a/test_suite/tests/ui/fail_duplicate_bounds_params.rs b/test_suite/tests/ui/fail_duplicate_bounds_params.rs new file mode 100644 index 00000000..98b27c6b --- /dev/null +++ b/test_suite/tests/ui/fail_duplicate_bounds_params.rs @@ -0,0 +1,10 @@ +use scale_info::TypeInfo; +use core::marker::PhantomData; + +#[derive(TypeInfo)] +#[scale_info(bounds(), bounds())] +struct A { + marker: PhantomData, +} + +fn main() {} \ No newline at end of file diff --git a/test_suite/tests/ui/fail_duplicate_bounds_params.stderr b/test_suite/tests/ui/fail_duplicate_bounds_params.stderr new file mode 100644 index 00000000..893bc6a3 --- /dev/null +++ b/test_suite/tests/ui/fail_duplicate_bounds_params.stderr @@ -0,0 +1,5 @@ +error: Duplicate `bounds` attributes + --> $DIR/fail_duplicate_bounds_params.rs:5:1 + | +5 | #[scale_info(bounds(), bounds())] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/test_suite/tests/ui/pass_combined_attributes.rs b/test_suite/tests/ui/pass_combined_attributes.rs new file mode 100644 index 00000000..b9208860 --- /dev/null +++ b/test_suite/tests/ui/pass_combined_attributes.rs @@ -0,0 +1,18 @@ +use scale_info::TypeInfo; + +#[allow(unused)] +#[derive(TypeInfo)] +#[scale_info(bounds(), skip_type_params(T))] +struct A { + marker: core::marker::PhantomData, +} + +#[allow(unused)] +#[derive(TypeInfo)] +#[scale_info(bounds())] +#[scale_info(skip_type_params(T))] +struct B { + marker: core::marker::PhantomData, +} + +fn main() { } \ No newline at end of file diff --git a/test_suite/tests/ui/pass_custom_bounds_empty.rs b/test_suite/tests/ui/pass_custom_bounds_empty.rs new file mode 100644 index 00000000..894d2fd7 --- /dev/null +++ b/test_suite/tests/ui/pass_custom_bounds_empty.rs @@ -0,0 +1,10 @@ +use scale_info::TypeInfo; +use core::marker::PhantomData; + +#[derive(TypeInfo)] +#[scale_info(bounds(), skip_type_params(T))] +struct A { + marker: PhantomData, +} + +fn main() {} \ No newline at end of file diff --git a/test_suite/tests/ui/pass_skip_type_params.rs b/test_suite/tests/ui/pass_skip_type_params.rs new file mode 100644 index 00000000..37feb4aa --- /dev/null +++ b/test_suite/tests/ui/pass_skip_type_params.rs @@ -0,0 +1,25 @@ +use scale_info::TypeInfo; + +trait Config { + type Balance; +} + +struct Runtime; + +impl Config for Runtime { + type Balance = u64; +} + +#[allow(unused)] +#[derive(TypeInfo)] +#[scale_info(skip_type_params(T))] +struct A { + balance: T::Balance, + marker: core::marker::PhantomData, +} + +fn assert_type_info() {} + +fn main() { + assert_type_info::>(); +} \ No newline at end of file