ink_macro/
contract_ref.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 ink_codegen::generate_code;
16use ink_ir::{
17    ast,
18    format_err_spanned,
19    utils::duplicate_config_err,
20};
21use ink_primitives::abi::Abi;
22use proc_macro2::TokenStream as TokenStream2;
23use quote::{
24    format_ident,
25    quote,
26    quote_spanned,
27};
28
29pub fn analyze(config: TokenStream2, input: TokenStream2) -> TokenStream2 {
30    match analyze_or_err(config, input) {
31        Ok(tokens) => tokens,
32        Err(err) => err.to_compile_error(),
33    }
34}
35
36pub fn analyze_or_err(
37    config: TokenStream2,
38    input: TokenStream2,
39) -> syn::Result<TokenStream2> {
40    // Parses interface/contract ref config (if any).
41    let config = Config::parse(config)?;
42    // Re-uses trait definition IR and codegen.
43    let trait_def = ink_ir::InkTraitDefinition::new(TokenStream2::new(), input)?;
44    let trait_def_impl = generate_code((&trait_def, config.abi));
45
46    let span = trait_def.item().span();
47    let trait_name = trait_def.item().ident();
48    let contract_ref_name = format_ident!("{trait_name}Ref");
49    let abi_ty = match config.abi.unwrap_or({
50        #[cfg(not(ink_abi = "sol"))]
51        {
52            Abi::Ink
53        }
54
55        #[cfg(ink_abi = "sol")]
56        {
57            Abi::Sol
58        }
59    }) {
60        Abi::Ink => quote!(::ink::abi::Ink),
61        Abi::Sol => quote!(::ink::abi::Sol),
62    };
63    let env = config
64        .env
65        .unwrap_or_else(|| syn::parse_quote! { ::ink::env::DefaultEnvironment });
66    Ok(quote_spanned!(span =>
67        // Trait def implementation.
68        #trait_def_impl
69
70        // Type alias for contract ref.
71        type #contract_ref_name =
72            <<::ink::reflect::TraitDefinitionRegistry<#env> as #trait_name>
73                    ::__ink_TraitInfo as ::ink::codegen::TraitCallForwarder>::Forwarder<#abi_ty>;
74    ))
75}
76
77/// The interface/contract ref configuration.
78#[derive(Debug, PartialEq, Eq, Default)]
79struct Config {
80    /// The callee contract's ABI.
81    abi: Option<Abi>,
82    /// The environmental types definition.
83    ///
84    /// This must be a type that implements `ink_env::Environment` and can
85    /// be used to change the underlying environmental types of an ink! smart
86    /// contract.
87    env: Option<syn::Path>,
88}
89
90impl Config {
91    /// Parses contract ref config from token stream.
92    fn parse(config: TokenStream2) -> syn::Result<Config> {
93        let args = syn::parse2::<ast::AttributeArgs>(config)?;
94        let mut abi_info: Option<(Abi, ast::MetaNameValue)> = None;
95        let mut env_info: Option<(syn::Path, ast::MetaNameValue)> = None;
96        for arg in args.into_iter() {
97            if arg.name().is_ident("abi") {
98                if let Some((_, ast)) = abi_info {
99                    return Err(duplicate_config_err(ast, arg, "abi", "contract"));
100                }
101                let arg_info = arg
102                    .name_value()
103                    .zip(arg.value().and_then(ast::MetaValue::to_string));
104                if let Some((name_value, abi_str)) = arg_info {
105                    let abi = match abi_str.as_str() {
106                        "ink" => Abi::Ink,
107                        "sol" => Abi::Sol,
108                        _ => {
109                            return Err(format_err_spanned!(
110                                arg,
111                                "expected one of `ink` or `sol` for `abi` ink! configuration argument",
112                            ))
113                        }
114                    };
115                    abi_info = Some((abi, name_value.clone()));
116                } else {
117                    return Err(format_err_spanned!(
118                        arg,
119                        "expected a string literal value for `abi` ink! configuration argument",
120                    ));
121                }
122            } else if arg.name().is_ident("env") {
123                if let Some((_, ast)) = env_info {
124                    return Err(duplicate_config_err(ast, arg, "env", "contract"));
125                }
126                let arg_info = arg
127                    .name_value()
128                    .zip(arg.value().and_then(ast::MetaValue::as_path));
129                if let Some((name_value, path)) = arg_info {
130                    env_info = Some((path.clone(), name_value.clone()))
131                } else {
132                    return Err(format_err_spanned!(
133                        arg,
134                        "expected a path value for `env` ink! configuration argument",
135                    ));
136                }
137            } else {
138                return Err(format_err_spanned!(
139                    arg,
140                    "encountered unknown or unsupported ink! configuration argument",
141                ));
142            }
143        }
144        Ok(Config {
145            abi: abi_info.map(|(abi, _)| abi),
146            env: env_info.map(|(path, _)| path),
147        })
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    /// Asserts that the given input configuration attribute argument are converted
156    /// into the expected ink! configuration or yields the expected error message.
157    fn assert_try_from(input: TokenStream2, expected: Result<Config, &'static str>) {
158        assert_eq!(
159            Config::parse(input).map_err(|err| err.to_string()),
160            expected.map_err(ToString::to_string),
161        );
162    }
163
164    #[test]
165    fn empty_config_works() {
166        assert_try_from(quote! {}, Ok(Config::default()))
167    }
168
169    #[test]
170    fn abi_works() {
171        assert_try_from(
172            quote! {
173                abi = "ink"
174            },
175            Ok(Config {
176                abi: Some(Abi::Ink),
177                env: None,
178            }),
179        );
180        assert_try_from(
181            quote! {
182                abi = "sol"
183            },
184            Ok(Config {
185                abi: Some(Abi::Sol),
186                env: None,
187            }),
188        );
189    }
190
191    #[test]
192    fn abi_invalid_value_fails() {
193        assert_try_from(
194            quote! { abi = "move" },
195            Err("expected one of `ink` or `sol` for `abi` ink! configuration argument"),
196        );
197        assert_try_from(
198            quote! { abi = 1u8 },
199            Err("expected a string literal value for `abi` ink! configuration argument"),
200        );
201    }
202
203    #[test]
204    fn abi_missing_value_fails() {
205        assert_try_from(
206            syn::parse_quote! { abi },
207            Err("expected a string literal value for `abi` ink! configuration argument"),
208        );
209    }
210
211    #[test]
212    fn env_works() {
213        assert_try_from(
214            quote! {
215                env = ::my::env::Types
216            },
217            Ok(Config {
218                abi: None,
219                env: Some(syn::parse_quote! { ::my::env::Types }),
220            }),
221        )
222    }
223
224    #[test]
225    fn env_invalid_value_fails() {
226        assert_try_from(
227            quote! { env = "invalid" },
228            Err("expected a path value for `env` ink! configuration argument"),
229        );
230    }
231
232    #[test]
233    fn env_missing_value_fails() {
234        assert_try_from(
235            quote! { env },
236            Err("expected a path value for `env` ink! configuration argument"),
237        );
238    }
239
240    #[test]
241    fn unknown_arg_fails() {
242        assert_try_from(
243            quote! { unknown = argument },
244            Err("encountered unknown or unsupported ink! configuration argument"),
245        );
246    }
247
248    #[test]
249    fn duplicate_args_fails() {
250        assert_try_from(
251            quote! {
252                abi = "ink",
253                abi = "sol",
254            },
255            Err("encountered duplicate ink! contract `abi` configuration argument"),
256        );
257        assert_try_from(
258            quote! {
259                env = ::my::env::Types,
260                env = ::my::other::env::Types,
261            },
262            Err("encountered duplicate ink! contract `env` configuration argument"),
263        );
264    }
265}