style: Allow to derive Parse/ToCss/SpecifiedValueInfo on bitflags

We keep getting this pattern of properties that have a set of joint and
disjoint flags, and copy-pasting or writing the same parsing and
serialization code in slightly different ways.

container-type is one such type, and I think we should have a single way
of dealing with this, thus implement deriving for various traits for
bitflags, with an attribute that says which flags are single vs mixed.

See docs and properties I ported. The remaining ones I left TODOs with,
they are a bit trickier but can be ported with some care.

Differential Revision: https://phabricator.services.mozilla.com/D142418
This commit is contained in:
Emilio Cobos Álvarez 2023-06-18 13:54:10 +02:00 committed by Martin Robinson
parent 19a43aa7da
commit f30837baf1
8 changed files with 222 additions and 312 deletions

View file

@ -2,14 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::to_css::CssVariantAttrs;
use crate::to_css::{CssBitflagAttrs, CssVariantAttrs};
use darling::FromField;
use darling::FromVariant;
use derive_common::cg;
use proc_macro2::TokenStream;
use quote::quote;
use proc_macro2::{TokenStream, Span};
use quote::{quote, TokenStreamExt};
use syn::parse_quote;
use syn::{self, DeriveInput, Path};
use syn::{self, Ident, DeriveInput, Path};
use synstructure::{Structure, VariantInfo};
#[derive(Default, FromVariant)]
@ -25,6 +25,70 @@ pub struct ParseFieldAttrs {
field_bound: bool,
}
fn parse_bitflags(bitflags: &CssBitflagAttrs) -> TokenStream {
let mut match_arms = TokenStream::new();
for (rust_name, css_name) in bitflags.single_flags() {
let rust_ident = Ident::new(&rust_name, Span::call_site());
match_arms.append_all(quote! {
#css_name if result.is_empty() => {
single_flag = true;
Self::#rust_ident
},
});
}
for (rust_name, css_name) in bitflags.mixed_flags() {
let rust_ident = Ident::new(&rust_name, Span::call_site());
match_arms.append_all(quote! {
#css_name => Self::#rust_ident,
});
}
let mut validate_condition = quote! { !result.is_empty() };
if let Some(ref function) = bitflags.validate_mixed {
validate_condition.append_all(quote! {
&& #function(result)
});
}
// NOTE(emilio): this loop has this weird structure because we run this code
// to parse stuff like text-decoration-line in the text-decoration
// shorthand, so we need to be a bit careful that we don't error if we don't
// consume the whole thing because we find an invalid identifier or other
// kind of token. Instead, we should leave it unconsumed.
quote! {
let mut result = Self::empty();
loop {
let mut single_flag = false;
let flag: Result<_, style_traits::ParseError<'i>> = input.try_parse(|input| {
Ok(try_match_ident_ignore_ascii_case! { input,
#match_arms
})
});
let flag = match flag {
Ok(flag) => flag,
Err(..) => break,
};
if single_flag {
return Ok(flag);
}
if result.intersects(flag) {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
result.insert(flag);
}
if #validate_condition {
Ok(result)
} else {
Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError))
}
}
}
fn parse_non_keyword_variant(
where_clause: &mut Option<syn::WhereClause>,
name: &syn::Ident,
@ -46,6 +110,13 @@ fn parse_non_keyword_variant(
let binding_ast = &bindings[0].ast();
let ty = &binding_ast.ty;
if let Some(ref bitflags) = variant_attrs.bitflags {
assert!(skip_try, "Should be the only variant");
assert!(parse_attrs.condition.is_none(), "Should be the only variant");
assert!(where_clause.is_none(), "Generic bitflags?");
return parse_bitflags(bitflags)
}
let field_attrs = cg::parse_field_attrs::<ParseFieldAttrs>(binding_ast);
if field_attrs.field_bound {
cg::add_predicate(where_clause, parse_quote!(#ty: crate::parser::Parse));

View file

@ -71,7 +71,14 @@ pub fn derive(mut input: DeriveInput) -> TokenStream {
}
},
Data::Struct(ref s) => {
if !derive_struct_fields(&s.fields, &mut types, &mut values) {
if let Some(ref bitflags) = css_attrs.bitflags {
for (_rust_name, css_name) in bitflags.single_flags() {
values.push(css_name)
}
for (_rust_name, css_name) in bitflags.mixed_flags() {
values.push(css_name)
}
} else if !derive_struct_fields(&s.fields, &mut types, &mut values) {
values.push(input_name());
}
},

View file

@ -7,13 +7,64 @@ use darling::FromDeriveInput;
use darling::FromField;
use darling::FromVariant;
use derive_common::cg;
use proc_macro2::TokenStream;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{ToTokens, TokenStreamExt};
use syn::parse_quote;
use syn::{self, Data, Path, WhereClause};
use syn::{self, Data, Ident, Path, WhereClause};
use synstructure::{BindingInfo, Structure, VariantInfo};
fn derive_bitflags(input: &syn::DeriveInput, bitflags: &CssBitflagAttrs) -> TokenStream {
let name = &input.ident;
let mut body = TokenStream::new();
for (rust_name, css_name) in bitflags.single_flags() {
let rust_ident = Ident::new(&rust_name, Span::call_site());
body.append_all(quote! {
if *self == Self::#rust_ident {
return dest.write_str(#css_name);
}
});
}
body.append_all(quote! {
let mut has_any = false;
});
for (rust_name, css_name) in bitflags.mixed_flags() {
let rust_ident = Ident::new(&rust_name, Span::call_site());
body.append_all(quote! {
if self.intersects(Self::#rust_ident) {
if has_any {
dest.write_char(' ')?;
}
has_any = true;
dest.write_str(#css_name)?;
}
});
}
body.append_all(quote! {
debug_assert!(has_any, "Shouldn't have parsed empty");
Ok(())
});
quote! {
impl style_traits::ToCss for #name {
#[allow(unused_variables)]
#[inline]
fn to_css<W>(
&self,
dest: &mut style_traits::CssWriter<W>,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
#body
}
}
}
}
pub fn derive(mut input: syn::DeriveInput) -> TokenStream {
let mut where_clause = input.generics.where_clause.take();
for param in input.generics.type_params() {
@ -21,12 +72,21 @@ pub fn derive(mut input: syn::DeriveInput) -> TokenStream {
}
let input_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input);
if let Data::Enum(_) = input.data {
if matches!(input.data, Data::Enum(..)) || input_attrs.bitflags.is_some() {
assert!(
input_attrs.function.is_none(),
"#[css(function)] is not allowed on enums"
"#[css(function)] is not allowed on enums or bitflags"
);
assert!(!input_attrs.comma, "#[css(comma)] is not allowed on enums");
assert!(
!input_attrs.comma,
"#[css(comma)] is not allowed on enums or bitflags"
);
}
if let Some(ref bitflags) = input_attrs.bitflags {
assert!(!input_attrs.derive_debug, "Bitflags can derive debug on their own");
assert!(where_clause.is_none(), "Generic bitflags?");
return derive_bitflags(&input, bitflags);
}
let match_body = {
@ -249,6 +309,32 @@ fn derive_single_field_expr(
expr
}
#[derive(Default, FromMeta)]
pub struct CssBitflagAttrs {
/// Flags that can only go on their own, comma-separated.
pub single: String,
/// Flags that can go mixed with each other, comma-separated.
pub mixed: String,
/// Extra validation of the resulting mixed flags.
#[darling(default)]
pub validate_mixed: Option<Path>,
}
impl CssBitflagAttrs {
/// Returns a vector of (rust_name, css_name) of a given flag list.
fn names(s: &str) -> Vec<(String, String)> {
s.split(',').map(|css_name| (cg::to_scream_case(css_name), css_name.to_owned())).collect()
}
pub fn single_flags(&self) -> Vec<(String, String)> {
Self::names(&self.single)
}
pub fn mixed_flags(&self) -> Vec<(String, String)> {
Self::names(&self.mixed)
}
}
#[derive(Default, FromDeriveInput)]
#[darling(attributes(css), default)]
pub struct CssInputAttrs {
@ -257,6 +343,7 @@ pub struct CssInputAttrs {
pub function: Option<Override<String>>,
// Here because structs variants are also their whole type definition.
pub comma: bool,
pub bitflags: Option<CssBitflagAttrs>,
}
#[derive(Default, FromVariant)]
@ -266,6 +353,7 @@ pub struct CssVariantAttrs {
// Here because structs variants are also their whole type definition.
pub derive_debug: bool,
pub comma: bool,
pub bitflags: Option<CssBitflagAttrs>,
pub dimension: bool,
pub keyword: Option<String>,
pub skip: bool,