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 syn::{Data, DataEnum, DeriveInput, Fields, WhereClause};
10use synstructure::{BindingInfo, Structure};
11
12/// Derive implementation of the `ToTyped` trait.
13///
14/// This derive supports both enums and structs:
15///
16/// * Enums
17///   * Pure keyword enums: Enums made up entirely of unit variants
18///     (e.g. `enum Visibility { Visible, Hidden, Collapse }`). In this case,
19///     the derive optimizes by delegating to `ToCss` once for the whole value,
20///     then wrapping the result in `TypedValue::Keyword`.
21///
22///   * Mixed enums with unit variants: Enums that contain both unit
23///     variants (keywords) and data-carrying variants. The derive generates a
24///     `match` implementation where unit variants are reified as
25///     `TypedValue::Keyword`, and fielded variants may be reified by calling
26///     `.to_typed()` on their inner values when the `derive_fields` attribute
27///     is enabled. Otherwise, those variants return `None`.
28///
29/// * Structs
30///   * Structs are handled similarly to data-carrying variants in mixed enums.
31///     When `derive_fields` is enabled and the type is not marked as a
32///     bitflags type, `.to_typed()` may be generated for their inner value;
33///     otherwise, they return `None`.
34///
35/// Unit variants are mapped to keywords using their Rust identifier converted
36/// via `to_css_identifier`. Attributes like `#[css(keyword = "...")]` are not
37/// yet supported in this path (see bug 1995187).
38///
39/// For other kinds of types (e.g. unions), no `to_typed` method is generated;
40/// the default implementation applies, which always returns `None`.
41///
42/// This allows keywords to be reified automatically into `CSSKeywordValue`
43/// objects, while leaving more complex value types to be implemented
44/// incrementally as Typed OM support expands.
45pub fn derive(mut input: DeriveInput) -> TokenStream {
46    // The mutable `where_clause` is passed down to helper functions so they
47    // can append trait bounds only when necessary. In particular, a bound of
48    // the form `T: ToTyped` is added only if the generated code actually calls
49    // `.to_typed()` on that inner value. This avoids forcing unrelated types
50    // to implement `ToTyped` prematurely, keeping compilation requirements
51    // minimal and isolated to cases where reification recursion is explicitly
52    // performed.
53    let mut where_clause = input.generics.where_clause.take();
54
55    let css_input_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input);
56
57    let input_attrs = cg::parse_input_attrs::<TypedValueInputAttrs>(&input);
58
59    let body = match &input.data {
60        // Handle enums.
61        Data::Enum(DataEnum { variants, .. }) => {
62            // Check if this enum consists entirely of unit variants (no fields).
63            let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
64
65            if all_unit {
66                // Optimization: for all-unit enums, reuse `ToCss` once and
67                // wrap the result in a `TypedValue::Keyword`, instead of
68                // generating a full match. This avoids code bloat while
69                // producing the same runtime behavior.
70                quote! {
71                    fn to_typed(&self) -> Option<style_traits::TypedValue> {
72                      let s = style_traits::ToCss::to_css_cssstring(self);
73                      Some(style_traits::TypedValue::Keyword(s))
74                    }
75                }
76            } else {
77                // Mixed enums: generate a `match` where unit variants map to
78                // `TypedValue::Keyword` and all other variants return `None`.
79                // This is more verbose in code size, but allows selective
80                // handling of individual variants.
81                let s = Structure::new(&input);
82                let match_body = s.each_variant(|variant| {
83                    derive_variant_arm(variant, input_attrs.derive_fields, &mut where_clause)
84                });
85
86                quote! {
87                    fn to_typed(&self) -> Option<style_traits::TypedValue> {
88                        match *self {
89                            #match_body
90                        }
91                    }
92                }
93            }
94        },
95
96        // Handle structs that are not bitflags.
97        Data::Struct(_) => {
98            if css_input_attrs.bitflags.is_none() {
99                let s = Structure::new(&input);
100                let match_body = s.each_variant(|variant| {
101                    derive_variant_arm(variant, input_attrs.derive_fields, &mut where_clause)
102                });
103
104                quote! {
105                    fn to_typed(&self) -> Option<style_traits::TypedValue> {
106                        match *self {
107                            #match_body
108                        }
109                    }
110                }
111            } else {
112                quote! {}
113            }
114        },
115
116        // Otherwise, don’t emit any `to_typed` method body. The default
117        // implementation (returning `None`) will apply.
118        _ => quote! {},
119    };
120
121    input.generics.where_clause = where_clause;
122
123    let name = &input.ident;
124
125    // Split the input type’s generics into pieces we can use for impl.
126    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
127
128    // Put it all together into the impl block.
129    quote! {
130        impl #impl_generics style_traits::ToTyped for #name #ty_generics #where_clause {
131            #body
132        }
133    }
134}
135
136/// Generate the match arm expression for a struct or enum variant in a derived
137/// `ToTyped` implementation.
138///
139/// * Unit variants are reified into `TypedValue::Keyword`, using the variant’s
140///   identifier converted with `cg::to_css_identifier`.
141/// * Variants marked with `#[css(skip)]` or `#[typed_value(skip)]` or
142///   `#[typed(todo)]` return `None`.
143/// * Variants with fields delegate to `derive_variant_fields_expr()` when
144///   `derive_fields` is enabled; otherwise they return `None`.
145///
146/// Note: `#[css(keyword = "...")]` overrides are not handled in this
147/// `derive_variant_arm` path. This is fine for now because all existing cases
148/// that use such overrides (e.g. `#[css(keyword = "preserve-3d")]`) occur in
149/// pure keyword enums, which are covered by the all-unit `ToCss` path. Support
150/// will need to be added here once mixed enums with keyword overrides start
151/// implementing `ToTyped`.
152fn derive_variant_arm(
153    variant: &synstructure::VariantInfo,
154    derive_fields: bool,
155    where_clause: &mut Option<WhereClause>,
156) -> TokenStream {
157    let bindings = variant.bindings();
158    // Get the underlying syn AST node for this variant.
159    let ast = variant.ast();
160    let identifier = &ast.ident;
161
162    // Parse any #[css(...)] attributes attached to this variant.
163    let css_variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&ast);
164
165    // Parse any #[typed_value(...)] attributes attached to this variant.
166    let variant_attrs = cg::parse_variant_attrs_from_ast::<TypedValueVariantAttrs>(&ast);
167
168    // If the variant is explicitly marked #[css(skip)], don’t generate
169    // anything for it, always return None.
170    if css_variant_attrs.skip {
171        return quote!(None);
172    }
173
174    assert!(
175        css_variant_attrs.keyword.is_none(),
176        "Unhandled keyword attribute"
177    );
178
179    // If the variant is explicitly marked #[typed_value(skip)] or
180    // #[typed_value(todo)], don’t generate anything for it, always return
181    // None.
182    if variant_attrs.skip || variant_attrs.todo {
183        return quote!(None);
184    }
185
186    // If the variant has no bindings (i.e. no data fields), treat it as a unit
187    // variant and reify it as a keyword.
188    if bindings.is_empty() {
189        // Convert the Rust variant name into its CSS identifier form
190        // (e.g. AvoidColumn -> "avoid-column").
191        let keyword = cg::to_css_identifier(&identifier.to_string());
192
193        // Emit code to wrap this keyword into a TypedValue.
194        quote! {
195            Some(style_traits::TypedValue::Keyword(
196                style_traits::CssString::from(#keyword)
197            ))
198        }
199    } else if derive_fields {
200        derive_variant_fields_expr(bindings, where_clause)
201    } else {
202        // This variant has one or more fields, but field reification is
203        // disabled. Without `derive_fields`, this variant simply returns
204        // `None`.
205        quote! {
206            None
207        }
208    }
209}
210
211/// Generate the match arm expression for fields of a struct or enum variant
212/// in a derived `ToTyped` implementation.
213///
214/// This helper examines the variant’s fields and determines whether it can
215/// safely generate a `.to_typed()` call for a single inner value. If the
216/// variant has exactly one non-skipped, non-iterable field, it emits a call to
217/// that field’s `ToTyped` implementation and adds the corresponding trait
218/// bound (e.g. `T: ToTyped`) to the `where` clause.
219///
220/// Variants with multiple usable fields or iterable fields are not yet
221/// supported and simply return `None`.
222fn derive_variant_fields_expr(
223    bindings: &[BindingInfo],
224    where_clause: &mut Option<WhereClause>,
225) -> TokenStream {
226    // Filter out fields marked with #[css(skip)] so they are ignored during
227    // reification.
228    let mut iter = bindings
229        .iter()
230        .filter_map(|binding| {
231            let css_field_attrs = cg::parse_field_attrs::<CssFieldAttrs>(&binding.ast());
232            if css_field_attrs.skip {
233                return None;
234            }
235            Some((binding, css_field_attrs))
236        })
237        .peekable();
238
239    // If no usable fields remain, generate code that just returns None.
240    let (first, css_field_attrs) = match iter.next() {
241        Some(pair) => pair,
242        None => return quote! { None },
243    };
244
245    // Handle the simple case of exactly one non-iterable field. Add a trait
246    // bound `T: ToTyped` to ensure the field type implements the required
247    // conversion, and emit a call to its `.to_typed()` method.
248    if !css_field_attrs.iterable && iter.peek().is_none() {
249        let ty = &first.ast().ty;
250        cg::add_predicate(where_clause, parse_quote!(#ty: style_traits::ToTyped));
251
252        return quote! { style_traits::ToTyped::to_typed(#first) };
253    }
254
255    // Complex cases (multiple fields, iterable fields, etc.) are not yet
256    // supported for automatic reification.
257    quote! {
258        None
259    }
260}
261
262#[derive(Default, FromDeriveInput)]
263#[darling(attributes(typed_value), default)]
264pub struct TypedValueInputAttrs {
265    /// Enables field-level recursion when deriving `ToTyped`.
266    ///
267    /// When set, the derive will attempt to call `.to_typed()` on inner
268    /// values (for example, struct fields or data-carrying enum variants)
269    /// instead of always returning `None`.
270    ///
271    /// This is intentionally opt-in: blindly enabling recursion would require
272    /// many types to implement `ToTyped` even when they don’t need to. Once
273    /// reification coverage is complete, this attribute may be replaced by
274    /// an opposite flag (see bug 1995184).
275    pub derive_fields: bool,
276}
277
278#[derive(Default, FromVariant)]
279#[darling(attributes(typed_value), default)]
280pub struct TypedValueVariantAttrs {
281    /// Same as the top-level `derive_fields`, but included here because
282    /// struct variants are represented as both a variant and a type
283    /// definition.
284    pub derive_fields: bool,
285
286    /// If present, this variant is excluded from generated reification code.
287    /// `to_typed()` will always return `None` for it.
288    pub skip: bool,
289
290    /// Marks this variant as a placeholder for a future implementation.
291    /// Behavior is the same as `skip`, but used to indicate that reification
292    /// is intentionally left unimplemented for now.
293    pub todo: bool,
294}