From b4fff412021c7a8d7cbcc1a67295835bc1b9a9fd Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Sat, 15 Apr 2023 15:44:34 +0200 Subject: [PATCH] Add `#[ink(default)]` attribute for constructors and messages (#1724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * default attribute * fmt * fix tests * update changelog * Update crates/metadata/src/specs.rs * Update crates/metadata/src/specs.rs * Apply suggestions from code review * Add UIs to dictionary --------- Co-authored-by: Michael Müller Co-authored-by: Hernando Castano Co-authored-by: Hernando Castano --- .config/cargo_spellcheck.dic | 2 +- CHANGELOG.md | 1 + crates/ink/codegen/src/generator/metadata.rs | 4 + crates/ink/ir/src/ir/attrs.rs | 25 ++++ crates/ink/ir/src/ir/item_impl/callable.rs | 11 ++ crates/ink/ir/src/ir/item_impl/constructor.rs | 37 ++++++ crates/ink/ir/src/ir/item_impl/message.rs | 37 ++++++ .../ir/src/ir/trait_def/item/trait_item.rs | 1 + crates/metadata/src/specs.rs | 52 +++++++- crates/metadata/src/tests.rs | 117 +++++++++++++++++- 10 files changed, 279 insertions(+), 8 deletions(-) diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index 463c3ac3121..284c18234ac 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -15,7 +15,7 @@ KECCAK Polkadot RPC SHA -UI +UI/S URI Wasm Wasm32 diff --git a/CHANGELOG.md b/CHANGELOG.md index 69adf22025e..213c1c0eb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgraded `syn` to version `2` - [#1731](https://github.com/paritytech/ink/pull/1731) +- Added `default` attribute to constructors and messages - [#1703](https://github.com/paritytech/ink/pull/1724) ## Version 4.1.0 diff --git a/crates/ink/codegen/src/generator/metadata.rs b/crates/ink/codegen/src/generator/metadata.rs index 76fa16c3ce2..0dac7172326 100644 --- a/crates/ink/codegen/src/generator/metadata.rs +++ b/crates/ink/codegen/src/generator/metadata.rs @@ -147,6 +147,7 @@ impl Metadata<'_> { let selector_bytes = constructor.composed_selector().hex_lits(); let selector_id = constructor.composed_selector().into_be_u32(); let is_payable = constructor.is_payable(); + let is_default = constructor.is_default(); let constructor = constructor.callable(); let ident = constructor.ident(); let args = constructor.inputs().map(Self::generate_dispatch_argument); @@ -163,6 +164,7 @@ impl Metadata<'_> { #( #args ),* ]) .payable(#is_payable) + .default(#is_default) .returns(#ret_ty) .docs([ #( #docs ),* @@ -242,6 +244,7 @@ impl Metadata<'_> { .filter_map(|attr| attr.extract_docs()); let selector_bytes = message.composed_selector().hex_lits(); let is_payable = message.is_payable(); + let is_default = message.is_default(); let message = message.callable(); let mutates = message.receiver().is_ref_mut(); let ident = message.ident(); @@ -260,6 +263,7 @@ impl Metadata<'_> { .returns(#ret_ty) .mutates(#mutates) .payable(#is_payable) + .default(#is_default) .docs([ #( #docs ),* ]) diff --git a/crates/ink/ir/src/ir/attrs.rs b/crates/ink/ir/src/ir/attrs.rs index 6aab9d28215..7f821e66ee5 100644 --- a/crates/ink/ir/src/ir/attrs.rs +++ b/crates/ink/ir/src/ir/attrs.rs @@ -287,6 +287,12 @@ impl InkAttribute { .any(|arg| matches!(arg.kind(), AttributeArg::Payable)) } + /// Returns `true` if the ink! attribute contains the `default` argument. + pub fn is_default(&self) -> bool { + self.args() + .any(|arg| matches!(arg.kind(), AttributeArg::Default)) + } + /// Returns `true` if the ink! attribute contains the wildcard selector. pub fn has_wildcard_selector(&self) -> bool { self.args().any(|arg| { @@ -351,6 +357,8 @@ pub enum AttributeArgKind { Constructor, /// `#[ink(payable)]` Payable, + /// `#[ink(default)]` + Default, /// `#[ink(selector = _)]` /// `#[ink(selector = 0xDEADBEEF)]` Selector, @@ -403,6 +411,9 @@ pub enum AttributeArg { /// Applied on ink! constructors or messages in order to specify that they /// can receive funds from callers. Payable, + /// Applied on ink! constructors or messages in order to indicate + /// they are default. + Default, /// Can be either one of: /// /// - `#[ink(selector = 0xDEADBEEF)]` Applied on ink! constructors or messages to @@ -463,6 +474,7 @@ impl core::fmt::Display for AttributeArgKind { } Self::Implementation => write!(f, "impl"), Self::HandleStatus => write!(f, "handle_status"), + Self::Default => write!(f, "default"), } } } @@ -483,6 +495,7 @@ impl AttributeArg { Self::Namespace(_) => AttributeArgKind::Namespace, Self::Implementation => AttributeArgKind::Implementation, Self::HandleStatus(_) => AttributeArgKind::HandleStatus, + Self::Default => AttributeArgKind::Default, } } } @@ -506,6 +519,7 @@ impl core::fmt::Display for AttributeArg { } Self::Implementation => write!(f, "impl"), Self::HandleStatus(value) => write!(f, "handle_status = {value:?}"), + Self::Default => write!(f, "default"), } } } @@ -959,6 +973,7 @@ impl Parse for AttributeFrag { "anonymous" => Ok(AttributeArg::Anonymous), "topic" => Ok(AttributeArg::Topic), "payable" => Ok(AttributeArg::Payable), + "default" => Ok(AttributeArg::Default), "impl" => Ok(AttributeArg::Implementation), _ => match ident.to_string().as_str() { "extension" => Err(format_err_spanned!( @@ -1217,6 +1232,16 @@ mod tests { ); } + #[test] + fn default_works() { + assert_attribute_try_from( + syn::parse_quote! { + #[ink(default)] + }, + Ok(test::Attribute::Ink(vec![AttributeArg::Default])), + ) + } + #[test] fn namespace_works() { assert_attribute_try_from( diff --git a/crates/ink/ir/src/ir/item_impl/callable.rs b/crates/ink/ir/src/ir/item_impl/callable.rs index e9ce9129268..1005cae58a9 100644 --- a/crates/ink/ir/src/ir/item_impl/callable.rs +++ b/crates/ink/ir/src/ir/item_impl/callable.rs @@ -116,6 +116,10 @@ where ::is_payable(self.callable) } + fn is_default(&self) -> bool { + ::is_default(self.callable) + } + fn has_wildcard_selector(&self) -> bool { ::has_wildcard_selector(self.callable) } @@ -166,6 +170,13 @@ pub trait Callable { /// Flagging as payable is done using the `#[ink(payable)]` attribute. fn is_payable(&self) -> bool; + /// Returns `true` if the ink! callable is flagged as default. + /// + /// # Note + /// + /// Flagging as default is done using the `#[ink(default)]` attribute. + fn is_default(&self) -> bool; + /// Returns `true` if the ink! callable is flagged as a wildcard selector. fn has_wildcard_selector(&self) -> bool; diff --git a/crates/ink/ir/src/ir/item_impl/constructor.rs b/crates/ink/ir/src/ir/item_impl/constructor.rs index bce50923a6c..30d8a255fa9 100644 --- a/crates/ink/ir/src/ir/item_impl/constructor.rs +++ b/crates/ink/ir/src/ir/item_impl/constructor.rs @@ -70,6 +70,8 @@ pub struct Constructor { pub(super) item: syn::ImplItemFn, /// If the ink! constructor can receive funds. is_payable: bool, + /// If the ink! constructor is default. + is_default: bool, /// An optional user provided selector. /// /// # Note @@ -139,6 +141,7 @@ impl Constructor { match arg.kind() { ir::AttributeArg::Constructor | ir::AttributeArg::Payable + | ir::AttributeArg::Default | ir::AttributeArg::Selector(_) => Ok(()), _ => Err(None), } @@ -156,10 +159,12 @@ impl TryFrom for Constructor { Self::ensure_no_self_receiver(&method_item)?; let (ink_attrs, other_attrs) = Self::sanitize_attributes(&method_item)?; let is_payable = ink_attrs.is_payable(); + let is_default = ink_attrs.is_default(); let selector = ink_attrs.selector(); Ok(Constructor { selector, is_payable, + is_default, item: syn::ImplItemFn { attrs: other_attrs, ..method_item @@ -195,6 +200,10 @@ impl Callable for Constructor { self.is_payable } + fn is_default(&self) -> bool { + self.is_default + } + fn visibility(&self) -> Visibility { match &self.item.vis { syn::Visibility::Public(vis_public) => Visibility::Public(*vis_public), @@ -336,6 +345,34 @@ mod tests { } } + #[test] + fn is_default_works() { + let test_inputs: Vec<(bool, syn::ImplItemFn)> = vec![ + // Not default. + ( + false, + syn::parse_quote! { + #[ink(constructor)] + fn my_constructor() -> Self {} + }, + ), + // Default constructor. + ( + true, + syn::parse_quote! { + #[ink(constructor, default)] + pub fn my_constructor() -> Self {} + }, + ), + ]; + for (expect_default, item_method) in test_inputs { + let is_default = >::try_from(item_method) + .unwrap() + .is_default(); + assert_eq!(is_default, expect_default); + } + } + #[test] fn visibility_works() { let test_inputs: Vec<(bool, syn::ImplItemFn)> = vec![ diff --git a/crates/ink/ir/src/ir/item_impl/message.rs b/crates/ink/ir/src/ir/item_impl/message.rs index 3ee6c3efc7a..0a594adfff9 100644 --- a/crates/ink/ir/src/ir/item_impl/message.rs +++ b/crates/ink/ir/src/ir/item_impl/message.rs @@ -100,6 +100,8 @@ pub struct Message { pub(super) item: syn::ImplItemFn, /// If the ink! message can receive funds. is_payable: bool, + /// If the ink! message is default. + is_default: bool, /// An optional user provided selector. /// /// # Note @@ -185,6 +187,7 @@ impl Message { match arg.kind() { ir::AttributeArg::Message | ir::AttributeArg::Payable + | ir::AttributeArg::Default | ir::AttributeArg::Selector(_) => Ok(()), _ => Err(None), } @@ -202,9 +205,11 @@ impl TryFrom for Message { Self::ensure_not_return_self(&method_item)?; let (ink_attrs, other_attrs) = Self::sanitize_attributes(&method_item)?; let is_payable = ink_attrs.is_payable(); + let is_default = ink_attrs.is_default(); let selector = ink_attrs.selector(); Ok(Self { is_payable, + is_default, selector, item: syn::ImplItemFn { attrs: other_attrs, @@ -241,6 +246,10 @@ impl Callable for Message { self.is_payable } + fn is_default(&self) -> bool { + self.is_default + } + fn visibility(&self) -> Visibility { match &self.item.vis { syn::Visibility::Public(vis_public) => Visibility::Public(*vis_public), @@ -466,6 +475,34 @@ mod tests { } } + #[test] + fn is_default_works() { + let test_inputs: Vec<(bool, syn::ImplItemFn)> = vec![ + // Not default. + ( + false, + syn::parse_quote! { + #[ink(message)] + fn my_message(&self) {} + }, + ), + // Default message. + ( + true, + syn::parse_quote! { + #[ink(message, payable, default)] + pub fn my_message(&self) {} + }, + ), + ]; + for (expect_default, item_method) in test_inputs { + let is_default = >::try_from(item_method) + .unwrap() + .is_default(); + assert_eq!(is_default, expect_default); + } + } + #[test] fn receiver_works() { let test_inputs: Vec<(Receiver, syn::ImplItemFn)> = vec![ diff --git a/crates/ink/ir/src/ir/trait_def/item/trait_item.rs b/crates/ink/ir/src/ir/trait_def/item/trait_item.rs index 3740b8d4fc6..9e215717ba7 100644 --- a/crates/ink/ir/src/ir/trait_def/item/trait_item.rs +++ b/crates/ink/ir/src/ir/trait_def/item/trait_item.rs @@ -92,6 +92,7 @@ impl<'a> InkTraitMessage<'a> { Err(Some(format_err!(arg.span(), "wildcard selectors are only supported for inherent ink! messages or constructors, not for traits."))), ir::AttributeArg::Message | ir::AttributeArg::Payable + | ir::AttributeArg::Default | ir::AttributeArg::Selector(_) => Ok(()), _ => Err(None), } diff --git a/crates/metadata/src/specs.rs b/crates/metadata/src/specs.rs index 73ffa4be9b9..e49f4655c7f 100644 --- a/crates/metadata/src/specs.rs +++ b/crates/metadata/src/specs.rs @@ -254,6 +254,14 @@ where !self.spec.messages.is_empty(), "must have at least one message" ); + assert!( + self.spec.constructors.iter().filter(|c| c.default).count() < 2, + "only one default constructor is allowed" + ); + assert!( + self.spec.messages.iter().filter(|m| m.default).count() < 2, + "only one default message is allowed" + ); self.spec } } @@ -303,6 +311,8 @@ pub struct ConstructorSpec { pub return_type: ReturnTypeSpec, /// The deployment handler documentation. pub docs: Vec, + /// If the constructor is the default for off-chain consumers (e.g UIs). + default: bool, } impl IntoPortable for ConstructorSpec { @@ -320,6 +330,7 @@ impl IntoPortable for ConstructorSpec { .collect::>(), return_type: self.return_type.into_portable(registry), docs: self.docs.into_iter().map(|s| s.into()).collect(), + default: self.default, } } } @@ -360,6 +371,10 @@ where pub fn docs(&self) -> &[F::String] { &self.docs } + + pub fn default(&self) -> &bool { + &self.default + } } /// A builder for constructors. @@ -397,6 +412,7 @@ where args: Vec::new(), return_type: ReturnTypeSpec::new(None), docs: Vec::new(), + default: false, }, marker: PhantomData, } @@ -445,7 +461,7 @@ impl ConstructorSpecBuilder> where F: Form, { - /// Sets the return type of the message. + /// Sets the return type of the constructor. pub fn returns( self, return_type: ReturnTypeSpec, @@ -464,7 +480,7 @@ impl ConstructorSpecBuilder where F: Form, { - /// Sets the input arguments of the message specification. + /// Sets the input arguments of the constructor specification. pub fn args(self, args: A) -> Self where A: IntoIterator>, @@ -475,7 +491,7 @@ where this } - /// Sets the documentation of the message specification. + /// Sets the documentation of the constructor specification. pub fn docs<'a, D>(self, docs: D) -> Self where D: IntoIterator, @@ -489,6 +505,17 @@ where .collect::>(); this } + + /// Sets the default of the constructor specification. + pub fn default(self, default: bool) -> Self { + ConstructorSpecBuilder { + spec: ConstructorSpec { + default, + ..self.spec + }, + marker: PhantomData, + } + } } impl ConstructorSpecBuilder @@ -526,6 +553,8 @@ pub struct MessageSpec { return_type: ReturnTypeSpec, /// The message documentation. docs: Vec, + /// If the message is the default for off-chain consumers (e.g UIs). + default: bool, } /// Type state for builders to tell that some mandatory state has not yet been set @@ -583,6 +612,7 @@ where args: Vec::new(), return_type: ReturnTypeSpec::new(None), docs: Vec::new(), + default: false, }, marker: PhantomData, } @@ -630,6 +660,10 @@ where pub fn docs(&self) -> &[F::String] { &self.docs } + + pub fn default(&self) -> &bool { + &self.default + } } /// A builder for messages. @@ -751,6 +785,17 @@ where this.spec.docs = docs.into_iter().collect::>(); this } + + /// Sets the default of the message specification. + pub fn default(self, default: bool) -> Self { + MessageSpecBuilder { + spec: MessageSpec { + default, + ..self.spec + }, + marker: PhantomData, + } + } } impl @@ -779,6 +824,7 @@ impl IntoPortable for MessageSpec { selector: self.selector, mutates: self.mutates, payable: self.payable, + default: self.default, args: self .args .into_iter() diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index 50c3f846ad0..9beee133c0b 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -47,12 +47,110 @@ fn spec_constructor_selector_must_serialize_to_hex() { "selector": "0x075bcd15", "returnType": null, "args": [], - "docs": [] + "docs": [], + "default": false, }) ); assert_eq!(deserialized.selector, portable_spec.selector); } +#[test] +#[should_panic(expected = "only one default message is allowed")] +fn spec_contract_only_one_default_message_allowed() { + ContractSpec::new() + .constructors(vec![ConstructorSpec::from_label("new") + .selector([94u8, 189u8, 136u8, 214u8]) + .payable(true) + .args(vec![MessageParamSpec::new("init_value") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .docs(Vec::new()) + .done()]) + .messages(vec![ + MessageSpec::from_label("inc") + .selector([231u8, 208u8, 89u8, 15u8]) + .mutates(true) + .payable(true) + .args(vec![MessageParamSpec::new("by") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .default(true) + .done(), + MessageSpec::from_label("get") + .selector([37u8, 68u8, 74u8, 254u8]) + .mutates(false) + .payable(false) + .args(Vec::new()) + .returns(ReturnTypeSpec::new(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + ))) + .default(true) + .done(), + ]) + .events(Vec::new()) + .lang_error(TypeSpec::with_name_segs::( + ::core::iter::Iterator::map( + ::core::iter::IntoIterator::into_iter(["ink", "LangError"]), + ::core::convert::AsRef::as_ref, + ), + )) + .done(); +} + +#[test] +#[should_panic(expected = "only one default constructor is allowed")] +fn spec_contract_only_one_default_constructor_allowed() { + ContractSpec::new() + .constructors(vec![ + ConstructorSpec::from_label("new") + .selector([94u8, 189u8, 136u8, 214u8]) + .payable(true) + .args(vec![MessageParamSpec::new("init_value") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .docs(Vec::new()) + .default(true) + .done(), + ConstructorSpec::from_label("default") + .selector([2u8, 34u8, 255u8, 24u8]) + .payable(Default::default()) + .args(Vec::new()) + .returns(ReturnTypeSpec::new(None)) + .docs(Vec::new()) + .default(true) + .done(), + ]) + .messages(vec![MessageSpec::from_label("inc") + .selector([231u8, 208u8, 89u8, 15u8]) + .mutates(true) + .payable(true) + .args(vec![MessageParamSpec::new("by") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .default(true) + .done()]) + .events(Vec::new()) + .lang_error(TypeSpec::with_name_segs::( + ::core::iter::Iterator::map( + ::core::iter::IntoIterator::into_iter(["ink", "LangError"]), + ::core::convert::AsRef::as_ref, + ), + )) + .done(); +} + #[test] fn spec_contract_json() { #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -86,6 +184,7 @@ fn spec_contract_json() { .args(Vec::new()) .returns(ReturnTypeSpec::new(None)) .docs(Vec::new()) + .default(true) .done(), ConstructorSpec::from_label("result_new") .selector([6u8, 3u8, 55u8, 123u8]) @@ -110,6 +209,7 @@ fn spec_contract_json() { )) .done()]) .returns(ReturnTypeSpec::new(None)) + .default(true) .done(), MessageSpec::from_label("get") .selector([37u8, 68u8, 74u8, 254u8]) @@ -194,6 +294,7 @@ fn spec_contract_json() { } ], "docs": [], + "default": false, "label": "new", "payable": true, "returnType": null, @@ -202,6 +303,7 @@ fn spec_contract_json() { { "args": [], "docs": [], + "default": true, "label": "default", "payable": false, "returnType": null, @@ -210,6 +312,7 @@ fn spec_contract_json() { { "args": [], "docs": [], + "default": false, "label": "result_new", "payable": false, "returnType": { @@ -284,6 +387,7 @@ fn spec_contract_json() { } } ], + "default": true, "docs": [], "mutates": true, "payable": true, @@ -293,6 +397,7 @@ fn spec_contract_json() { }, { "args": [], + "default": false, "docs": [], "mutates": false, "payable": false, @@ -338,7 +443,8 @@ fn trim_docs() { "returnType": null, "selector": "0x075bcd15", "args": [], - "docs": ["foobar"] + "docs": ["foobar"], + "default": false }) ); assert_eq!(deserialized.docs, compact_spec.docs); @@ -386,7 +492,8 @@ fn trim_docs_with_code() { " \"Hello, World\"", "}", "```" - ] + ], + "default": false }) ); assert_eq!(deserialized.docs, compact_spec.docs); @@ -517,7 +624,8 @@ fn construct_runtime_contract_spec() { "docs": [ "foo", "bar" - ] + ], + "default": false } ); assert_eq!(constructor_spec, expected_constructor_spec); @@ -546,6 +654,7 @@ fn construct_runtime_contract_spec() { "FooType" ] }, + "default": false, "docs": [ "foo", "bar"