interoptopus_proc_impl 0.16.0-alpha.24

Macros to produce Interoptopus item info.
Documentation
use proc_macro2::TokenStream;
use quote::quote_spanned;
use syn::spanned::Spanned;
use syn::{ItemFn, ReturnType};

use crate::function::model::FunctionModel;

impl FunctionModel {
    pub fn emit_modified_function(&self, original_fn: &ItemFn) -> TokenStream {
        let vis = &self.vis;
        let name = &self.name;
        let export_name = self.generate_export_name();
        let generics = &self.signature.generics;
        let inputs = &original_fn.sig.inputs;
        let output = &self.signature.output;
        let block = &original_fn.block;
        let unsafety = if self.is_unsafe {
            quote_spanned! { self.name.span() => unsafe }
        } else {
            quote_spanned! { self.name.span() => }
        };
        let where_clause = &generics.where_clause;

        // Preserve non-doc, non-ffi attributes (e.g. #[allow(...)])
        let preserved_attrs: Vec<_> = original_fn
            .attrs
            .iter()
            .filter(|attr| {
                let path = attr.path();
                !path.is_ident("doc") && !path.is_ident("ffi") && !path.is_ident("no_mangle") && !path.is_ident("unsafe")
            })
            .collect();

        quote_spanned! { self.name.span() =>
            #(#preserved_attrs)*
            #[unsafe(export_name = #export_name)]
            #vis #unsafety extern "C-unwind" fn #name #generics(#inputs) #output #where_clause #block
        }
    }

