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}