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