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}