Allow pallet error enum variants to contain fields (#10242)

* Allow pallet errors to contain at most one field

* Update docs on pallet::error

* Reword documentation

* cargo fmt

* Introduce CompactPalletError trait and require #[pallet::error] fields to implement them

* cargo fmt

* Do not assume tuple variants

* Add CompactPalletError derive macro

* Check for error type compactness in construct_runtime

* cargo fmt

* Derive CompactPalletError instead of implementing it directly during macro expansion

* Implement CompactPalletError on OptionBool instead of Option<bool>

* Check for type idents instead of variant ident

* Add doc comments for ErrorCompactnessTest

* Add an trait implementation of ErrorCompactnessTest for ()

* Convert the error field of DispatchError to a 4-element byte array

* Add static check for pallet error size

* Rename to MAX_PALLET_ERROR_ENCODED_SIZE

* Remove ErrorCompactnessTest trait

* Remove check_compactness

* Return only the most significant byte when constructing a custom InvalidTransaction

* Rename CompactPalletError to PalletError

* Use counter to generate unique idents for assert macros

* Make declarative pallet macros compile with pallet error size checks

* Remove unused doc comment

* Try and fix build errors

* Fix build errors

* Add macro_use for some test modules

* Test fix

* Fix compilation errors

* Remove unneeded #[macro_use]

* Resolve import ambiguity

* Make path to pallet Error enum more specific

* Fix test expectation

* Disambiguate imports

* Fix test expectations

* Revert appending pallet module name to path

* Rename bags_list::list::Error to BagError

* Fixes

* Fixes

* Fixes

* Fix test expectations

* Fix test expectation

* Add more implementations for PalletError

* Lift the 1-field requirement for nested pallet errors

* Fix UI test expectation

* Remove PalletError impl for OptionBool

* Use saturating operations

* cargo fmt

* Delete obsolete test

* Fix test expectation

* Try and use assert macro in const context

* Pull out the pallet error size check macro

* Fix UI test for const assertion

* cargo fmt

* Apply clippy suggestion

* Fix doc comment

* Docs for create_tt_return_macro

* Ensure TryInto is imported in earlier Rust editions

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Fix up comments and names

* Implement PalletError for Never

* cargo fmt

* Don't compile example code

* Bump API version for block builder

* Factor in codec attributes while derving PalletError

* Rename module and fix unit test

* Add missing attribute

* Check API version and convert ApplyExtrinsicResult accordingly

* Rename BagError to ListError

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Use codec crate re-exported from frame support

* Add links to types mentioned in doc comments

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* cargo fmt

* cargo fmt

* Re-add attribute for hidden docs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Keith Yeung
2022-03-24 09:11:14 +01:00
committed by GitHub
parent 5c9f23af13
commit 208be86934
38 changed files with 1263 additions and 241 deletions
@@ -241,6 +241,7 @@ fn construct_runtime_final_expansion(
expand::expand_outer_inherent(&name, &block, &unchecked_extrinsic, &pallets, &scrate);
let validate_unsigned = expand::expand_outer_validate_unsigned(&name, &pallets, &scrate);
let integrity_test = decl_integrity_test(&scrate);
let static_assertions = decl_static_assertions(&name, &pallets, &scrate);
let res = quote!(
#scrate_decl
@@ -282,6 +283,8 @@ fn construct_runtime_final_expansion(
#validate_unsigned
#integrity_test
#static_assertions
);
Ok(res)
@@ -471,3 +474,34 @@ fn decl_integrity_test(scrate: &TokenStream2) -> TokenStream2 {
}
)
}
fn decl_static_assertions(
runtime: &Ident,
pallet_decls: &[Pallet],
scrate: &TokenStream2,
) -> TokenStream2 {
let error_encoded_size_check = pallet_decls.iter().map(|decl| {
let path = &decl.path;
let assert_message = format!(
"The maximum encoded size of the error type in the `{}` pallet exceeds \
`MAX_MODULE_ERROR_ENCODED_SIZE`",
decl.name,
);
quote! {
#scrate::tt_call! {
macro = [{ #path::tt_error_token }]
frame_support = [{ #scrate }]
~~> #scrate::assert_error_encoded_size! {
path = [{ #path }]
runtime = [{ #runtime }]
assert_message = [{ #assert_message }]
}
}
}
});
quote! {
#(#error_encoded_size_check)*
}
}
+16 -3
View File
@@ -28,9 +28,11 @@ mod dummy_part_checker;
mod key_prefix;
mod match_and_insert;
mod pallet;
mod pallet_error;
mod partial_eq_no_bound;
mod storage;
mod transactional;
mod tt_macro;
use proc_macro::TokenStream;
use std::{cell::RefCell, str::FromStr};
@@ -41,9 +43,9 @@ thread_local! {
static COUNTER: RefCell<Counter> = RefCell::new(Counter(0));
}
/// Counter to generate a relatively unique identifier for macros querying for the existence of
/// pallet parts. This is necessary because declarative macros gets hoisted to the crate root,
/// which shares the namespace with other pallets containing the very same query macros.
/// Counter to generate a relatively unique identifier for macros. This is necessary because
/// declarative macros gets hoisted to the crate root, which shares the namespace with other pallets
/// containing the very same macros.
struct Counter(u64);
impl Counter {
@@ -562,3 +564,14 @@ pub fn __generate_dummy_part_checker(input: TokenStream) -> TokenStream {
pub fn match_and_insert(input: TokenStream) -> TokenStream {
match_and_insert::match_and_insert(input)
}
#[proc_macro_derive(PalletError, attributes(codec))]
pub fn derive_pallet_error(input: TokenStream) -> TokenStream {
pallet_error::derive_pallet_error(input)
}
/// Internal macro used by `frame_support` to create tt-call-compliant macros
#[proc_macro]
pub fn __create_tt_macro(input: TokenStream) -> TokenStream {
tt_macro::create_tt_return_macro(input)
}
@@ -15,20 +15,48 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::pallet::Def;
use crate::{
pallet::{parse::error::VariantField, Def},
COUNTER,
};
use frame_support_procedural_tools::get_doc_literals;
use syn::spanned::Spanned;
///
/// * impl various trait on Error
pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream {
let error = if let Some(error) = &def.error { error } else { return Default::default() };
let count = COUNTER.with(|counter| counter.borrow_mut().inc());
let error_token_unique_id =
syn::Ident::new(&format!("__tt_error_token_{}", count), def.item.span());
let error_ident = &error.error;
let frame_support = &def.frame_support;
let frame_system = &def.frame_system;
let config_where_clause = &def.config.where_clause;
let error = if let Some(error) = &def.error {
error
} else {
return quote::quote! {
#[macro_export]
#[doc(hidden)]
macro_rules! #error_token_unique_id {
{
$caller:tt
frame_support = [{ $($frame_support:ident)::* }]
} => {
$($frame_support::)*tt_return! {
$caller
}
};
}
pub use #error_token_unique_id as tt_error_token;
}
};
let error_ident = &error.error;
let type_impl_gen = &def.type_impl_generics(error.attr_span);
let type_use_gen = &def.type_use_generics(error.attr_span);
let config_where_clause = &def.config.where_clause;
let phantom_variant: syn::Variant = syn::parse_quote!(
#[doc(hidden)]
@@ -39,13 +67,19 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream {
)
);
let as_u8_matches = error.variants.iter().enumerate().map(
|(i, (variant, _))| quote::quote_spanned!(error.attr_span => Self::#variant => #i as u8,),
);
let as_str_matches = error.variants.iter().map(|(variant, _)| {
let variant_str = format!("{}", variant);
quote::quote_spanned!(error.attr_span => Self::#variant => #variant_str,)
let as_str_matches = error.variants.iter().map(|(variant, field_ty, _)| {
let variant_str = variant.to_string();
match field_ty {
Some(VariantField { is_named: true }) => {
quote::quote_spanned!(error.attr_span => Self::#variant { .. } => #variant_str,)
},
Some(VariantField { is_named: false }) => {
quote::quote_spanned!(error.attr_span => Self::#variant(..) => #variant_str,)
},
None => {
quote::quote_spanned!(error.attr_span => Self::#variant => #variant_str,)
},
}
});
let error_item = {
@@ -62,9 +96,14 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream {
let capture_docs = if cfg!(feature = "no-metadata-docs") { "never" } else { "always" };
// derive TypeInfo for error metadata
error_item
.attrs
.push(syn::parse_quote!( #[derive(#frame_support::scale_info::TypeInfo)] ));
error_item.attrs.push(syn::parse_quote! {
#[derive(
#frame_support::codec::Encode,
#frame_support::codec::Decode,
#frame_support::scale_info::TypeInfo,
#frame_support::PalletError,
)]
});
error_item.attrs.push(syn::parse_quote!(
#[scale_info(skip_type_params(#type_use_gen), capture_docs = #capture_docs)]
));
@@ -90,14 +129,6 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream {
}
impl<#type_impl_gen> #error_ident<#type_use_gen> #config_where_clause {
#[doc(hidden)]
pub fn as_u8(&self) -> u8 {
match &self {
Self::__Ignore(_, _) => unreachable!("`__Ignore` can never be constructed"),
#( #as_u8_matches )*
}
}
#[doc(hidden)]
pub fn as_str(&self) -> &'static str {
match &self {
@@ -120,18 +151,37 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream {
#config_where_clause
{
fn from(err: #error_ident<#type_use_gen>) -> Self {
use #frame_support::codec::Encode;
let index = <
<T as #frame_system::Config>::PalletInfo
as #frame_support::traits::PalletInfo
>::index::<Pallet<#type_use_gen>>()
.expect("Every active module has an index in the runtime; qed") as u8;
let mut encoded = err.encode();
encoded.resize(#frame_support::MAX_MODULE_ERROR_ENCODED_SIZE, 0);
#frame_support::sp_runtime::DispatchError::Module(#frame_support::sp_runtime::ModuleError {
index,
error: err.as_u8(),
error: core::convert::TryInto::try_into(encoded).expect("encoded error is resized to be equal to the maximum encoded error size; qed"),
message: Some(err.as_str()),
})
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! #error_token_unique_id {
{
$caller:tt
frame_support = [{ $($frame_support:ident)::* }]
} => {
$($frame_support::)*tt_return! {
$caller
error = [{ #error_ident }]
}
};
}
pub use #error_token_unique_id as tt_error_token;
)
}
@@ -18,20 +18,26 @@
use super::helper;
use frame_support_procedural_tools::get_doc_literals;
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{spanned::Spanned, Fields};
/// List of additional token to be used for parsing.
mod keyword {
syn::custom_keyword!(Error);
}
/// Records information about the error enum variants.
pub struct VariantField {
/// Whether or not the field is named, i.e. whether it is a tuple variant or struct variant.
pub is_named: bool,
}
/// This checks error declaration as a enum declaration with only variants without fields nor
/// discriminant.
pub struct ErrorDef {
/// The index of error item in pallet module.
pub index: usize,
/// Variants ident and doc literals (ordered as declaration order)
pub variants: Vec<(syn::Ident, Vec<syn::Lit>)>,
/// Variants ident, optional field and doc literals (ordered as declaration order)
pub variants: Vec<(syn::Ident, Option<VariantField>, Vec<syn::Lit>)>,
/// A set of usage of instance, must be check for consistency with trait.
pub instances: Vec<helper::InstanceUsage>,
/// The keyword error used (contains span).
@@ -70,18 +76,19 @@ impl ErrorDef {
.variants
.iter()
.map(|variant| {
if !matches!(variant.fields, syn::Fields::Unit) {
let msg = "Invalid pallet::error, unexpected fields, must be `Unit`";
return Err(syn::Error::new(variant.fields.span(), msg))
}
let field_ty = match &variant.fields {
Fields::Unit => None,
Fields::Named(_) => Some(VariantField { is_named: true }),
Fields::Unnamed(_) => Some(VariantField { is_named: false }),
};
if variant.discriminant.is_some() {
let msg = "Invalid pallet::error, unexpected discriminant, discriminant \
let msg = "Invalid pallet::error, unexpected discriminant, discriminants \
are not supported";
let span = variant.discriminant.as_ref().unwrap().0.span();
return Err(syn::Error::new(span, msg))
}
Ok((variant.ident.clone(), get_doc_literals(&variant.attrs)))
Ok((variant.ident.clone(), field_ty, get_doc_literals(&variant.attrs)))
})
.collect::<Result<_, _>>()?;
@@ -0,0 +1,197 @@
// This file is part of Substrate.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// 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 frame_support_procedural_tools::generate_crate_access_2018;
use quote::ToTokens;
use std::str::FromStr;
// Derive `PalletError`
pub fn derive_pallet_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let syn::DeriveInput { ident: name, generics, data, .. } = match syn::parse(input) {
Ok(input) => input,
Err(e) => return e.to_compile_error().into(),
};
let frame_support = match generate_crate_access_2018("frame-support") {
Ok(c) => c,
Err(e) => return e.into_compile_error().into(),
};
let frame_support = &frame_support;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let max_encoded_size = match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => match fields {
syn::Fields::Named(syn::FieldsNamed { named: fields, .. }) |
syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: fields, .. }) => {
let maybe_field_tys = fields
.iter()
.map(|f| generate_field_types(f, &frame_support))
.collect::<syn::Result<Vec<_>>>();
let field_tys = match maybe_field_tys {
Ok(tys) => tys.into_iter().flatten(),
Err(e) => return e.into_compile_error().into(),
};
quote::quote! {
0_usize
#(
.saturating_add(<
#field_tys as #frame_support::traits::PalletError
>::MAX_ENCODED_SIZE)
)*
}
},
syn::Fields::Unit => quote::quote!(0),
},
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let field_tys = variants
.iter()
.map(|variant| generate_variant_field_types(variant, &frame_support))
.collect::<Result<Vec<Option<Vec<proc_macro2::TokenStream>>>, syn::Error>>();
let field_tys = match field_tys {
Ok(tys) => tys.into_iter().flatten().collect::<Vec<_>>(),
Err(e) => return e.to_compile_error().into(),
};
// We start with `1`, because the discriminant of an enum is stored as u8
if field_tys.is_empty() {
quote::quote!(1)
} else {
let variant_sizes = field_tys.into_iter().map(|variant_field_tys| {
quote::quote! {
1_usize
#(.saturating_add(<
#variant_field_tys as #frame_support::traits::PalletError
>::MAX_ENCODED_SIZE))*
}
});
quote::quote! {{
let mut size = 1_usize;
let mut tmp = 0_usize;
#(
tmp = #variant_sizes;
size = if tmp > size { tmp } else { size };
tmp = 0_usize;
)*
size
}}
}
},
syn::Data::Union(syn::DataUnion { union_token, .. }) => {
let msg = "Cannot derive `PalletError` for union; please implement it directly";
return syn::Error::new(union_token.span, msg).into_compile_error().into()
},
};
quote::quote!(
const _: () = {
impl #impl_generics #frame_support::traits::PalletError
for #name #ty_generics #where_clause
{
const MAX_ENCODED_SIZE: usize = #max_encoded_size;
}
};
)
.into()
}
fn generate_field_types(
field: &syn::Field,
scrate: &syn::Ident,
) -> syn::Result<Option<proc_macro2::TokenStream>> {
let attrs = &field.attrs;
for attr in attrs {
if attr.path.is_ident("codec") {
match attr.parse_meta()? {
syn::Meta::List(ref meta_list) if meta_list.nested.len() == 1 => {
match meta_list
.nested
.first()
.expect("Just checked that there is one item; qed")
{
syn::NestedMeta::Meta(syn::Meta::Path(path))
if path.get_ident().map_or(false, |i| i == "skip") =>
return Ok(None),
syn::NestedMeta::Meta(syn::Meta::Path(path))
if path.get_ident().map_or(false, |i| i == "compact") =>
{
let field_ty = &field.ty;
return Ok(Some(quote::quote!(#scrate::codec::Compact<#field_ty>)))
},
syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue {
path,
lit: syn::Lit::Str(lit_str),
..
})) if path.get_ident().map_or(false, |i| i == "encoded_as") => {
let ty = proc_macro2::TokenStream::from_str(&lit_str.value())?;
return Ok(Some(ty))
},
_ => (),
}
},
_ => (),
}
}
}
Ok(Some(field.ty.to_token_stream()))
}
fn generate_variant_field_types(
variant: &syn::Variant,
scrate: &syn::Ident,
) -> syn::Result<Option<Vec<proc_macro2::TokenStream>>> {
let attrs = &variant.attrs;
for attr in attrs {
if attr.path.is_ident("codec") {
match attr.parse_meta()? {
syn::Meta::List(ref meta_list) if meta_list.nested.len() == 1 => {
match meta_list
.nested
.first()
.expect("Just checked that there is one item; qed")
{
syn::NestedMeta::Meta(syn::Meta::Path(path))
if path.get_ident().map_or(false, |i| i == "skip") =>
return Ok(None),
_ => (),
}
},
_ => (),
}
}
}
match &variant.fields {
syn::Fields::Named(syn::FieldsNamed { named: fields, .. }) |
syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: fields, .. }) => {
let field_tys = fields
.iter()
.map(|field| generate_field_types(field, scrate))
.collect::<syn::Result<Vec<_>>>()?;
Ok(Some(field_tys.into_iter().flatten().collect()))
},
syn::Fields::Unit => Ok(None),
}
}
@@ -0,0 +1,110 @@
// This file is part of Substrate.
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// 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.
//! Implementation of the `create_tt_return_macro` macro
use crate::COUNTER;
use frame_support_procedural_tools::generate_crate_access_2018;
use proc_macro2::{Ident, TokenStream};
use quote::format_ident;
struct CreateTtReturnMacroDef {
name: Ident,
args: Vec<(Ident, TokenStream)>,
}
impl syn::parse::Parse for CreateTtReturnMacroDef {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let _ = input.parse::<syn::Token![,]>()?;
let mut args = Vec::new();
while !input.is_empty() {
let mut value;
let key: Ident = input.parse()?;
let _ = input.parse::<syn::Token![=]>()?;
let _: syn::token::Bracket = syn::bracketed!(value in input);
let _: syn::token::Brace = syn::braced!(value in value);
let value: TokenStream = value.parse()?;
args.push((key, value))
}
Ok(Self { name, args })
}
}
/// A proc macro that accepts a name and any number of key-value pairs, to be used to create a
/// declarative macro that follows tt-call conventions and simply calls [`tt_call::tt_return`],
/// accepting an optional `frame-support` argument and returning the key-value pairs that were
/// supplied to the proc macro.
///
/// # Example
/// ```ignore
/// __create_tt_macro! {
/// my_tt_macro,
/// foo = [{ bar }]
/// }
///
/// // Creates the following declarative macro:
///
/// macro_rules! my_tt_macro {
/// {
/// $caller:tt
/// $(frame_support = [{ $($frame_support:ident)::* }])?
/// } => {
/// frame_support::tt_return! {
/// $caller
/// foo = [{ bar }]
/// }
/// }
/// }
/// ```
pub fn create_tt_return_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let CreateTtReturnMacroDef { name, args } =
syn::parse_macro_input!(input as CreateTtReturnMacroDef);
let frame_support = match generate_crate_access_2018("frame-support") {
Ok(i) => i,
Err(e) => return e.into_compile_error().into(),
};
let (keys, values): (Vec<_>, Vec<_>) = args.into_iter().unzip();
let count = COUNTER.with(|counter| counter.borrow_mut().inc());
let unique_name = format_ident!("{}_{}", name, count);
let decl_macro = quote::quote! {
#[macro_export]
#[doc(hidden)]
macro_rules! #unique_name {
{
$caller:tt
$(frame_support = [{ $($frame_support:ident)::* }])?
} => {
#frame_support::tt_return! {
$caller
#(
#keys = [{ #values }]
)*
}
}
}
pub use #unique_name as #name;
};
decl_macro.into()
}