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