mirui-macros 0.46.2

Declarative UI DSL macro for mirui — powered by xrune
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;

pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let parsed: syn::Item = match syn::parse2(item) {
        Ok(i) => i,
        Err(e) => return e.to_compile_error(),
    };

    let mut f = match parsed {
        syn::Item::Fn(f) => f,
        other => {
            return syn::Error::new(
                other.span(),
                "`#[compose]` applies to functions only; use `ui!(Name { ... })` macro form for widget classes",
            )
            .to_compile_error();
        }
    };

    if f.sig.asyncness.is_some() {
        return syn::Error::new(
            f.sig.asyncness.span(),
            "`#[compose]` does not support async fn in v0.40",
        )
        .to_compile_error();
    }

    if !f.sig.generics.params.is_empty() {
        return syn::Error::new(
            f.sig.generics.span(),
            "`#[compose]` does not support generic fn in v0.40",
        )
        .to_compile_error();
    }

    for input in f.sig.inputs.iter() {
        if let syn::FnArg::Typed(pat_ty) = input
            && let syn::Pat::Ident(pat_ident) = &*pat_ty.pat
            && pat_ident.ident == "cx"
        {
            return syn::Error::new(
                pat_ident.ident.span(),
                "`#[compose]` fn cannot have a parameter named `cx` — the attribute injects one for you",
            )
            .to_compile_error();
        }
    }

    let cx_param: syn::FnArg = syn::parse_quote! {
        cx: &mut ::mirui::ui::scope::UiScope<'_>
    };
    f.sig.inputs.insert(0, cx_param);

    quote! { #f }
}

#[cfg(test)]
mod tests {
    use super::expand;
    use quote::quote;

    fn contains_compile_error(ts: &proc_macro2::TokenStream) -> bool {
        ts.to_string().contains("compile_error")
    }

    #[test]
    fn injects_cx_on_simple_fn() {
        let out = expand(quote! {}, quote! { fn build() {} });
        let s = out.to_string();
        assert!(
            s.contains("cx") && s.contains("UiScope"),
            "expected injected cx: &mut UiScope, got: {s}"
        );
        assert!(!contains_compile_error(&out));
    }

    #[test]
    fn preserves_other_params() {
        let out = expand(quote! {}, quote! { fn build(label: &str, count: i32) {} });
        let s = out.to_string();
        assert!(s.contains("label") && s.contains("count"));
        assert!(s.contains("cx") && s.contains("UiScope"));
    }

    #[test]
    fn rejects_async_fn() {
        let out = expand(quote! {}, quote! { async fn build() {} });
        assert!(contains_compile_error(&out));
        assert!(out.to_string().contains("async"));
    }

    #[test]
    fn rejects_generic_fn() {
        let out = expand(quote! {}, quote! { fn build<T>() {} });
        assert!(contains_compile_error(&out));
        assert!(out.to_string().contains("generic"));
    }

    #[test]
    fn rejects_struct() {
        let out = expand(quote! {}, quote! { struct Card; });
        assert!(contains_compile_error(&out));
        assert!(out.to_string().contains("functions only"));
    }

    #[test]
    fn rejects_existing_cx_param() {
        let out = expand(quote! {}, quote! { fn build(cx: i32) {} });
        assert!(contains_compile_error(&out));
        assert!(out.to_string().contains("cx"));
    }
}