Skip to main content

ab_contracts_macros_impl/
contract.rs

1mod common;
2mod init;
3mod method;
4mod update;
5mod view;
6
7use crate::contract::common::{
8    derive_ident_metadata, extract_ident_from_type, max_depth_assertion,
9};
10use crate::contract::init::process_init_fn;
11use crate::contract::method::{ExtTraitComponents, MethodDetails};
12use crate::contract::update::{process_update_fn, process_update_fn_definition};
13use crate::contract::view::{process_view_fn, process_view_fn_definition};
14use ab_contracts_common::METADATA_STATIC_NAME_PREFIX;
15use heck::ToSnakeCase;
16use proc_macro2::{Ident, Literal, Span, TokenStream};
17use quote::{format_ident, quote};
18use std::collections::HashMap;
19use syn::spanned::Spanned;
20use syn::{
21    Error, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Meta, TraitItem, TraitItemConst, TraitItemFn,
22    Type, Visibility, parse_quote, parse2,
23};
24
25#[derive(Default)]
26struct MethodOutput {
27    guest_ffi: TokenStream,
28    trait_ext_components: ExtTraitComponents,
29}
30
31struct Method {
32    /// As authored in source code
33    original_ident: Ident,
34    methods_details: MethodDetails,
35}
36
37#[derive(Default)]
38struct ContractDetails {
39    methods: Vec<Method>,
40}
41
42pub(super) fn contract(item: TokenStream) -> Result<TokenStream, Error> {
43    if let Ok(item_trait) = parse2::<ItemTrait>(item.clone()) {
44        // Trait definition
45        return process_trait_definition(item_trait);
46    }
47
48    let error_message = "`#[contract]` must be applied to struct implementation, trait definition or trait \
49        implementation";
50
51    let item_impl =
52        parse2::<ItemImpl>(item).map_err(|error| Error::new(error.span(), error_message))?;
53
54    if let Some((path, _for)) = &item_impl.trait_ {
55        let trait_name = path
56            .get_ident()
57            .ok_or_else(|| Error::new(path.span(), error_message))?
58            .clone();
59        // Trait implementation
60        process_trait_impl(item_impl, &trait_name)
61    } else {
62        // Implementation of a struct
63        process_struct_impl(item_impl)
64    }
65}
66
67fn process_trait_definition(mut item_trait: ItemTrait) -> Result<TokenStream, Error> {
68    let trait_name = &item_trait.ident;
69
70    if !item_trait.generics.params.is_empty() {
71        return Err(Error::new(
72            item_trait.generics.span(),
73            "`#[contract]` does not support generics",
74        ));
75    }
76
77    let mut guest_ffis = Vec::with_capacity(item_trait.items.len());
78    let mut trait_ext_components = Vec::with_capacity(item_trait.items.len());
79    let mut contract_details = ContractDetails::default();
80
81    for item in &mut item_trait.items {
82        if let TraitItem::Fn(trait_item_fn) = item {
83            let method_output =
84                process_fn_definition(trait_name, trait_item_fn, &mut contract_details)?;
85            guest_ffis.push(method_output.guest_ffi);
86            trait_ext_components.push(method_output.trait_ext_components);
87
88            // This is needed to make trait itself object safe, which is in turn used as a hack for
89            // some APIs
90            if let Some(where_clause) = &mut trait_item_fn.sig.generics.where_clause {
91                where_clause.predicates.push(parse_quote! {
92                    Self: ::core::marker::Sized
93                });
94            } else {
95                trait_item_fn
96                    .sig
97                    .generics
98                    .where_clause
99                    .replace(parse_quote! {
100                        where
101                            Self: ::core::marker::Sized
102                    });
103            }
104        }
105    }
106
107    let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_trait.span())?;
108    let ext_trait = generate_extension_trait(trait_name, &trait_ext_components);
109
110    Ok(quote! {
111        #item_trait
112
113        // `dyn ContractTrait` here is a bit of a hack that allows treating a trait as a type. These
114        // constants specifically can't be implemented on a trait itself because that'll make trait
115        // not object safe, which is needed for `ContractTrait` that uses a similar hack with
116        // `dyn ContractTrait`.
117        impl ::ab_contracts_macros::__private::ContractTraitDefinition for dyn #trait_name {
118            #[cfg(feature = "guest")]
119            #[doc(hidden)]
120            const GUEST_FEATURE_ENABLED: () = ();
121            #metadata_const
122        }
123
124        #ext_trait
125
126        /// FFI code generated by procedural macro
127        #[expect(clippy::inline_modules, reason = "Macro-generated")]
128        #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
129        pub mod ffi {
130            use super::*;
131
132            #( #guest_ffis )*
133        }
134    })
135}
136
137fn process_trait_impl(mut item_impl: ItemImpl, trait_name: &Ident) -> Result<TokenStream, Error> {
138    let struct_name = item_impl.self_ty.as_ref();
139
140    if !item_impl.generics.params.is_empty() {
141        return Err(Error::new(
142            item_impl.generics.span(),
143            "`#[contract]` does not support generics",
144        ));
145    }
146
147    let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
148    let mut contract_details = ContractDetails::default();
149
150    for item in &mut item_impl.items {
151        match item {
152            ImplItem::Fn(impl_item_fn) => {
153                let method_output = process_fn(
154                    struct_name.clone(),
155                    Some(trait_name),
156                    impl_item_fn,
157                    &mut contract_details,
158                )?;
159                guest_ffis.push(method_output.guest_ffi);
160
161                if let Some(where_clause) = &mut impl_item_fn.sig.generics.where_clause {
162                    where_clause.predicates.push(parse_quote! {
163                        Self: ::core::marker::Sized
164                    });
165                } else {
166                    impl_item_fn
167                        .sig
168                        .generics
169                        .where_clause
170                        .replace(parse_quote! {
171                            where
172                                Self: ::core::marker::Sized
173                        });
174                }
175            }
176            ImplItem::Const(impl_item_const) if impl_item_const.ident == "METADATA" => {
177                return Err(Error::new(
178                    impl_item_const.span(),
179                    "`#[contract]` doesn't allow overriding `METADATA` constant",
180                ));
181            }
182            _ => {
183                // Ignore
184            }
185        }
186    }
187
188    let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", trait_name);
189    let ffi_mod_ident = format_ident!("{}_ffi", trait_name.to_string().to_snake_case());
190    let metadata_const = generate_trait_metadata(&contract_details, trait_name, item_impl.span())?;
191    let method_fn_pointers_const = {
192        let methods = contract_details
193            .methods
194            .iter()
195            .map(|method| &method.original_ident);
196
197        quote! {
198            #[doc(hidden)]
199            const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
200                #( #ffi_mod_ident::#methods::fn_pointer::METHOD_FN_POINTER, )*
201            ];
202        }
203    };
204
205    Ok(quote! {
206        // The lint can only be allowed on an enclosing item for it to apply to `cfg_attr`
207        #[cfg(feature = "guest")]
208        #[allow(
209            unexpected_cfgs,
210            reason = "`abundance` is only a known `target_env` value when compiling for contracts"
211        )]
212        const _: () = {
213            /// Contribute trait metadata to contract's metadata
214            ///
215            /// Enabled with `guest` feature to appear in the final binary.
216            ///
217            /// See [`Contract::MAIN_CONTRACT_METADATA`] for details.
218            ///
219            /// [`Contract::MAIN_CONTRACT_METADATA`]: ::ab_contracts_macros::__private::Contract::MAIN_CONTRACT_METADATA
220            #[used]
221            #[unsafe(no_mangle)]
222            #[cfg_attr(
223                target_env = "abundance",
224                unsafe(link_section = "ab-contract-metadata")
225            )]
226            static #static_name: [::core::primitive::u8; <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.len()] = unsafe {
227                *<dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA.as_ptr().cast()
228            };
229        };
230
231        // Sanity check that trait implementation fully matches trait definition
232        const _: () = {
233            // Import as `ffi` for generated metadata constant to pick up a correct version
234            use #ffi_mod_ident as ffi;
235            #metadata_const
236
237            // Comparing compact metadata to allow argument name differences and similar things
238            // TODO: This two-step awkwardness because simple comparison doesn't work in const
239            //  environment yet
240            let (impl_compact_metadata, impl_compact_metadata_size) =
241                ::ab_contracts_macros::__private::ContractMetadataKind::compact(METADATA)
242                    .expect(
243                        "Generated metadata is correct and its types are not nested deeper than \
244                        `MAX_METADATA_DEPTH`; qed"
245                    );
246            let (def_compact_metadata, def_compact_metadata_size) =
247                ::ab_contracts_macros::__private::ContractMetadataKind::compact(
248                    <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::METADATA,
249                )
250                    .expect(
251                        "Generated metadata is correct and its types are not nested deeper than \
252                        `MAX_METADATA_DEPTH`; qed"
253                    );
254            assert!(
255                impl_compact_metadata_size == def_compact_metadata_size,
256                "Trait implementation must match trait definition exactly"
257            );
258            let mut i = 0;
259            while impl_compact_metadata_size > i {
260                assert!(
261                    impl_compact_metadata[i] == def_compact_metadata[i],
262                    "Trait implementation must match trait definition exactly"
263                );
264                i += 1;
265            }
266        };
267
268        // Ensure `guest` feature is enabled for crate with trait definition
269        #[cfg(feature = "guest")]
270        const _: () = <dyn #trait_name as ::ab_contracts_macros::__private::ContractTraitDefinition>::GUEST_FEATURE_ENABLED;
271
272        #item_impl
273
274        // `dyn ContractTrait` here is a bit of a hack that allows treating a trait as a type for
275        // convenient API in native execution environment
276        unsafe impl ::ab_contracts_macros::__private::ContractTrait<dyn #trait_name> for #struct_name {
277            #method_fn_pointers_const
278        }
279
280        /// FFI code generated by procedural macro
281        #[expect(clippy::inline_modules, reason = "Macro-generated")]
282        #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
283        pub mod #ffi_mod_ident {
284            use super::*;
285
286            #( #guest_ffis )*
287        }
288    })
289}
290
291fn generate_trait_metadata(
292    contract_details: &ContractDetails,
293    trait_name: &Ident,
294    span: Span,
295) -> Result<TraitItemConst, Error> {
296    let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
297        Error::new(
298            span,
299            format!("Trait can't have more than {} methods", u8::MAX),
300        )
301    })?;
302    let num_methods = Literal::u8_unsuffixed(num_methods);
303    let methods = contract_details
304        .methods
305        .iter()
306        .map(|method| &method.original_ident);
307    let trait_name_metadata = derive_ident_metadata(trait_name)?;
308
309    // Encodes the following:
310    // * Type: trait definition
311    // * Length of trait name in bytes (u8)
312    // * Trait name as UTF-8 bytes
313    // * Number of methods
314    // * Metadata of methods
315    Ok(parse_quote! {
316        /// Trait metadata, see [`ContractMetadataKind`] for encoding details
317        ///
318        /// [`ContractMetadataKind`]: ::ab_contracts_macros::__private::ContractMetadataKind
319        const METADATA: &[::core::primitive::u8] = {
320            const fn metadata()
321                -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
322            {
323                ::ab_contracts_macros::__private::concat_metadata_sources(&[
324                    &[::ab_contracts_macros::__private::ContractMetadataKind::Trait as ::core::primitive::u8],
325                    #trait_name_metadata,
326                    &[#num_methods],
327                    #( ffi::#methods::METADATA, )*
328                ])
329            }
330
331            // The reference extends the lifetime of metadata scratch to that of the constant
332            let (metadata_scratch, metadata_size) = &metadata();
333            <[_]>::split_at(metadata_scratch, *metadata_size).0
334        };
335    })
336}
337
338fn process_struct_impl(mut item_impl: ItemImpl) -> Result<TokenStream, Error> {
339    let struct_name = item_impl.self_ty.as_ref();
340
341    if !item_impl.generics.params.is_empty() {
342        return Err(Error::new(
343            item_impl.generics.span(),
344            "`#[contract]` does not support generics",
345        ));
346    }
347
348    // Needed for arguments
349    item_impl.attrs.extend([
350        parse_quote! { #[expect(clippy::allow_attributes, reason = "Attribute below")] },
351        parse_quote! { #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")] },
352    ]);
353
354    let mut guest_ffis = Vec::with_capacity(item_impl.items.len());
355    let mut trait_ext_components = Vec::with_capacity(item_impl.items.len());
356    let mut contract_details = ContractDetails::default();
357
358    for item in &mut item_impl.items {
359        if let ImplItem::Fn(impl_item_fn) = item {
360            let method_output = process_fn(
361                struct_name.clone(),
362                None,
363                impl_item_fn,
364                &mut contract_details,
365            )?;
366            guest_ffis.push(method_output.guest_ffi);
367            trait_ext_components.push(method_output.trait_ext_components);
368        }
369    }
370
371    let maybe_slot_type = MethodDetails::slot_type(
372        contract_details
373            .methods
374            .iter()
375            .map(|method| &method.methods_details),
376    );
377    let Some(slot_type) = maybe_slot_type else {
378        return Err(Error::new(
379            item_impl.span(),
380            "All `#[slot]` arguments must be of the same type in all methods of a contract",
381        ));
382    };
383
384    let maybe_tmp_type = MethodDetails::tmp_type(
385        contract_details
386            .methods
387            .iter()
388            .map(|method| &method.methods_details),
389    );
390
391    let Some(tmp_type) = maybe_tmp_type else {
392        return Err(Error::new(
393            item_impl.span(),
394            "All `#[tmp]` arguments must be of the same type in all methods of a contract",
395        ));
396    };
397
398    let max_depth_assertions = [
399        max_depth_assertion(
400            struct_name,
401            "State type must not be nested deeper than `MAX_METADATA_DEPTH`",
402        ),
403        max_depth_assertion(
404            &slot_type,
405            "`#[slot]` type must not be nested deeper than `MAX_METADATA_DEPTH`",
406        ),
407        max_depth_assertion(
408            &tmp_type,
409            "`#[tmp]` type must not be nested deeper than `MAX_METADATA_DEPTH`",
410        ),
411    ];
412
413    let metadata_const = {
414        let num_methods = u8::try_from(contract_details.methods.len()).map_err(|_error| {
415            Error::new(
416                item_impl.span(),
417                format!("Struct can't have more than {} methods", u8::MAX),
418            )
419        })?;
420        let num_methods = Literal::u8_unsuffixed(num_methods);
421        let methods = contract_details
422            .methods
423            .iter()
424            .map(|method| &method.original_ident);
425
426        // Encodes the following:
427        // * Type: contract
428        // * Metadata of the state type
429        // * Number of methods
430        // * Metadata of methods
431        quote! {
432            const MAIN_CONTRACT_METADATA: &[::core::primitive::u8] = {
433                const fn metadata()
434                    -> ([::core::primitive::u8; ::ab_contracts_macros::__private::MAX_METADATA_CAPACITY], usize)
435                {
436                    ::ab_contracts_macros::__private::concat_metadata_sources(&[
437                        &[::ab_contracts_macros::__private::ContractMetadataKind::Contract as ::core::primitive::u8],
438                        <#struct_name as ::ab_contracts_macros::__private::IoType>::METADATA,
439                        <#slot_type as ::ab_contracts_macros::__private::IoType>::METADATA,
440                        <#tmp_type as ::ab_contracts_macros::__private::IoType>::METADATA,
441                        &[#num_methods],
442                        #( ffi::#methods::METADATA, )*
443                    ])
444                }
445
446                // The reference extends the lifetime of metadata scratch to that of the constant
447                let (metadata_scratch, metadata_size) = &metadata();
448                <[_]>::split_at(metadata_scratch, *metadata_size).0
449            };
450        }
451    };
452    let method_fn_pointers_const = {
453        let methods = contract_details
454            .methods
455            .iter()
456            .map(|method| &method.original_ident);
457
458        quote! {
459            #[doc(hidden)]
460            const NATIVE_EXECUTOR_METHODS: &[::ab_contracts_macros::__private::NativeExecutorContactMethod] = &[
461                #( ffi::#methods::fn_pointer::METHOD_FN_POINTER, )*
462            ];
463        }
464    };
465
466    let struct_name_ident = extract_ident_from_type(struct_name).ok_or_else(|| {
467        Error::new(
468            struct_name.span(),
469            "`#[contract]` must be applied to simple struct implementation",
470        )
471    })?;
472
473    let ext_trait = generate_extension_trait(struct_name_ident, &trait_ext_components);
474
475    let struct_name_str = struct_name_ident.to_string();
476    let static_name = format_ident!("{METADATA_STATIC_NAME_PREFIX}{}", struct_name_str);
477    Ok(quote! {
478        // The lint can only be allowed on an enclosing item for it to apply to `cfg_attr`
479        #[cfg(feature = "guest")]
480        #[allow(
481            unexpected_cfgs,
482            reason = "`abundance` is only a known `target_env` value when compiling for contracts"
483        )]
484        const _: () = {
485            /// Main contract metadata
486            ///
487            /// Enabled with `guest` feature to appear in the final binary, also prevents from
488            /// `guest` feature being enabled in dependencies at the same time since that'll cause
489            /// duplicated symbols.
490            ///
491            /// See [`Contract::MAIN_CONTRACT_METADATA`] for details.
492            ///
493            /// [`Contract::MAIN_CONTRACT_METADATA`]: ::ab_contracts_macros::__private::Contract::MAIN_CONTRACT_METADATA
494            #[used]
495            #[unsafe(no_mangle)]
496            #[cfg_attr(
497                target_env = "abundance",
498                unsafe(link_section = "ab-contract-metadata")
499            )]
500            static #static_name: [
501                ::core::primitive::u8;
502                <#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
503                    .len()
504            ] = unsafe {
505                *<#struct_name as ::ab_contracts_macros::__private::Contract>::MAIN_CONTRACT_METADATA
506                    .as_ptr()
507                    .cast()
508            };
509        };
510
511        // Contracts decode metadata of these types with `MAX_METADATA_DEPTH` as the max nesting
512        // depth, so deeper types must not compile
513        const _: () = {
514            #( #max_depth_assertions )*
515        };
516
517        unsafe impl ::ab_contracts_macros::__private::Contract for #struct_name {
518            #metadata_const
519            #method_fn_pointers_const
520            #[doc(hidden)]
521            const CODE: &::core::primitive::str = ::ab_contracts_macros::__private::concatcp!(
522                #struct_name_str,
523                '[',
524                ::core::env!("CARGO_PKG_NAME"),
525                '/',
526                ::core::file!(),
527                ':',
528                ::core::line!(),
529                ':',
530                ::core::column!(),
531                ']',
532            );
533            // Ensure `guest` feature is enabled for `ab-contracts-common` crate
534            #[cfg(feature = "guest")]
535            #[doc(hidden)]
536            const GUEST_FEATURE_ENABLED: () = ();
537            type Slot = #slot_type;
538            type Tmp = #tmp_type;
539
540            fn code() -> impl ::core::ops::Deref<
541                Target = ::ab_contracts_macros::__private::VariableBytes<
542                    'static,
543                    { ::ab_contracts_macros::__private::MAX_CODE_SIZE },
544                >,
545            > {
546                const fn code_bytes() -> &'static [::core::primitive::u8] {
547                    <#struct_name as ::ab_contracts_macros::__private::Contract>::CODE.as_bytes()
548                }
549
550                const fn code_size() -> ::core::primitive::u32 {
551                    code_bytes().len() as ::core::primitive::u32
552                }
553
554                static CODE_SIZE: ::core::primitive::u32 = code_size();
555
556                ::ab_contracts_macros::__private::VariableBytes::from_buffer(
557                    code_bytes(),
558                    &CODE_SIZE
559                )
560                .expect("Size is the length of the code, which is a short string; qed")
561            }
562        }
563
564        #item_impl
565
566        #ext_trait
567
568        /// FFI code generated by procedural macro
569        #[expect(clippy::inline_modules, reason = "Macro-generated")]
570        #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
571        pub mod ffi {
572            use super::*;
573
574            #( #guest_ffis )*
575        }
576    })
577}
578
579fn process_fn_definition(
580    trait_name: &Ident,
581    trait_item_fn: &mut TraitItemFn,
582    contract_details: &mut ContractDetails,
583) -> Result<MethodOutput, Error> {
584    let supported_attrs = HashMap::<_, fn(_, _, _, _) -> _>::from_iter([
585        (format_ident!("update"), process_update_fn_definition as _),
586        (format_ident!("view"), process_view_fn_definition as _),
587    ]);
588    let mut attrs = trait_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
589        Meta::Path(path) => {
590            path.leading_colon.is_none()
591                && path.segments.len() == 1
592                && supported_attrs.contains_key(&path.segments[0].ident)
593        }
594        Meta::List(_meta_list) => false,
595        Meta::NameValue(_meta_name_value) => false,
596    });
597
598    let Some(attr) = attrs.next() else {
599        drop(attrs);
600
601        // Return an unmodified original if no recognized arguments are present
602        return Ok(MethodOutput::default());
603    };
604
605    if let Some(next_attr) = attrs.take(1).next() {
606        return Err(Error::new(
607            next_attr.span(),
608            format!(
609                "The method `{}` can only have one of `#[update]` or `#[view]` attributes specified",
610                trait_item_fn.sig.ident
611            ),
612        ));
613    }
614
615    // Make sure the method doesn't have customized ABI
616    if let Some(abi) = &trait_item_fn.sig.abi {
617        return Err(Error::new(
618            abi.span(),
619            format!(
620                "The method `{}` with `#[{}]` attribute must have default ABI",
621                trait_item_fn.sig.ident,
622                attr.meta.path().segments[0].ident
623            ),
624        ));
625    }
626
627    if trait_item_fn.default.is_some() {
628        return Err(Error::new(
629            trait_item_fn.span(),
630            "`#[contract]` does not support `#[update]` or `#[view]` methods with default implementation \
631            in trait definition",
632        ));
633    }
634
635    let processor = supported_attrs
636        .get(&attr.path().segments[0].ident)
637        .expect("Matched above to be one of the supported attributes; qed");
638    processor(
639        trait_name,
640        &mut trait_item_fn.sig,
641        trait_item_fn.attrs.as_slice(),
642        contract_details,
643    )
644}
645
646fn process_fn(
647    struct_name: Type,
648    trait_name: Option<&Ident>,
649    impl_item_fn: &mut ImplItemFn,
650    contract_details: &mut ContractDetails,
651) -> Result<MethodOutput, Error> {
652    let supported_attrs = HashMap::<_, fn(_, _, _, _, _) -> _>::from_iter([
653        (format_ident!("init"), process_init_fn as _),
654        (format_ident!("update"), process_update_fn as _),
655        (format_ident!("view"), process_view_fn as _),
656    ]);
657    let mut attrs = impl_item_fn.attrs.extract_if(.., |attr| match &attr.meta {
658        Meta::Path(path) => {
659            path.leading_colon.is_none()
660                && path.segments.len() == 1
661                && supported_attrs.contains_key(&path.segments[0].ident)
662        }
663        Meta::List(_meta_list) => false,
664        Meta::NameValue(_meta_name_value) => false,
665    });
666
667    let Some(attr) = attrs.next() else {
668        drop(attrs);
669
670        // Return an unmodified original if no recognized arguments are present
671        return Ok(MethodOutput::default());
672    };
673
674    if let Some(next_attr) = attrs.take(1).next() {
675        return Err(Error::new(
676            next_attr.span(),
677            format!(
678                "The method `{}` can only have one of `#[init]`, `#[update]` or `#[view]` attributes specified",
679                impl_item_fn.sig.ident
680            ),
681        ));
682    }
683
684    // Make sure the method is public if not a trait impl
685    if !(matches!(impl_item_fn.vis, Visibility::Public(_)) || trait_name.is_some()) {
686        return Err(Error::new(
687            impl_item_fn.sig.span(),
688            format!(
689                "The method `{}` with `#[{}]` attribute must be public",
690                impl_item_fn.sig.ident,
691                attr.meta.path().segments[0].ident
692            ),
693        ));
694    }
695
696    // Make sure the method doesn't have customized ABI
697    if let Some(abi) = &impl_item_fn.sig.abi {
698        return Err(Error::new(
699            abi.span(),
700            format!(
701                "The method `{}` with `#[{}]` attribute must have default ABI",
702                impl_item_fn.sig.ident,
703                attr.meta.path().segments[0].ident
704            ),
705        ));
706    }
707
708    let processor = supported_attrs
709        .get(&attr.path().segments[0].ident)
710        .expect("Matched above to be one of the supported attributes; qed");
711    processor(
712        struct_name,
713        trait_name,
714        &mut impl_item_fn.sig,
715        impl_item_fn.attrs.as_slice(),
716        contract_details,
717    )
718}
719
720fn generate_extension_trait(
721    ident: &Ident,
722    trait_ext_components: &[ExtTraitComponents],
723) -> TokenStream {
724    let trait_name = format_ident!("{ident}Ext");
725    let trait_doc = format!(
726        "Extension trait that provides helper methods for calling [`{ident}`]'s methods on \
727        [`Env`](::ab_contracts_macros::__private::Env) for convenience purposes"
728    );
729    let definitions = trait_ext_components
730        .iter()
731        .map(|components| &components.definition);
732    let impls = trait_ext_components
733        .iter()
734        .map(|components| &components.r#impl);
735
736    quote! {
737        #[expect(clippy::wildcard_imports, reason = "Macro-generated")]
738        use ffi::*;
739
740        #[doc = #trait_doc]
741        #[automatically_derived]
742        #[expect(clippy::allow_attributes, reason = "Attribute below")]
743        #[allow(clippy::trivially_copy_pass_by_ref, reason = "API requirement")]
744        #[allow(rustdoc::redundant_explicit_links, reason = "Macro-generated")]
745        pub trait #trait_name {
746            #( #definitions )*
747        }
748
749        #[automatically_derived]
750        impl #trait_name for ::ab_contracts_macros::__private::Env<'_> {
751            #( #impls )*
752        }
753    }
754}