Skip to main content

stylo_derive/
to_typed.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::cg;
6use crate::to_css::{CssFieldAttrs, CssInputAttrs, CssVariantAttrs};
7use proc_macro2::TokenStream;
8use quote::quote;
9use quote::ToTokens;
10use syn::{Data, DataEnum, DeriveInput, Fields, Path, WhereClause};
11use synstructure::{BindingInfo, Structure};
12
13/// Derive implementation of the `ToTyped` trait.
14///
15/// This derive supports both enums and structs:
16///
17/// * Enums
18///   * Pure keyword enums: Enums made up entirely of unit variants
19///     (e.g. `enum Visibility { Visible, Hidden, Collapse }`). In this case,
20///     the derive optimizes by delegating to `ToCss` once for the whole value,
21///     then wrapping the result in `TypedValue::Keyword`.
22///
23///   * Mixed enums with unit variants: Enums that contain both unit
24///     variants (keywords) and data-carrying variants. The derive generates a
25///     `match` implementation where unit variants are reified as
26///     `TypedValue::Keyword`, and fielded variants reify by calling
27///     `.to_typed()` on their inner values by default. Variants or types
28///     marked with `skip_derive_fields` return `Err(())` instead.
29///
30/// * Structs
31///   * Structs are handled similarly to data-carrying variants in mixed enums.
32///     Unless `skip_derive_fields` is set, and as long as the type is not
33///     marked as a bitflags type, `.to_typed()` is generated for their inner
34///     values; otherwise, they return `Err(())`.
35///
36/// Unit variants are mapped to keywords using their Rust identifier converted
37/// via `to_css_identifier`. Attributes like `#[css(keyword = "...")]` will
38/// override the behavior and use the provided keyword instead.
39///
40/// For other kinds of types (e.g. unions), no `to_typed` method is generated;
41/// the default implementation applies, which always returns `Err(())`.
42///
43/// This allows keywords to be reified automatically into `CSSKeywordValue`
44/// objects, while leaving more complex value types to be implemented
45/// incrementally as Typed OM support expands.
46///
47/// Summary of derive attributes recognized by this derive:
48///
49/// * `#[typed(skip_derive_fields)]` on the type disables field recursion for
50///   structs and data-carrying enum variants.
51///
52/// * `#[css(skip)]`, `#[typed(skip)]`, or `#[typed(todo)]` on a variant mark
53///   it as unsupported and cause the generated arm to return
54///   `Err(())`.
55///
56/// * `#[css(skip)]` on a field disables reification for that field.
57///
58/// * `#[typed(skip_if = "...")]` on a field conditionally disables reification
59///   for that field. If the provided function returns `true` for the field
60///   value, the field is ignored.
61///
62/// * `#[css(keyword = "...")]` on a unit variant overrides the keyword
63///   string.
64///
65/// * `#[css(comma)]` on the variant indicates that fields may reify to
66///   multiple separate values. When present, multiple `TypedValue`s may be
67///   produced across the supported fields. If it is not present and the
68///   derived implementation would produce more than one item, it treats the
69///   value as unsupported and returns `Err(())`.
70///
71/// * `#[css(iterable)]` on a field indicates that the field is an iterable
72///   collection whose elements should be reified individually.
73///
74/// * `#[css(if_empty = "...")]` on an iterable field causes the provided
75///   keyword to be emitted when the iterable contains no elements.
76pub fn derive(mut input: DeriveInput) -> TokenStream {
77    // The mutable `where_clause` is passed down to helper functions so they
78    // can append trait bounds only when necessary. In particular, a bound of
79    // the form `T: ToTyped` is added only if the generated code actually calls
80    // `.to_typed()` on that inner value. This avoids forcing unrelated types
81    // to implement `ToTyped` when field recursion is disabled, keeping
82    // compilation requirements minimal and isolated to cases where reification
83    // recursion is actually performed.
84    let mut where_clause = input.generics.where_clause.take();
85
86    let css_input_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input);
87
88    let input_attrs = cg::parse_input_attrs::<TypedInputAttrs>(&input);
89
90    let body = match &input.data {
91        // Handle enums.
92        Data::Enum(DataEnum { variants, .. }) => {
93            // Check if this enum consists entirely of unit variants (no fields).
94            let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
95
96            if all_unit {
97                // Optimization: for all-unit enums, reuse `ToCss` once and
98                // wrap the result in a `TypedValue::Keyword`, instead of
99                // generating a full match. This avoids code bloat while
100                // producing the same runtime behavior.
101                quote! {
102                    fn to_typed(&self, dest: &mut thin_vec::ThinVec<style_traits::TypedValue>) -> Result<(), ()> {
103                      let s = style_traits::ToCss::to_css_cssstring(self);
104                      dest.push(style_traits::TypedValue::Keyword(style_traits::KeywordValue(s)));
105                      Ok(())
106                    }
107                }
108            } else {
109                // Mixed enums: generate a `match` where unit variants map to
110                // `TypedValue::Keyword` and all other variants return
111                // `Err(())`. This is more verbose in code size, but allows
112                // selective handling of individual variants.
113                let s = Structure::new(&input);
114                let match_body = s.each_variant(|variant| {
115                    derive_variant_arm(
116                        variant,
117                        input_attrs.skip_derive_fields || input_attrs.todo_derive_fields,
118                        &mut where_clause,
119                    )
120                });
121
122                quote! {
123                    fn to_typed(&self, dest: &mut thin_vec::ThinVec<style_traits::TypedValue>) -> Result<(), ()> {
124                        match *self {
125                            #match_body
126                        }
127                    }
128                }
129            }
130        },
131
132        // Handle structs that are not bitflags.
133        Data::Struct(_) => {
134            if css_input_attrs.bitflags.is_none() {
135                let s = Structure::new(&input);
136                let match_body = s.each_variant(|variant| {
137                    derive_variant_arm(
138                        variant,
139                        input_attrs.skip_derive_fields || input_attrs.todo_derive_fields,
140                        &mut where_clause,
141                    )
142                });
143
144                quote! {
145                    fn to_typed(&self, dest: &mut thin_vec::ThinVec<style_traits::TypedValue>) -> Result<(), ()> {
146                        match *self {
147                            #match_body
148                        }
149                    }
150                }
151            } else {
152                quote! {}
153            }
154        },
155
156        // Otherwise, don’t emit any `to_typed` method body. The default
157        // implementation (returning `Err(())`) will apply.
158        _ => quote! {},
159    };
160
161    input.generics.where_clause = where_clause;
162
163    let name = &input.ident;
164
165    // Split the input type’s generics into pieces we can use for impl.
166    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
167
168    // Put it all together into the impl block.
169    quote! {
170        impl #impl_generics style_traits::ToTyped for #name #ty_generics #where_clause {
171            #body
172        }
173    }
174}
175
176/// Generate the match arm expression for a struct or enum variant in a derived
177/// `ToTyped` implementation.
178///
179/// * Unit variants are reified into `TypedValue::Keyword`, using the variant’s
180///   identifier converted with `cg::to_css_identifier` or a custom keyword if
181///   provided through `#[css(keyword = "...")]`.
182/// * Variants marked with `#[css(skip)]` or `#[typed(skip)]` or
183///   `#[typed(todo)]` return `Err(())`.
184/// * Variants with fields delegate to `derive_variant_fields_expr()` by
185///   default; if `skip_derive_fields` is set, they return `Err(())`.
186///
187/// Note: `#[css(keyword = "...")]` overrides are now recognized in this
188/// `derive_variant_arm` path, but the support is not yet exercised because we
189/// currently have no mixed enums that use keyword overrides together with
190/// `ToTyped`. This keeps the behavior the same as before, all existing enums
191/// with keyword overrides (e.g. `#[css(keyword = "preserve-3d")]`) are still
192/// pure keyword enums and are handled through the all-unit `ToCss` path.
193fn derive_variant_arm(
194    variant: &synstructure::VariantInfo,
195    skip_derive_fields: bool,
196    where_clause: &mut Option<WhereClause>,
197) -> TokenStream {
198    let bindings = variant.bindings();
199    // Get the underlying syn AST node for this variant.
200    let ast = variant.ast();
201    let identifier = &ast.ident;
202
203    // Parse any #[css(...)] attributes attached to this variant.
204    let css_variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&ast);
205
206    // Parse any #[typed(...)] attributes attached to this variant.
207    let variant_attrs = cg::parse_variant_attrs_from_ast::<TypedVariantAttrs>(&ast);
208
209    // If the variant is explicitly marked #[css(skip)], don’t generate
210    // anything for it, always return Err(()).
211    if css_variant_attrs.skip {
212        return quote! {Err(())};
213    }
214
215    // If the variant is explicitly marked #[typed(skip)] or #[typed(todo)],
216    // don’t generate anything for it, always return Err(()).
217    if variant_attrs.skip || variant_attrs.todo {
218        return quote! {Err(())};
219    }
220
221    // If the variant has no bindings (i.e. no data fields), treat it as a unit
222    // variant and reify it as a keyword.
223    if bindings.is_empty() {
224        // If #[css(keyword = "...")] is present, use it.
225        // Else convert the Rust variant name into its CSS identifier form
226        // (e.g. AvoidColumn -> "avoid-column").
227        let keyword = css_variant_attrs
228            .keyword
229            .unwrap_or_else(|| cg::to_css_identifier(&identifier.to_string()));
230
231        // Emit code to wrap this keyword into a TypedValue.
232        quote! {
233            dest.push(style_traits::TypedValue::Keyword(
234                style_traits::KeywordValue(style_traits::CssString::from(#keyword))
235            ));
236            Ok(())
237        }
238    } else if !skip_derive_fields {
239        derive_variant_fields_expr(bindings, where_clause, css_variant_attrs.comma)
240    } else {
241        // This variant has one or more fields, but field reification is
242        // disabled. With `skip_derive_fields`, this variant simply returns
243        // `Err(())`.
244        quote! {
245            Err(())
246        }
247    }
248}
249
250/// Generate the match arm expression for fields of a struct or enum variant
251/// in a derived `ToTyped` implementation.
252///
253/// This helper examines the variant’s fields and generates reification code
254/// for the usable fields.
255///
256/// * If the variant has exactly one non-iterable field, it emits a direct
257///   call to that field’s `ToTyped` implementation and adds the corresponding
258///   trait bound (e.g. `T: ToTyped`) to the `where` clause.
259///
260/// * Otherwise, it appends the reified output of the supported fields to the
261///   destination and then validates the combined result against the enclosing
262///   variant’s `#[css(comma)]` setting.
263///
264/// Fields marked with `#[css(skip)]`, or skipped by
265/// `#[typed(skip_if = "...")]`, are ignored.
266fn derive_variant_fields_expr(
267    bindings: &[BindingInfo],
268    where_clause: &mut Option<WhereClause>,
269    comma: bool,
270) -> TokenStream {
271    // Filter out fields marked with #[css(skip)] so they are ignored during
272    // reification.
273    let mut iter = bindings
274        .iter()
275        .filter_map(|binding| {
276            let css_field_attrs = cg::parse_field_attrs::<CssFieldAttrs>(&binding.ast());
277            let field_attrs = cg::parse_field_attrs::<TypedFieldAttrs>(&binding.ast());
278            if css_field_attrs.skip {
279                return None;
280            }
281            Some((binding, css_field_attrs, field_attrs))
282        })
283        .peekable();
284
285    // If no usable fields remain, generate code that just returns Err(()).
286    let (first, css_field_attrs, field_attrs) = match iter.next() {
287        Some(triple) => triple,
288        None => return quote! { Err(()) },
289    };
290
291    // At this point we have at least one usable field in `first`.
292
293    // Handle the simple case of exactly one non-iterable field.
294    if !css_field_attrs.iterable && iter.peek().is_none() {
295        // Add a trait bound `T: ToTyped` to ensure the field type implements
296        // the required conversion, and emit a call to its `.to_typed()`
297        // method.
298        let ty = &first.ast().ty;
299        cg::add_predicate(where_clause, parse_quote!(#ty: style_traits::ToTyped));
300
301        let mut expr = quote! { style_traits::ToTyped::to_typed(#first, dest) };
302
303        if let Some(condition) = field_attrs.skip_if {
304            expr = quote! {
305                if !#condition(#first) {
306                    #expr
307                }
308            }
309        }
310
311        return expr;
312    }
313
314    // Handle the general case by appending reified output from the supported
315    // fields directly to the destination.
316    let mut expr = derive_single_field_expr(first, css_field_attrs, field_attrs, where_clause);
317    for (binding, css_field_attrs, field_attrs) in iter {
318        derive_single_field_expr(binding, css_field_attrs, field_attrs, where_clause)
319            .to_tokens(&mut expr)
320    }
321
322    quote! {{
323        let old_len = dest.len();
324        #expr
325        if !#comma && dest.len() - old_len > 1 {
326            dest.truncate(old_len);
327            return Err(());
328        }
329        Ok(())
330    }}
331}
332
333/// Generate the expression used to reify a single field in a derived
334/// `ToTyped` implementation.
335///
336/// For fields marked with `#[css(iterable)]`, this helper generates code that
337/// iterates over the field and calls `ToTyped::to_typed` for each element. If
338/// `#[css(if_empty = "...")]` is present, the generated code emits the
339/// specified keyword when the iterable is empty.
340///
341/// For non-iterable fields, it generates a direct `ToTyped::to_typed` call
342/// for the field value.
343///
344/// If `#[typed(skip_if = "...")]` is present and the provided function returns
345/// `true` for the field value, the field contributes no reified output.
346///
347/// The appropriate `T: ToTyped` bounds for the field type or iterable element
348/// type(s) are added to the `where` clause.
349fn derive_single_field_expr(
350    field: &BindingInfo,
351    css_field_attrs: CssFieldAttrs,
352    field_attrs: TypedFieldAttrs,
353    where_clause: &mut Option<WhereClause>,
354) -> TokenStream {
355    let mut expr = if css_field_attrs.iterable {
356        // We add `ToTyped` bounds for the iterable's element type(s), rather
357        // than for the container type itself. This avoids ToTyped forcing
358        // unrelated container types to implement ToTyped.
359        //
360        // This is a bit more involved than other derives, but it matches how
361        // Typed OM reification is structured today. If this approach works
362        // well, the helper that extracts the field types
363        // (field_generic_arguments) can be moved into cg.rs alongside other
364        // derive helpers.
365        //
366        // See also the comment in the beginning of the main `derive` fn.
367        for item_ty in field_generic_arguments(field) {
368            cg::add_predicate(where_clause, parse_quote!(#item_ty: style_traits::ToTyped));
369        }
370
371        if let Some(if_empty) = css_field_attrs.if_empty {
372            quote! {
373                let mut iter = #field.iter().peekable();
374                if iter.peek().is_none() {
375                    dest.push(style_traits::TypedValue::Keyword(
376                        style_traits::KeywordValue(style_traits::CssString::from(#if_empty)),
377                    ));
378                } else {
379                    for item in iter {
380                        style_traits::ToTyped::to_typed(&item, dest)?;
381                    }
382                }
383            }
384        } else {
385            quote! {
386                for item in #field.iter() {
387                    style_traits::ToTyped::to_typed(&item, dest)?;
388                }
389            }
390        }
391    } else {
392        // Add a trait bound `T: ToTyped` to ensure the field type implements
393        // the required conversion, and emit a call to its `.to_typed()`
394        // method.
395        let ty = &field.ast().ty;
396        cg::add_predicate(where_clause, parse_quote!(#ty: style_traits::ToTyped));
397
398        quote! {
399           style_traits::ToTyped::to_typed(#field, dest)?;
400        }
401    };
402
403    if let Some(condition) = field_attrs.skip_if {
404        expr = quote! {
405            if !#condition(#field) {
406                #expr
407            }
408        }
409    }
410
411    expr
412}
413
414/// Extract generic type arguments from a field type.
415///
416/// This helper is used by the `derive_variant_fields_expr` when handling
417/// iterable fields. The function needs to add `T: ToTyped` bounds for the
418/// item type produced by iteration (since the generated code calls
419/// `.to_typed()` on each item).
420///
421/// For example:
422///   * `Vec<T>` / `OwnedSlice<T>` -> `T`
423///   * `SmallVec<[T; N]>` -> `T`
424///
425/// The function inspects the last path segment of the field’s type and
426/// returns any generic type arguments it finds, unwrapping array forms such
427/// as `[T; N]` used by containers like `SmallVec`.
428///
429/// This is intentionally minimal and currently supports the container shapes
430/// used in style structs.
431pub(crate) fn field_generic_arguments(field: &BindingInfo) -> Vec<syn::Type> {
432    use syn::{GenericArgument, PathArguments, Type};
433
434    let ty = &field.ast().ty;
435
436    let Type::Path(type_path) = ty else {
437        return vec![];
438    };
439    let Some(seg) = type_path.path.segments.last() else {
440        return vec![];
441    };
442    let PathArguments::AngleBracketed(args) = &seg.arguments else {
443        return vec![];
444    };
445
446    let mut result = Vec::new();
447    for arg in &args.args {
448        let GenericArgument::Type(arg_ty) = arg else {
449            continue;
450        };
451
452        // If it's something like SmallVec<[T; N]>, take T.
453        match arg_ty {
454            Type::Array(arr) => result.push((*arr.elem).clone()),
455            _ => result.push(arg_ty.clone()),
456        }
457    }
458    result
459}
460
461#[derive(Default, FromDeriveInput)]
462#[darling(attributes(typed), default)]
463pub struct TypedInputAttrs {
464    /// Disables field-level recursion when deriving `ToTyped`.
465    ///
466    /// When set, the derive will not call `.to_typed()` on inner values (for
467    /// example, struct fields or data-carrying enum variants), and instead
468    /// the generated code will return `Err(())` for those cases.
469    ///
470    /// This is useful to avoid requiring inner types to implement `ToTyped`
471    /// when reification of those fields is not yet supported.
472    pub skip_derive_fields: bool,
473
474    /// Temporarily disables field-level recursion while marking it as TODO.
475    ///
476    /// When set, the derive will not call `.to_typed()` on inner values and
477    /// instead return `Err(())` for those cases.
478    ///
479    /// Unlike `skip_derive_fields`, this indicates that reification is
480    /// expected to be revisited later, either to implement it or to switch to
481    /// `skip_derive_fields` if it turns out to be unsupported.
482    pub todo_derive_fields: bool,
483}
484
485#[derive(Default, FromVariant)]
486#[darling(attributes(typed), default)]
487pub struct TypedVariantAttrs {
488    /// Same as the top-level `skip_derive_fields`, but included here because
489    /// struct variants are represented as both a variant and a type
490    /// definition.
491    ///
492    /// When set, field-level reification for this variant is disabled and the
493    /// generated code returns `Err(())`.
494    pub skip_derive_fields: bool,
495
496    /// Same as the top-level `todo_derive_fields`, but included here because
497    /// struct variants are represented as both a variant and a type
498    /// definition.
499    ///
500    /// When set, field-level reification for this variant is disabled and the
501    /// generated code returns `Err(())`.
502    pub todo_derive_fields: bool,
503
504    /// If present, this variant is excluded from generated reification code.
505    /// `to_typed()` will always return `Err(())` for it.
506    pub skip: bool,
507
508    /// Marks this variant as a placeholder for a future implementation.
509    /// Behavior is the same as `skip`, but used to indicate that reification
510    /// is intentionally left unimplemented for now.
511    pub todo: bool,
512}
513
514#[derive(Default, FromField)]
515#[darling(attributes(typed), default)]
516pub struct TypedFieldAttrs {
517    /// Conditionally skips reification of this field.
518    ///
519    /// The provided function is called with the field value. If it returns
520    /// `true`, the field is ignored and produces no `TypedValue` items.
521    pub skip_if: Option<Path>,
522}