native-theme-derive 0.6.0

Proc-macro derive for native-theme widget struct pairs
Documentation
// Generation of check_ranges() on the Resolved struct.

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Ident;

use crate::parse::{FieldCategory, FieldMeta, LayerMeta, RangeCheck};

/// Generate `check_ranges()` impl block on the Resolved struct.
pub(crate) fn gen_ranges(opt_name: &Ident, fields: &[FieldMeta], layer: &LayerMeta) -> TokenStream {
    let resolved_name = layer
        .resolved_name
        .clone()
        .unwrap_or_else(|| format_ident!("Resolved{}", opt_name));

    let check_stmts = gen_check_stmts(fields);

    quote! {
        #[allow(clippy::ptr_arg)] // Generated code passes Vec to helper functions that push
        impl #resolved_name {
            /// Validate field ranges for this widget theme.
            /// Generated by `#[derive(ThemeWidget)]` from `#[theme(check/range)]` attributes.
            pub(crate) fn check_ranges(
                &self,
                prefix: &str,
                errors: &mut Vec<crate::error::RangeViolation>,
            ) {
                #check_stmts
            }
        }
    }
}

/// Generate range check statements from field metadata.
///
/// All generated code defers `format!` to error branches only -- the happy path
/// (all values in range) allocates zero path strings.
fn gen_check_stmts(fields: &[FieldMeta]) -> TokenStream {
    let mut stmts = Vec::new();

    for f in fields {
        let ident = &f.ident;
        let field_name = ident.to_string();

        // Emit explicit range check if present. A soft-option field stays
        // `Option<T>` on the resolved struct, so it is checked only where it
        // is stated.
        if let Some(ref check) = f.range_check {
            let soft = matches!(f.category, FieldCategory::SoftOption);
            let value = if soft {
                quote! { __value }
            } else {
                quote! { self.#ident }
            };
            let stmt = match check {
                RangeCheck::NonNegative => quote! {
                    crate::resolve::validate_helpers::check_non_negative(
                        #value,
                        prefix,
                        #field_name,
                        errors,
                    );
                },
                RangeCheck::Positive => quote! {
                    crate::resolve::validate_helpers::check_positive(
                        #value,
                        prefix,
                        #field_name,
                        errors,
                    );
                },
                RangeCheck::Range { min, max } => {
                    let min_lit = *min as f32;
                    let max_lit = *max as f32;
                    quote! {
                        crate::resolve::validate_helpers::check_range_f32(
                            #value,
                            #min_lit,
                            #max_lit,
                            prefix,
                            #field_name,
                            errors,
                        );
                    }
                }
                RangeCheck::RangeU16 { min, max } => {
                    let min_lit = *min;
                    let max_lit = *max;
                    quote! {
                        crate::resolve::validate_helpers::check_range_u16(
                            #value,
                            #min_lit,
                            #max_lit,
                            prefix,
                            #field_name,
                            errors,
                        );
                    }
                }
            };
            if soft {
                stmts.push(quote! {
                    if let Some(__value) = self.#ident {
                        #stmt
                    }
                });
            } else {
                stmts.push(stmt);
            }
        }

        // Emit min_max_pair check if present
        if let Some(ref pair_ident) = f.min_max_pair {
            let pair_name = pair_ident.to_string();
            stmts.push(quote! {
                crate::resolve::validate_helpers::check_min_max(
                    self.#ident,
                    self.#pair_ident,
                    prefix,
                    #field_name,
                    #pair_name,
                    errors,
                );
            });
        }

        // Auto-emit the padding check for nested ResolvedWidgetBorder fields:
        // every side the theme states must be >= 0; an unstated side is not
        // checked.
        if let FieldCategory::Nested { resolved_ty } = &f.category
            && is_resolved_widget_border(resolved_ty)
        {
            stmts.push(quote! {
                crate::resolve::validate_helpers::check_padding(
                    &self.#ident.padding,
                    prefix,
                    #field_name,
                    errors,
                );
            });
        }

        // Auto-emit font range checks for nested ResolvedFontSpec fields.
        // Inlines the check logic so that format! only runs in error branches,
        // avoiding sub-prefix allocation on the happy path.
        if let FieldCategory::Nested { resolved_ty } = &f.category
            && is_resolved_font_spec(resolved_ty)
        {
            stmts.push(quote! {
                {
                    let font = &self.#ident;
                    if !font.size.is_finite() || font.size <= 0.0 {
                        errors.push(crate::error::RangeViolation {
                            path: format!("{prefix}.{}.size", #field_name),
                            value: font.size as f64,
                            min: Some(f64::MIN_POSITIVE),
                            max: None,
                        });
                    }
                    if font.weight < 100 || font.weight > 900 {
                        errors.push(crate::error::RangeViolation {
                            path: format!("{prefix}.{}.weight", #field_name),
                            value: font.weight as f64,
                            min: Some(100.0),
                            max: Some(900.0),
                        });
                    }
                }
            });
        }
    }

    quote! { #(#stmts)* }
}

/// Check if a type path refers to ResolvedFontSpec.
fn is_resolved_font_spec(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty
        && let Some(seg) = type_path.path.segments.last()
    {
        return seg.ident == "ResolvedFontSpec";
    }
    false
}

/// Check if a type path refers to ResolvedWidgetBorder.
fn is_resolved_widget_border(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty
        && let Some(seg) = type_path.path.segments.last()
    {
        return seg.ident == "ResolvedWidgetBorder";
    }
    false
}