use crate::GenerateCode;
use ::core::iter;
use derive_more::From;
use ir::{
Callable as _,
HexLiteral,
IsDocAttribute,
};
use proc_macro2::{
Ident,
TokenStream as TokenStream2,
};
use quote::{
quote,
quote_spanned,
};
use syn::spanned::Spanned as _;
#[derive(From)]
pub struct Metadata<'a> {
contract: &'a ir::Contract,
}
impl_as_ref_for_generator!(Metadata);
impl GenerateCode for Metadata<'_> {
fn generate_code(&self) -> TokenStream2 {
let contract = self.generate_contract();
let layout = self.generate_layout();
quote! {
#[cfg(feature = "std")]
#[cfg(not(feature = "ink-as-dependency"))]
const _: () = {
#[no_mangle]
pub fn __ink_generate_metadata() -> ::ink::metadata::InkProject {
let layout = #layout;
::ink::metadata::layout::ValidateLayout::validate(&layout).unwrap_or_else(|error| {
::core::panic!("metadata ink! generation failed: {}", error)
});
::ink::metadata::InkProject::new(layout, #contract)
}
};
}
}
}
impl Metadata<'_> {
fn generate_layout(&self) -> TokenStream2 {
let storage_span = self.contract.module().storage().span();
let storage_ident = self.contract.module().storage().ident();
let key = quote! { <#storage_ident as ::ink::storage::traits::StorageKey>::KEY };
let layout_key = quote! {
<::ink::metadata::layout::LayoutKey
as ::core::convert::From<::ink::primitives::Key>>::from(#key)
};
quote_spanned!(storage_span=>
::ink::metadata::layout::Layout::Root(::ink::metadata::layout::RootLayout::new(
#layout_key,
<#storage_ident as ::ink::storage::traits::StorageLayout>::layout(
&#key,
),
))
)
}
fn generate_contract(&self) -> TokenStream2 {
let constructors = self.generate_constructors();
let messages = self.generate_messages();
let events = self.generate_events();
let docs = self
.contract
.module()
.attrs()
.iter()
.filter_map(|attr| attr.extract_docs());
let error_ty = syn::parse_quote! {
::ink::LangError
};
let error = Self::generate_type_spec(&error_ty);
quote! {
::ink::metadata::ContractSpec::new()
.constructors([
#( #constructors ),*
])
.messages([
#( #messages ),*
])
.events([
#( #events ),*
])
.docs([
#( #docs ),*
])
.lang_error(
#error
)
.done()
}
}
#[allow(clippy::redundant_closure)] fn generate_constructors(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.contract
.module()
.impls()
.flat_map(|item_impl| item_impl.iter_constructors())
.map(|constructor| self.generate_constructor(constructor))
}
fn generate_constructor(
&self,
constructor: ir::CallableWithSelector<ir::Constructor>,
) -> TokenStream2 {
let span = constructor.span();
let docs = constructor
.attrs()
.iter()
.filter_map(|attr| attr.extract_docs());
let selector_bytes = constructor.composed_selector().hex_lits();
let selector_id = constructor.composed_selector().into_be_u32();
let is_payable = constructor.is_payable();
let constructor = constructor.callable();
let ident = constructor.ident();
let args = constructor.inputs().map(Self::generate_dispatch_argument);
let storage_ident = self.contract.module().storage().ident();
let ret_ty = Self::generate_constructor_return_type(storage_ident, selector_id);
quote_spanned!(span=>
::ink::metadata::ConstructorSpec::from_label(::core::stringify!(#ident))
.selector([
#( #selector_bytes ),*
])
.args([
#( #args ),*
])
.payable(#is_payable)
.returns(#ret_ty)
.docs([
#( #docs ),*
])
.done()
)
}
fn generate_dispatch_argument(pat_type: &syn::PatType) -> TokenStream2 {
let ident = match &*pat_type.pat {
syn::Pat::Ident(ident) => &ident.ident,
_ => unreachable!("encountered ink! dispatch input with missing identifier"),
};
let type_spec = Self::generate_type_spec(&pat_type.ty);
quote! {
::ink::metadata::MessageParamSpec::new(::core::stringify!(#ident))
.of_type(#type_spec)
.done()
}
}
fn generate_type_spec(ty: &syn::Type) -> TokenStream2 {
fn without_display_name(ty: &syn::Type) -> TokenStream2 {
quote! { ::ink::metadata::TypeSpec::of_type::<#ty>() }
}
if let syn::Type::Path(type_path) = ty {
if type_path.qself.is_some() {
return without_display_name(ty)
}
let path = &type_path.path;
if path.segments.is_empty() {
return without_display_name(ty)
}
let segs = path
.segments
.iter()
.map(|seg| &seg.ident)
.collect::<Vec<_>>();
quote! {
::ink::metadata::TypeSpec::with_name_segs::<#ty, _>(
::core::iter::Iterator::map(
::core::iter::IntoIterator::into_iter([ #( ::core::stringify!(#segs) ),* ]),
::core::convert::AsRef::as_ref
)
)
}
} else {
without_display_name(ty)
}
}
fn generate_messages(&self) -> Vec<TokenStream2> {
let mut messages = Vec::new();
let inherent_messages = self.generate_inherent_messages();
let trait_messages = self.generate_trait_messages();
messages.extend(inherent_messages);
messages.extend(trait_messages);
messages
}
fn generate_inherent_messages(&self) -> Vec<TokenStream2> {
self.contract
.module()
.impls()
.filter(|item_impl| item_impl.trait_path().is_none())
.flat_map(|item_impl| item_impl.iter_messages())
.map(|message| {
let span = message.span();
let docs = message
.attrs()
.iter()
.filter_map(|attr| attr.extract_docs());
let selector_bytes = message.composed_selector().hex_lits();
let is_payable = message.is_payable();
let message = message.callable();
let mutates = message.receiver().is_ref_mut();
let ident = message.ident();
let args = message.inputs().map(Self::generate_dispatch_argument);
let ret_ty = Self::generate_return_type(Some(&message.wrapped_output()));
quote_spanned!(span =>
::ink::metadata::MessageSpec::from_label(::core::stringify!(#ident))
.selector([
#( #selector_bytes ),*
])
.args([
#( #args ),*
])
.returns(#ret_ty)
.mutates(#mutates)
.payable(#is_payable)
.docs([
#( #docs ),*
])
.done()
)
})
.collect()
}
fn generate_trait_messages(&self) -> Vec<TokenStream2> {
let storage_ident = self.contract.module().storage().ident();
self.contract
.module()
.impls()
.filter_map(|item_impl| {
item_impl
.trait_path()
.map(|trait_path| {
let trait_ident = item_impl.trait_ident().expect(
"must have an ink! trait identifier if it is an ink! trait implementation"
);
iter::repeat((trait_ident, trait_path)).zip(item_impl.iter_messages())
})
})
.flatten()
.map(|((trait_ident, trait_path), message)| {
let message_span = message.span();
let message_ident = message.ident();
let message_docs = message
.attrs()
.iter()
.filter_map(|attr| attr.extract_docs());
let message_args = message
.inputs()
.map(Self::generate_dispatch_argument);
let mutates = message.receiver().is_ref_mut();
let local_id = message.local_id().hex_padded_suffixed();
let is_payable = quote! {{
<<::ink::reflect::TraitDefinitionRegistry<<#storage_ident as ::ink::env::ContractEnv>::Env>
as #trait_path>::__ink_TraitInfo
as ::ink::reflect::TraitMessageInfo<#local_id>>::PAYABLE
}};
let selector = quote! {{
<<::ink::reflect::TraitDefinitionRegistry<<#storage_ident as ::ink::env::ContractEnv>::Env>
as #trait_path>::__ink_TraitInfo
as ::ink::reflect::TraitMessageInfo<#local_id>>::SELECTOR
}};
let ret_ty = Self::generate_return_type(Some(&message.wrapped_output()));
let label = [trait_ident.to_string(), message_ident.to_string()].join("::");
quote_spanned!(message_span=>
::ink::metadata::MessageSpec::from_label(#label)
.selector(#selector)
.args([
#( #message_args ),*
])
.returns(#ret_ty)
.mutates(#mutates)
.payable(#is_payable)
.docs([
#( #message_docs ),*
])
.done()
)
})
.collect()
}
fn generate_return_type(ret_ty: Option<&syn::Type>) -> TokenStream2 {
match ret_ty {
None => {
quote! {
::ink::metadata::ReturnTypeSpec::new(::core::option::Option::None)
}
}
Some(ty) => {
let type_spec = Self::generate_type_spec(ty);
quote! {
::ink::metadata::ReturnTypeSpec::new(#type_spec)
}
}
}
}
fn generate_constructor_return_type(
storage_ident: &Ident,
selector_id: u32,
) -> TokenStream2 {
let span = storage_ident.span();
let constructor_info = quote_spanned!(span =>
< #storage_ident as ::ink::reflect::DispatchableConstructorInfo<#selector_id>>
);
quote_spanned!(span=>
::ink::metadata::ReturnTypeSpec::new(if #constructor_info::IS_RESULT {
::core::option::Option::Some(::ink::metadata::TypeSpec::with_name_str::<
::ink::ConstructorResult<::core::result::Result<(), #constructor_info::Error>>,
>("ink_primitives::ConstructorResult"))
} else {
::core::option::Option::Some(::ink::metadata::TypeSpec::with_name_str::<
::ink::ConstructorResult<()>,
>("ink_primitives::ConstructorResult"))
})
)
}
fn generate_events(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.contract.module().events().map(|event| {
let span = event.span();
let ident = event.ident();
let docs = event.attrs().iter().filter_map(|attr| attr.extract_docs());
let args = Self::generate_event_args(event);
quote_spanned!(span =>
::ink::metadata::EventSpec::new(::core::stringify!(#ident))
.args([
#( #args ),*
])
.docs([
#( #docs ),*
])
.done()
)
})
}
fn generate_event_args(event: &ir::Event) -> impl Iterator<Item = TokenStream2> + '_ {
event.fields().map(|event_field| {
let span = event_field.span();
let ident = event_field.ident();
let is_topic = event_field.is_topic;
let docs = event_field
.attrs()
.into_iter()
.filter_map(|attr| attr.extract_docs());
let ty = Self::generate_type_spec(event_field.ty());
quote_spanned!(span =>
::ink::metadata::EventParamSpec::new(::core::stringify!(#ident))
.of_type(#ty)
.indexed(#is_topic)
.docs([
#( #docs ),*
])
.done()
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extract_doc_attributes(attrs: &[syn::Attribute]) -> Vec<String> {
attrs
.iter()
.filter_map(|attr| attr.extract_docs())
.collect()
}
#[test]
fn extract_doc_comments_works() {
assert_eq!(
extract_doc_attributes(&[syn::parse_quote!( #[doc = r"content"] )]),
vec!["content".to_string()],
);
assert_eq!(
extract_doc_attributes(&[syn::parse_quote!(
)]),
vec![" content".to_string()],
);
assert_eq!(
extract_doc_attributes(&[syn::parse_quote!(
)]),
vec![r"
* Multi-line comments
* may span many,
* many lines
"
.to_string()],
);
assert_eq!(
extract_doc_attributes(&[
syn::parse_quote!(
),
syn::parse_quote!(
),
syn::parse_quote!(
),
syn::parse_quote!(
),
]),
vec![
" multiple".to_string(),
" single".to_string(),
" line".to_string(),
" comments".to_string(),
],
);
assert_eq!(
extract_doc_attributes(&[
syn::parse_quote!( #[doc = r"a"] ),
syn::parse_quote!( #[non_doc] ),
syn::parse_quote!( #[doc = r"b"] ),
syn::parse_quote!( #[derive(NonDoc)] ),
syn::parse_quote!( #[doc = r"c"] ),
syn::parse_quote!( #[docker = false] ),
syn::parse_quote!( #[doc = r"d"] ),
syn::parse_quote!( #[doc(Nope)] ),
syn::parse_quote!( #[doc = r"e"] ),
]),
vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"e".to_string(),
],
)
}
}