    pub fn emit_companion_struct(&self) -> TokenStream {
        let vis = &self.vis;
        let struct_name = &self.name;
        let generics = &self.signature.generics;
        let where_clause = &generics.where_clause;

        // If we have generic parameters, we need to use them or add PhantomData
        let phantom_data_field = if generics.params.is_empty() {
            quote_spanned! { self.name.span() => }
        } else {
            // Create a tuple type of all the generic parameters
            let param_types = generics
                .params
                .iter()
                .map(|param| match param {
                    syn::GenericParam::Lifetime(lifetime) => {
                        let lifetime_ident = &lifetime.lifetime;
                        quote_spanned! { lifetime_ident.span() => &#lifetime_ident () }
                    }
                    syn::GenericParam::Type(type_param) => {
                        let type_ident = &type_param.ident;
                        quote_spanned! { type_ident.span() => #type_ident }
                    }
                    syn::GenericParam::Const(const_param) => {
                        let const_ident = &const_param.ident;
                        quote_spanned! { const_ident.span() => [(); #const_ident] }
                    }
                })
                .collect::<Vec<_>>();

            if param_types.len() == 1 {
                quote_spanned! { self.name.span() => _phantom: ::std::marker::PhantomData<#(#param_types)*>, }
            } else {
                quote_spanned! { self.name.span() => _phantom: ::std::marker::PhantomData<(#(#param_types),*)>, }
            }
        };

        quote_spanned! { self.name.span() =>
            #[allow(non_camel_case_types)]
            #vis struct #struct_name #generics #where_clause {
                #phantom_data_field
            }
        }
    }

    pub fn emit_function_info_impl(&self) -> TokenStream {
        let struct_name = &self.name;
        let export_name = self.generate_export_name();
        let generics = &self.signature.generics;
        let where_clause = &generics.where_clause;

        let arguments = self.emit_arguments();
        let parameter_registrations = self.emit_parameter_types();
        let return_type = self.emit_return_type();
        let return_type_registration = self.emit_return_type_registration();
        let emission = self.emit_emission();
        let visibility = self.emit_visibility();
        let docs_tokens = self.emit_docs();
        let validation_guards = self.emit_validation_guards();

        quote_spanned! { struct_name.span() =>
            #validation_guards

            unsafe impl #generics ::interoptopus::lang::function::FunctionInfo for #struct_name #generics #where_clause {
                fn id() -> ::interoptopus::inventory::FunctionId {
                    ::interoptopus::inventory::FunctionId::from_id(::interoptopus::id!(#struct_name))
                }

                fn signature() -> ::interoptopus::lang::function::Signature {
                    ::interoptopus::lang::function::Signature {
                        arguments: vec![#(#arguments),*],
                        rval: #return_type,
                    }
                }

                fn function() -> ::interoptopus::lang::function::Function {
                    ::interoptopus::lang::function::Function {
                        name: #export_name.to_string(),
                        visibility: #visibility,
                        docs: #docs_tokens,
                        emission: #emission,
                        signature: Self::signature(),
                    }
                }

                fn register(inventory: &mut impl ::interoptopus::inventory::Inventory) {
                    // Register all parameter types
                    #(
                        #parameter_registrations;
                    )*

                    // Register return type
                    #return_type_registration;

                    // Register the function itself
                    inventory.register_function(Self::id(), Self::function());
                }
            }
        }
    }

    fn emit_arguments(&self) -> Vec<TokenStream> {
        self.signature
            .inputs
            .iter()
            .map(|param| {
                let name = param.name.to_string();
                let ty = &param.ty;
                quote_spanned! { param.name.span() =>
                    ::interoptopus::lang::function::Argument::new(
                        #name,
                        <#ty as ::interoptopus::lang::types::TypeInfo>::id()
                    )
                }
            })
            .collect()
    }

    fn emit_parameter_types(&self) -> Vec<TokenStream> {
        self.signature
            .inputs
            .iter()
            .map(|param| {
                let ty = &param.ty;
                quote_spanned! { param.ty.span() =>
                    <#ty as ::interoptopus::lang::types::TypeInfo>::register(inventory)
                }
            })
            .collect()
    }

    fn emit_return_type(&self) -> TokenStream {
        match &self.signature.output {
            ReturnType::Default => quote_spanned! { self.name.span() =>
                <() as ::interoptopus::lang::types::TypeInfo>::id()
            },
            ReturnType::Type(_, ty) => quote_spanned! { ty.span() =>
                <#ty as ::interoptopus::lang::types::TypeInfo>::id()
            },
        }
    }

    fn emit_return_type_registration(&self) -> TokenStream {
        match &self.signature.output {
            ReturnType::Default => quote_spanned! { self.name.span() =>
                <() as ::interoptopus::lang::types::TypeInfo>::register(inventory)
            },
            ReturnType::Type(_, ty) => quote_spanned! { ty.span() =>
                <#ty as ::interoptopus::lang::types::TypeInfo>::register(inventory)
            },
        }
    }

    fn emit_emission(&self) -> TokenStream {
        match &self.args.module {
            Some(crate::function::args::ModuleKind::Named(name)) => {
                quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Emission::FileEmission(::interoptopus::lang::meta::FileEmission::CustomModule(::interoptopus::lang::meta::Module::from_string(#name))) }
            }
            Some(crate::function::args::ModuleKind::Common) => {
                quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Emission::FileEmission(::interoptopus::lang::meta::FileEmission::Common) }
            }
            None => {
                quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Emission::FileEmission(::interoptopus::lang::meta::FileEmission::Default) }
            }
        }
    }

    fn emit_visibility(&self) -> TokenStream {
        match &self.vis {
            syn::Visibility::Public(_) => quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Visibility::Public },
            syn::Visibility::Restricted(_) => quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Visibility::Private },
            syn::Visibility::Inherited => quote_spanned! { self.name.span() => ::interoptopus::lang::meta::Visibility::Private },
        }
    }

    fn emit_docs(&self) -> TokenStream {
        let docs = &self.docs;
        quote_spanned! { self.name.span() =>
            ::interoptopus::lang::meta::Docs::from_lines(vec![#(#docs.to_string()),*])
        }
    }

    fn emit_validation_guards(&self) -> TokenStream {
        // Generate validation for each parameter with improved span attribution
        let parameter_validations = self.signature.inputs.iter().map(|param| {
            let param_ty = Self::elide_lifetimes(&param.ty);

            // Since we've proven that syn::Error::new_spanned works perfectly for span attribution,
            // we need to implement our own check rather than relying on assert_raw_safe.
            // This generates a properly-spanned error that covers the entire type.

            // For now, we'll use the original assert_raw_safe but acknowledge the span limitation
            // A future improvement could implement custom span-aware checking
            quote_spanned! {param.ty.span()=>
                const _: () = const {
                    // NOTE: This has a known limitation where complex path types like std::string::String
                    // only highlight the first segment (std) rather than the entire type.
                    // This is due to how syn::Type::span() works for path types.
                    ::interoptopus::lang::types::assert_raw_safe::<#param_ty>();
                };
            }
        });

        // Generate validation for return type
        let return_type_validation = match &self.signature.output {
            syn::ReturnType::Default => quote_spanned! { self.name.span() =>
                // Unit type is always RAW_SAFE, no validation needed
            },
            syn::ReturnType::Type(_, return_ty) => {
                let elided_return_ty = Self::elide_lifetimes(return_ty);

                // Use the return type token directly for proper span attribution
                quote_spanned! {return_ty.span()=>
                    const _: () = const {
                        ::interoptopus::lang::types::assert_raw_safe::<#elided_return_ty>();
                    };
                }
            }
        };

        quote_spanned! { self.name.span() =>
            // Compile-time validation guards
            #(#parameter_validations)*
            #return_type_validation
        }
    }

    fn elide_lifetimes(ty: &syn::Type) -> syn::Type {
        use syn::visit_mut::VisitMut;

        struct LifetimeElisor;

        impl VisitMut for LifetimeElisor {
            fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
                *i = syn::Lifetime::new("'_", i.span());
            }
        }

        let mut elided_ty = ty.clone();
        LifetimeElisor.visit_type_mut(&mut elided_ty);
        elided_ty
    }
}