ink_codegen/generator/
metadata.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::GenerateCode;
16use ::core::iter;
17use derive_more::From;
18use ir::{
19    Callable as _,
20    HexLiteral,
21    IsDocAttribute,
22};
23use proc_macro2::{
24    Ident,
25    TokenStream as TokenStream2,
26};
27use quote::{
28    quote,
29    quote_spanned,
30};
31use syn::{
32    parse_quote,
33    spanned::Spanned as _,
34};
35
36/// Generates code to generate the metadata of the contract.
37#[derive(From)]
38pub struct Metadata<'a> {
39    /// The contract to generate code for.
40    contract: &'a ir::Contract,
41}
42impl_as_ref_for_generator!(Metadata);
43
44impl GenerateCode for Metadata<'_> {
45    fn generate_code(&self) -> TokenStream2 {
46        let contract = self.generate_contract();
47        let layout = self.generate_layout();
48
49        quote! {
50            #[cfg(feature = "std")]
51            #[cfg(not(feature = "ink-as-dependency"))]
52            // TODO: (@davidsemakula) Re-enable `cfg` gate when cargo contract is updated to use
53            // not use `scale_info::TypeInfo` for Solidity compatible metadata generation
54            //#[cfg(not(ink_abi = "sol"))]
55            const _: () = {
56                #[no_mangle]
57                pub fn __ink_generate_metadata() -> ::ink::metadata::InkProject  {
58                    let layout = #layout;
59                    ::ink::metadata::layout::ValidateLayout::validate(&layout).unwrap_or_else(|error| {
60                        ::core::panic!("metadata ink! generation failed: {}", error)
61                    });
62                    ::ink::metadata::InkProject::new(layout, #contract)
63                }
64            };
65        }
66    }
67}
68
69impl Metadata<'_> {
70    fn generate_layout(&self) -> TokenStream2 {
71        let storage_span = self.contract.module().storage().span();
72        let storage_ident = self.contract.module().storage().ident();
73        let key = quote! { <#storage_ident as ::ink::storage::traits::StorageKey>::KEY };
74
75        let layout_key = quote! {
76            <::ink::metadata::layout::LayoutKey
77                as ::core::convert::From<::ink::primitives::Key>>::from(#key)
78        };
79        quote_spanned!(storage_span=>
80            // Wrap the layout of the contract into the `RootLayout`, because
81            // contract storage key is reserved for all packed fields
82            ::ink::metadata::layout::Layout::Root(::ink::metadata::layout::RootLayout::new(
83                #layout_key,
84                <#storage_ident as ::ink::storage::traits::StorageLayout>::layout(
85                    &#key,
86                ),
87                ::ink::scale_info::meta_type::<#storage_ident>(),
88            ))
89        )
90    }
91
92    fn generate_contract(&self) -> TokenStream2 {
93        let constructors = self.generate_constructors();
94        let messages = self.generate_messages();
95        let docs = self
96            .contract
97            .module()
98            .attrs()
99            .iter()
100            .filter_map(|attr| attr.extract_docs());
101        let error_ty = syn::parse_quote! {
102            ::ink::LangError
103        };
104        let error = generate_type_spec(&error_ty);
105        let environment = self.generate_environment();
106        quote! {
107            ::ink::metadata::ContractSpec::new()
108                .constructors([
109                    #( #constructors ),*
110                ])
111                .messages([
112                    #( #messages ),*
113                ])
114                .events(
115                    ::ink::collect_events()
116                )
117                .docs([
118                    #( #docs ),*
119                ])
120                .lang_error(
121                     #error
122                )
123                .environment(
124                    #environment
125                )
126                .done()
127        }
128    }
129
130    /// Generates ink! metadata for all ink! smart contract constructors.
131    #[allow(clippy::redundant_closure)] // We are getting arcane lifetime errors otherwise.
132    fn generate_constructors(&self) -> impl Iterator<Item = TokenStream2> + '_ {
133        self.contract
134            .module()
135            .impls()
136            .flat_map(|item_impl| item_impl.iter_constructors())
137            .map(|constructor| self.generate_constructor(constructor))
138    }
139
140    /// Generates ink! metadata for a single ink! constructor.
141    fn generate_constructor(
142        &self,
143        constructor: ir::CallableWithSelector<ir::Constructor>,
144    ) -> TokenStream2 {
145        let span = constructor.span();
146        let docs = constructor
147            .attrs()
148            .iter()
149            .filter_map(|attr| attr.extract_docs());
150        let selector_bytes = constructor.composed_selector().hex_lits();
151        let selector_id = constructor.composed_selector().into_be_u32();
152        let is_payable = constructor.is_payable();
153        let is_default = constructor.is_default();
154        let constructor = constructor.callable();
155        let ident = constructor.ident();
156        let args = constructor.inputs().map(Self::generate_dispatch_argument);
157        let storage_ident = self.contract.module().storage().ident();
158        let ret_ty = Self::generate_constructor_return_type(storage_ident, selector_id);
159        let cfg_attrs = constructor.get_cfg_attrs(span);
160        quote_spanned!(span=>
161            #( #cfg_attrs )*
162            ::ink::metadata::ConstructorSpec::from_label(::core::stringify!(#ident))
163                .selector([
164                    #( #selector_bytes ),*
165                ])
166                .args([
167                    #( #args ),*
168                ])
169                .payable(#is_payable)
170                .default(#is_default)
171                .returns(#ret_ty)
172                .docs([
173                    #( #docs ),*
174                ])
175                .done()
176        )
177    }
178
179    /// Generates the ink! metadata for the given parameter and parameter type.
180    fn generate_dispatch_argument(pat_type: &syn::PatType) -> TokenStream2 {
181        let ident = match &*pat_type.pat {
182            syn::Pat::Ident(ident) => &ident.ident,
183            _ => unreachable!("encountered ink! dispatch input with missing identifier"),
184        };
185        let type_spec = generate_type_spec(&pat_type.ty);
186        quote! {
187            ::ink::metadata::MessageParamSpec::new(::core::stringify!(#ident))
188                .of_type(#type_spec)
189                .done()
190        }
191    }
192    /// Generates the ink! metadata for all ink! smart contract messages.
193    fn generate_messages(&self) -> Vec<TokenStream2> {
194        let mut messages = Vec::new();
195        let inherent_messages = self.generate_inherent_messages();
196        let trait_messages = self.generate_trait_messages();
197        messages.extend(inherent_messages);
198        messages.extend(trait_messages);
199        messages
200    }
201
202    /// Generates the ink! metadata for all inherent ink! smart contract messages.
203    fn generate_inherent_messages(&self) -> Vec<TokenStream2> {
204        self.contract
205            .module()
206            .impls()
207            .filter(|item_impl| item_impl.trait_path().is_none())
208            .flat_map(|item_impl| item_impl.iter_messages())
209            .map(|message| {
210                let span = message.span();
211                let docs = message
212                    .attrs()
213                    .iter()
214                    .filter_map(|attr| attr.extract_docs());
215                let selector_bytes = message.composed_selector().hex_lits();
216                let is_payable = message.is_payable();
217                let is_default = message.is_default();
218                let message = message.callable();
219                let mutates = message.receiver().is_ref_mut();
220                let ident = message.ident();
221                let args = message.inputs().map(Self::generate_dispatch_argument);
222                let cfg_attrs = message.get_cfg_attrs(span);
223                let ret_ty =
224                    Self::generate_message_return_type(&message.wrapped_output());
225                quote_spanned!(span =>
226                    #( #cfg_attrs )*
227                    ::ink::metadata::MessageSpec::from_label(::core::stringify!(#ident))
228                        .selector([
229                            #( #selector_bytes ),*
230                        ])
231                        .args([
232                            #( #args ),*
233                        ])
234                        .returns(#ret_ty)
235                        .mutates(#mutates)
236                        .payable(#is_payable)
237                        .default(#is_default)
238                        .docs([
239                            #( #docs ),*
240                        ])
241                        .done()
242                )
243            })
244            .collect()
245    }
246
247    /// Generates the ink! metadata for all inherent ink! smart contract messages.
248    fn generate_trait_messages(&self) -> Vec<TokenStream2> {
249        let storage_ident = self.contract.module().storage().ident();
250        self.contract
251            .module()
252            .impls()
253            .filter_map(|item_impl| {
254                item_impl
255                    .trait_path()
256                    .map(|trait_path| {
257                        let trait_ident = item_impl.trait_ident().expect(
258                            "must have an ink! trait identifier if it is an ink! trait implementation"
259                        );
260                        iter::repeat((trait_ident, trait_path)).zip(item_impl.iter_messages())
261                    })
262            })
263            .flatten()
264            .map(|((trait_ident, trait_path), message)| {
265                let message_span = message.span();
266                let message_ident = message.ident();
267                let message_docs = message
268                    .attrs()
269                    .iter()
270                    .filter_map(|attr| attr.extract_docs());
271                let message_args = message
272                    .inputs()
273                    .map(Self::generate_dispatch_argument);
274                let cfg_attrs = message.get_cfg_attrs(message_span);
275                let mutates = message.receiver().is_ref_mut();
276                let local_id = message.local_id().hex_padded_suffixed();
277                let is_payable = quote! {{
278                    <<::ink::reflect::TraitDefinitionRegistry<<#storage_ident as ::ink::env::ContractEnv>::Env>
279                        as #trait_path>::__ink_TraitInfo
280                        as ::ink::reflect::TraitMessageInfo<#local_id>>::PAYABLE
281                }};
282                let selector = quote! {{
283                    <<::ink::reflect::TraitDefinitionRegistry<<#storage_ident as ::ink::env::ContractEnv>::Env>
284                        as #trait_path>::__ink_TraitInfo
285                        as ::ink::reflect::TraitMessageInfo<#local_id>>::SELECTOR
286                }};
287                let ret_ty = Self::generate_message_return_type(&message.wrapped_output());
288                let label = [trait_ident.to_string(), message_ident.to_string()].join("::");
289                quote_spanned!(message_span=>
290                    #( #cfg_attrs )*
291                    ::ink::metadata::MessageSpec::from_label(#label)
292                        .selector(#selector)
293                        .args([
294                            #( #message_args ),*
295                        ])
296                        .returns(#ret_ty)
297                        .mutates(#mutates)
298                        .payable(#is_payable)
299                        .docs([
300                            #( #message_docs ),*
301                        ])
302                        .done()
303                )
304            })
305            .collect()
306    }
307
308    /// Generates ink! metadata for the given return type.
309    fn generate_message_return_type(ret_ty: &syn::Type) -> TokenStream2 {
310        let type_spec = generate_type_spec(ret_ty);
311        quote! {
312            ::ink::metadata::ReturnTypeSpec::new(#type_spec)
313        }
314    }
315
316    /// Generates ink! metadata for the storage with given selector and ident.
317    fn generate_constructor_return_type(
318        storage_ident: &Ident,
319        selector_id: u32,
320    ) -> TokenStream2 {
321        let span = storage_ident.span();
322        let constructor_info = quote_spanned!(span =>
323            < #storage_ident as ::ink::reflect::DispatchableConstructorInfo<#selector_id>>
324        );
325
326        quote_spanned!(span=>
327            ::ink::metadata::ReturnTypeSpec::new(if #constructor_info::IS_RESULT {
328                ::ink::metadata::TypeSpec::with_name_str::<
329                    ::ink::ConstructorResult<::core::result::Result<(), #constructor_info::Error>>,
330                >("ink_primitives::ConstructorResult")
331            } else {
332                ::ink::metadata::TypeSpec::with_name_str::<
333                    ::ink::ConstructorResult<()>,
334                >("ink_primitives::ConstructorResult")
335            })
336        )
337    }
338
339    fn generate_environment(&self) -> TokenStream2 {
340        let span = self.contract.module().span();
341
342        let account_id: syn::Type = parse_quote!(AccountId);
343        let balance: syn::Type = parse_quote!(Balance);
344        let hash: syn::Type = parse_quote!(Hash);
345        let timestamp: syn::Type = parse_quote!(Timestamp);
346        let block_number: syn::Type = parse_quote!(BlockNumber);
347        let chain_extension: syn::Type = parse_quote!(ChainExtension);
348
349        let account_id = generate_type_spec(&account_id);
350        let balance = generate_type_spec(&balance);
351        let hash = generate_type_spec(&hash);
352        let timestamp = generate_type_spec(&timestamp);
353        let block_number = generate_type_spec(&block_number);
354        let chain_extension = generate_type_spec(&chain_extension);
355        let buffer_size_const = quote!(::ink::env::BUFFER_SIZE);
356        quote_spanned!(span=>
357            ::ink::metadata::EnvironmentSpec::new()
358                .account_id(#account_id)
359                .balance(#balance)
360                .hash(#hash)
361                .timestamp(#timestamp)
362                .block_number(#block_number)
363                .chain_extension(#chain_extension)
364                .max_event_topics(MAX_EVENT_TOPICS)
365                .static_buffer_size(#buffer_size_const)
366                .done()
367        )
368    }
369}
370
371/// Generates the ink! metadata for the given type.
372pub fn generate_type_spec(ty: &syn::Type) -> TokenStream2 {
373    fn without_display_name(ty: &syn::Type) -> TokenStream2 {
374        quote! { ::ink::metadata::TypeSpec::of_type::<#ty>() }
375    }
376
377    if let syn::Type::Path(type_path) = ty {
378        if type_path.qself.is_some() {
379            return without_display_name(ty)
380        }
381        let path = &type_path.path;
382        if path.segments.is_empty() {
383            return without_display_name(ty)
384        }
385        let segs = path
386            .segments
387            .iter()
388            .map(|seg| &seg.ident)
389            .collect::<Vec<_>>();
390        quote! {
391            ::ink::metadata::TypeSpec::with_name_segs::<#ty, _>(
392                ::core::iter::Iterator::map(
393                    ::core::iter::IntoIterator::into_iter([ #( ::core::stringify!(#segs) ),* ]),
394                    ::core::convert::AsRef::as_ref
395                )
396            )
397        }
398    } else {
399        without_display_name(ty)
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    /// Extracts and collects the contents of the Rust documentation attributes.
408    fn extract_doc_attributes(attrs: &[syn::Attribute]) -> Vec<String> {
409        attrs
410            .iter()
411            .filter_map(|attr| attr.extract_docs())
412            .collect()
413    }
414
415    #[test]
416    fn extract_doc_comments_works() {
417        assert_eq!(
418            extract_doc_attributes(&[syn::parse_quote!( #[doc = r"content"] )]),
419            vec!["content".to_string()],
420        );
421        assert_eq!(
422            extract_doc_attributes(&[syn::parse_quote!(
423                /// content
424            )]),
425            vec![" content".to_string()],
426        );
427        assert_eq!(
428            extract_doc_attributes(&[syn::parse_quote!(
429                /**
430                 * Multi-line comments
431                 * may span many,
432                 * many lines
433                 */
434            )]),
435            vec![r"
436                 * Multi-line comments
437                 * may span many,
438                 * many lines
439                 "
440            .to_string()],
441        );
442        assert_eq!(
443            extract_doc_attributes(&[
444                syn::parse_quote!(
445                    /// multiple
446                ),
447                syn::parse_quote!(
448                    /// single
449                ),
450                syn::parse_quote!(
451                    /// line
452                ),
453                syn::parse_quote!(
454                    /// comments
455                ),
456            ]),
457            vec![
458                " multiple".to_string(),
459                " single".to_string(),
460                " line".to_string(),
461                " comments".to_string(),
462            ],
463        );
464        assert_eq!(
465            extract_doc_attributes(&[
466                syn::parse_quote!( #[doc = r"a"] ),
467                syn::parse_quote!( #[non_doc] ),
468                syn::parse_quote!( #[doc = r"b"] ),
469                syn::parse_quote!( #[derive(NonDoc)] ),
470                syn::parse_quote!( #[doc = r"c"] ),
471                syn::parse_quote!( #[docker = false] ),
472                syn::parse_quote!( #[doc = r"d"] ),
473                syn::parse_quote!( #[doc(Nope)] ),
474                syn::parse_quote!( #[doc = r"e"] ),
475            ]),
476            vec![
477                "a".to_string(),
478                "b".to_string(),
479                "c".to_string(),
480                "d".to_string(),
481                "e".to_string(),
482            ],
483        )
484    }
485}