Files
dioxus/packages/core-macro/src/props/mod.rs
T
Evan Almloff 115cc0ad42 Fix component names when they are re-exported (#2744)
* Fix component names when they are re-exported
* Fix read only signal props expansion
2024-07-30 12:28:19 -07:00

1744 lines
71 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! This code mostly comes from idanarye/rust-typed-builder
//!
//! However, it has been adopted to fit the Dioxus Props builder pattern.
//!
//! For Dioxus, we make a few changes:
//! - [x] Automatically implement [`Into<Option>`] on the setters (IE the strip setter option)
//! - [x] Automatically implement a default of none for optional fields (those explicitly wrapped with [`Option<T>`])
use proc_macro2::TokenStream;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse::Error, PathArguments};
use quote::quote;
use syn::{parse_quote, Type};
pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let data = match &ast.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(fields) => {
let struct_info = struct_info::StructInfo::new(ast, fields.named.iter())?;
let builder_creation = struct_info.builder_creation_impl()?;
let conversion_helper = struct_info.conversion_helper_impl()?;
let fields = struct_info
.included_fields()
.map(|f| struct_info.field_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let extends = struct_info
.extend_fields()
.map(|f| struct_info.extends_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let fields = quote!(#(#fields)*).into_iter();
let required_fields = struct_info
.included_fields()
.filter(|f| f.builder_attr.default.is_none())
.map(|f| struct_info.required_field_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let build_method = struct_info.build_method_impl();
quote! {
#builder_creation
#conversion_helper
#( #fields )*
#( #extends )*
#( #required_fields )*
#build_method
}
}
syn::Fields::Unnamed(_) => {
return Err(Error::new(
ast.span(),
"Props is not supported for tuple structs",
))
}
syn::Fields::Unit => {
return Err(Error::new(
ast.span(),
"Props is not supported for unit structs",
))
}
},
syn::Data::Enum(_) => {
return Err(Error::new(ast.span(), "Props is not supported for enums"))
}
syn::Data::Union(_) => {
return Err(Error::new(ast.span(), "Props is not supported for unions"))
}
};
Ok(data)
}
mod util {
use quote::ToTokens;
pub fn path_to_single_string(path: &syn::Path) -> Option<String> {
if path.leading_colon.is_some() {
return None;
}
let mut it = path.segments.iter();
let segment = it.next()?;
if it.next().is_some() {
// Multipart path
return None;
}
if segment.arguments != syn::PathArguments::None {
return None;
}
Some(segment.ident.to_string())
}
pub fn expr_to_single_string(expr: &syn::Expr) -> Option<String> {
if let syn::Expr::Path(path) = expr {
path_to_single_string(&path.path)
} else {
None
}
}
pub fn ident_to_type(ident: syn::Ident) -> syn::Type {
let mut path = syn::Path {
leading_colon: None,
segments: Default::default(),
};
path.segments.push(syn::PathSegment {
ident,
arguments: Default::default(),
});
syn::Type::Path(syn::TypePath { qself: None, path })
}
pub fn empty_type() -> syn::Type {
syn::TypeTuple {
paren_token: Default::default(),
elems: Default::default(),
}
.into()
}
pub fn type_tuple(elems: impl Iterator<Item = syn::Type>) -> syn::TypeTuple {
let mut result = syn::TypeTuple {
paren_token: Default::default(),
elems: elems.collect(),
};
if !result.elems.empty_or_trailing() {
result.elems.push_punct(Default::default());
}
result
}
pub fn empty_type_tuple() -> syn::TypeTuple {
syn::TypeTuple {
paren_token: Default::default(),
elems: Default::default(),
}
}
pub fn make_punctuated_single<T, P: Default>(value: T) -> syn::punctuated::Punctuated<T, P> {
let mut punctuated = syn::punctuated::Punctuated::new();
punctuated.push(value);
punctuated
}
pub fn modify_types_generics_hack<F>(
ty_generics: &syn::TypeGenerics,
mut mutator: F,
) -> syn::AngleBracketedGenericArguments
where
F: FnMut(&mut syn::punctuated::Punctuated<syn::GenericArgument, syn::token::Comma>),
{
let mut abga: syn::AngleBracketedGenericArguments =
syn::parse(ty_generics.clone().into_token_stream().into()).unwrap_or_else(|_| {
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: Default::default(),
gt_token: Default::default(),
}
});
mutator(&mut abga.args);
abga
}
pub fn strip_raw_ident_prefix(mut name: String) -> String {
if name.starts_with("r#") {
name.replace_range(0..2, "");
}
name
}
}
mod field_info {
use crate::props::type_from_inside_option;
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{parse::Error, punctuated::Punctuated};
use syn::{parse_quote, Expr, Path};
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
};
#[derive(Debug)]
pub struct FieldInfo<'a> {
pub ordinal: usize,
pub name: &'a syn::Ident,
pub generic_ident: syn::Ident,
pub ty: &'a syn::Type,
pub builder_attr: FieldBuilderAttr,
}
impl<'a> FieldInfo<'a> {
pub fn new(
ordinal: usize,
field: &syn::Field,
field_defaults: FieldBuilderAttr,
) -> Result<FieldInfo, Error> {
if let Some(ref name) = field.ident {
let mut builder_attr = field_defaults.with(&field.attrs)?;
// children field is automatically defaulted to None
if name == "children" {
builder_attr.default =
Some(syn::parse(quote!(dioxus_core::VNode::empty()).into()).unwrap());
}
// String fields automatically use impl Display
if field.ty == parse_quote!(::std::string::String)
|| field.ty == parse_quote!(std::string::String)
|| field.ty == parse_quote!(string::String)
|| field.ty == parse_quote!(String)
{
builder_attr.from_displayable = true;
// ToString is both more general and provides a more useful error message than From<String>. If the user tries to use `#[into]`, use ToString instead.
builder_attr.auto_into = false;
}
// extended field is automatically empty
if !builder_attr.extends.is_empty() {
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
// auto detect optional
let strip_option_auto = builder_attr.strip_option
|| !builder_attr.ignore_option
&& type_from_inside_option(&field.ty, true).is_some();
if !builder_attr.strip_option && strip_option_auto {
builder_attr.strip_option = true;
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
Ok(FieldInfo {
ordinal,
name,
generic_ident: syn::Ident::new(
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
name.span(),
),
ty: &field.ty,
builder_attr,
})
} else {
Err(Error::new(field.span(), "Nameless field in struct"))
}
}
pub fn generic_ty_param(&self) -> syn::GenericParam {
syn::GenericParam::Type(self.generic_ident.clone().into())
}
pub fn type_ident(&self) -> syn::Type {
ident_to_type(self.generic_ident.clone())
}
pub fn tuplized_type_ty_param(&self) -> syn::Type {
let mut types = syn::punctuated::Punctuated::default();
types.push(self.ty.clone());
types.push_punct(Default::default());
syn::TypeTuple {
paren_token: Default::default(),
elems: types,
}
.into()
}
}
#[derive(Debug, Default, Clone)]
pub struct FieldBuilderAttr {
pub default: Option<syn::Expr>,
pub docs: Vec<syn::Attribute>,
pub skip: bool,
pub auto_into: bool,
pub from_displayable: bool,
pub strip_option: bool,
pub ignore_option: bool,
pub extends: Vec<Path>,
}
impl FieldBuilderAttr {
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
let mut skip_tokens = None;
for attr in attrs {
if attr.path().is_ident("doc") {
self.docs.push(attr.clone());
continue;
}
if path_to_single_string(attr.path()).as_deref() != Some("props") {
continue;
}
match &attr.meta {
syn::Meta::List(list) => {
if list.tokens.is_empty() {
continue;
}
}
_ => {
continue;
}
}
let as_expr = attr.parse_args_with(
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
)?;
for expr in as_expr.into_iter() {
self.apply_meta(expr)?;
}
// Stash its span for later (we don’t yet know if it’ll be an error)
if self.skip && skip_tokens.is_none() {
skip_tokens = Some(attr.meta.clone());
}
}
if self.skip && self.default.is_none() {
return Err(Error::new_spanned(
skip_tokens.unwrap(),
"#[props(skip)] must be accompanied by default or default_code",
));
}
Ok(self)
}
pub fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
match expr {
// #[props(default = "...")]
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
"extends" => {
if let syn::Expr::Path(path) = *assign.right {
self.extends.push(path.path);
Ok(())
} else {
Err(Error::new_spanned(
assign.right,
"Expected simple identifier",
))
}
}
"default" => {
self.default = Some(*assign.right);
Ok(())
}
"default_code" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(code),
..
}) = *assign.right
{
use std::str::FromStr;
let tokenized_code = TokenStream::from_str(&code.value())?;
self.default = Some(
syn::parse(tokenized_code.into())
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
);
} else {
return Err(Error::new_spanned(assign.right, "Expected string"));
}
Ok(())
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {name:?}"),
)),
}
}
// #[props(default)]
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
match name.as_str() {
"default" => {
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
Ok(())
}
"optional" => {
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
self.strip_option = true;
Ok(())
}
"extend" => {
self.extends.push(path.path);
Ok(())
}
_ => {
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
match name.as_str() {
$(
$flag => {
if self.$field {
Err(Error::new(path.span(), concat!("Illegal setting - field is already ", $already)))
} else {
self.$field = true;
Ok(())
}
}
)*
_ => Err(Error::new_spanned(
&path,
format!("Unknown setter parameter {:?}", name),
))
}
}
}
handle_fields!(
"skip", skip, "skipped";
"into", auto_into, "calling into() on the argument";
"displayable", from_displayable, "calling to_string() on the argument";
"strip_option", strip_option, "putting the argument in Some(...)";
)
}
}
}
syn::Expr::Unary(syn::ExprUnary {
op: syn::UnOp::Not(_),
expr,
..
}) => {
if let syn::Expr::Path(path) = *expr {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
match name.as_str() {
"default" => {
self.default = None;
Ok(())
}
"skip" => {
self.skip = false;
Ok(())
}
"auto_into" => {
self.auto_into = false;
Ok(())
}
"displayable" => {
self.from_displayable = false;
Ok(())
}
"optional" => {
self.strip_option = false;
self.ignore_option = true;
Ok(())
}
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
}
} else {
Err(Error::new_spanned(
expr,
"Expected simple identifier".to_owned(),
))
}
}
_ => Err(Error::new_spanned(expr, "Expected (<...>=<...>)")),
}
}
}
}
fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> {
let path = if let syn::Type::Path(type_path) = ty {
if type_path.qself.is_some() {
return None;
} else {
&type_path.path
}
} else {
return None;
};
let segment = path.segments.last()?;
if check_option_name && segment.ident != "Option" {
return None;
}
let generic_params =
if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
generic_params
} else {
return None;
};
if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
Some(ty)
} else {
None
}
}
mod struct_info {
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Expr, Ident};
use crate::props::strip_option;
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::util::{
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple,
};
use super::{child_owned_type, looks_like_callback_type, looks_like_signal_type};
#[derive(Debug)]
pub struct StructInfo<'a> {
pub vis: &'a syn::Visibility,
pub name: &'a syn::Ident,
pub generics: &'a syn::Generics,
pub fields: Vec<FieldInfo<'a>>,
pub builder_attr: TypeBuilderAttr,
pub builder_name: syn::Ident,
pub conversion_helper_trait_name: syn::Ident,
#[allow(unused)]
pub core: syn::Ident,
}
impl<'a> StructInfo<'a> {
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields
.iter()
.filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
}
pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields
.iter()
.filter(|f| !f.builder_attr.extends.is_empty())
}
pub fn new(
ast: &'a syn::DeriveInput,
fields: impl Iterator<Item = &'a syn::Field>,
) -> Result<StructInfo<'a>, Error> {
let builder_attr = TypeBuilderAttr::new(&ast.attrs)?;
let builder_name = strip_raw_ident_prefix(format!("{}Builder", ast.ident));
Ok(StructInfo {
vis: &ast.vis,
name: &ast.ident,
generics: &ast.generics,
fields: fields
.enumerate()
.map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone()))
.collect::<Result<_, _>>()?,
builder_attr,
builder_name: syn::Ident::new(&builder_name, ast.ident.span()),
conversion_helper_trait_name: syn::Ident::new(
&format!("{builder_name}_Optional"),
ast.ident.span(),
),
core: syn::Ident::new(&format!("{builder_name}_core"), ast.ident.span()),
})
}
fn modify_generics<F: FnMut(&mut syn::Generics)>(&self, mut mutator: F) -> syn::Generics {
let mut generics = self.generics.clone();
mutator(&mut generics);
generics
}
/// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadOnlySignal<T>`, the new signal should be owned by the child
fn has_child_owned_fields(&self) -> bool {
self.fields.iter().any(|f| child_owned_type(f.ty))
}
fn memoize_impl(&self) -> Result<TokenStream, Error> {
// First check if there are any ReadOnlySignal fields, if there are not, we can just use the partialEq impl
let signal_fields: Vec<_> = self
.included_fields()
.filter(|f| looks_like_signal_type(f.ty))
.map(|f| {
let name = f.name;
quote!(#name)
})
.collect();
let move_signal_fields = quote! {
trait NonPartialEq: Sized {
fn compare(&self, other: &Self) -> bool;
}
impl<T> NonPartialEq for &&T {
fn compare(&self, other: &Self) -> bool {
false
}
}
trait CanPartialEq: PartialEq {
fn compare(&self, other: &Self) -> bool;
}
impl<T: PartialEq> CanPartialEq for T {
fn compare(&self, other: &Self) -> bool {
self == other
}
}
// If they are equal, we don't need to rerun the component we can just update the existing signals
#(
// Try to memo the signal
let field_eq = {
let old_value: &_ = &*#signal_fields.peek();
let new_value: &_ = &*new.#signal_fields.peek();
(&old_value).compare(&&new_value)
};
if !field_eq {
(#signal_fields).__set(new.#signal_fields.__take());
}
// Move the old value back
self.#signal_fields = #signal_fields;
)*
};
let event_handlers_fields: Vec<_> = self
.included_fields()
.filter(|f| looks_like_callback_type(f.ty))
.collect();
let regular_fields: Vec<_> = self
.included_fields()
.chain(self.extend_fields())
.filter(|f| !looks_like_signal_type(f.ty) && !looks_like_callback_type(f.ty))
.map(|f| {
let name = f.name;
quote!(#name)
})
.collect();
let move_event_handlers: TokenStream = event_handlers_fields.iter().map(|field| {
// If this is an optional event handler, we need to check if it's None before we try to update it
let optional = strip_option(field.ty).is_some();
let name = field.name;
if optional {
quote! {
// If the event handler is None, we don't need to update it
if let (Some(old_handler), Some(new_handler)) = (self.#name.as_mut(), new.#name.as_ref()) {
old_handler.__set(new_handler.__take());
}
}
} else {
quote! {
// Update the event handlers
self.#name.__set(new.#name.__take());
}
}
}).collect();
// If there are signals, we automatically try to memoize the signals
if !signal_fields.is_empty() {
Ok(quote! {
// First check if the fields are equal. This will compare the signal fields by pointer
let exactly_equal = self == new;
if exactly_equal {
// If they are return early, they can be memoized without any changes
return true;
}
// If they are not, move the signal fields into self and check if they are equal now that the signal fields are equal
#(
let mut #signal_fields = self.#signal_fields;
self.#signal_fields = new.#signal_fields;
)*
// Then check if the fields are equal now that we know the signal fields are equal
// NOTE: we don't compare other fields individually because we want to let users opt-out of memoization for certain fields by implementing PartialEq themselves
let non_signal_fields_equal = self == new;
// If they are not equal, we need to move over all the fields that are not event handlers or signals to self
if !non_signal_fields_equal {
let new_clone = new.clone();
#(
self.#regular_fields = new_clone.#regular_fields;
)*
}
// Move any signal and event fields into their old container.
// We update signals and event handlers in place so that they are always up to date even if they were moved into a future in a previous render
#move_signal_fields
#move_event_handlers
non_signal_fields_equal
})
} else {
Ok(quote! {
let equal = self == new;
// Move any signal and event fields into their old container.
#move_event_handlers
// If they are not equal, we need to move over all the fields that are not event handlers to self
if !equal {
let new_clone = new.clone();
#(
self.#regular_fields = new_clone.#regular_fields;
)*
}
equal
})
}
}
pub fn builder_creation_impl(&self) -> Result<TokenStream, Error> {
let StructInfo {
ref vis,
ref name,
ref builder_name,
..
} = *self;
let generics = self.generics.clone();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let (_, b_initial_generics, _) = self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
);
let b_generics = self.modify_generics(|g| {
g.params.insert(0, all_fields_param.clone());
});
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
});
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
Some(quote!(::core::marker::PhantomData<#ty>))
}
syn::GenericParam::Const(_cnst) => None,
});
let builder_method_doc = match self.builder_attr.builder_method_doc {
Some(ref doc) => quote!(#doc),
None => {
let doc = format!(
"
Create a builder for building `{name}`.
On the builder, call {setters} to set the values of the fields.
Finally, call `.build()` to create the instance of `{name}`.
",
name = self.name,
setters = {
let mut result = String::new();
let mut is_first = true;
for field in self.included_fields() {
use std::fmt::Write;
if is_first {
is_first = false;
} else {
write!(&mut result, ", ").unwrap();
}
write!(&mut result, "`.{}(...)`", field.name).unwrap();
if field.builder_attr.default.is_some() {
write!(&mut result, "(optional)").unwrap();
}
}
result
}
);
quote!(#doc)
}
};
let builder_type_doc = if self.builder_attr.doc {
match self.builder_attr.builder_type_doc {
Some(ref doc) => quote!(#[doc = #doc]),
None => {
let doc = format!(
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
);
quote!(#[doc = #doc])
}
}
} else {
quote!(#[doc(hidden)])
};
let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl();
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
where Self: Clone
})?;
if let Some(predicates) = b_generics_where_extras_predicates {
b_generics_where
.predicates
.extend(predicates.predicates.clone());
}
let memoize = self.memoize_impl()?;
let global_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
})
.chain(self.has_child_owned_fields().then(|| quote!(owner: Owner)));
let global_fields_value = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: Vec::new())
})
.chain(
self.has_child_owned_fields()
.then(|| quote!(owner: Owner::default())),
);
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
#[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
#(#global_fields_value,)*
fields: #empties_tuple,
_phantom: ::core::default::Default::default(),
}
}
}
#[must_use]
#builder_type_doc
#[allow(dead_code, non_camel_case_types, non_snake_case)]
#vis struct #builder_name #b_generics {
#(#global_fields,)*
fields: #all_fields_param,
_phantom: (#( #phantom_generics ),*),
}
impl #impl_generics dioxus_core::prelude::Properties for #name #ty_generics
#b_generics_where
{
type Builder = #builder_name #generics_with_empty;
fn builder() -> Self::Builder {
#name::builder()
}
fn memoize(&mut self, new: &Self) -> bool {
#memoize
}
}
})
}
// TODO: once the proc-macro crate limitation is lifted, make this an util trait of this
// crate.
pub fn conversion_helper_impl(&self) -> Result<TokenStream, Error> {
let trait_name = &self.conversion_helper_trait_name;
Ok(quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub trait #trait_name<T> {
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
}
impl<T> #trait_name<T> for () {
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
default()
}
}
impl<T> #trait_name<T> for (T,) {
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
self.0
}
}
})
}
pub fn extends_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let StructInfo {
ref builder_name, ..
} = *self;
let field_name = field.name;
let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
});
let reconstructing = self.included_fields().map(|f| f.name);
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
}
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
})
.collect();
let mut target_generics_tuple = empty_type_tuple();
let mut ty_generics_tuple = empty_type_tuple();
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for f in self.included_fields() {
if f.ordinal == field.ordinal {
ty_generics_tuple.elems.push_value(empty_type());
target_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
} else {
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple.elems.push_value(generic_argument);
}
ty_generics_tuple.elems.push_punct(Default::default());
target_generics_tuple.elems.push_punct(Default::default());
}
});
let mut target_generics = ty_generics.clone();
let index_after_lifetime_in_generics = target_generics
.iter()
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
.count();
target_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(target_generics_tuple.into()),
);
ty_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(ty_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let forward_owner = self
.has_child_owned_fields()
.then(|| quote!(owner: self.owner))
.into_iter();
let extends_impl = field.builder_attr.extends.iter().map(|path| {
let name_str = path_to_single_string(path).unwrap();
let camel_name = name_str.to_case(Case::UpperCamel);
let marker_name = Ident::new(
format!("{}Extension", &camel_name).as_str(),
path.span(),
);
quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics dioxus_elements::extensions::#marker_name for #builder_name < #( #ty_generics ),* > #where_clause {}
}
});
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics dioxus_core::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause {
fn push_attribute(
mut self,
name: &'static str,
ns: Option<&'static str>,
attr: impl dioxus_core::prelude::IntoAttributeValue,
volatile: bool
) -> Self {
let ( #(#descructuring,)* ) = self.fields;
self.#field_name.push(
dioxus_core::Attribute::new(
name,
{
use dioxus_core::prelude::IntoAttributeValue;
attr.into_value()
},
ns,
volatile,
)
);
#builder_name {
#(#forward_extended_fields,)*
#(#forward_owner,)*
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
}
}
#(#extends_impl)*
})
}
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let FieldInfo {
name: field_name, ..
} = field;
if *field_name == "key" {
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.5/reference/dynamic_rendering#rendering-lists for more information about keys"));
}
let StructInfo {
ref builder_name, ..
} = *self;
let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
});
let reconstructing = self.included_fields().map(|f| f.name);
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
}
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
})
.collect();
let mut target_generics_tuple = empty_type_tuple();
let mut ty_generics_tuple = empty_type_tuple();
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for f in self.included_fields() {
if f.ordinal == field.ordinal {
ty_generics_tuple.elems.push_value(empty_type());
target_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
} else {
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple.elems.push_value(generic_argument);
}
ty_generics_tuple.elems.push_punct(Default::default());
target_generics_tuple.elems.push_punct(Default::default());
}
});
let mut target_generics = ty_generics.clone();
let index_after_lifetime_in_generics = target_generics
.iter()
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
.count();
target_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(target_generics_tuple.into()),
);
ty_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(ty_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let docs = &field.builder_attr.docs;
let arg_type = field_type;
// If the field is auto_into, we need to add a generic parameter to the builder for specialization
let mut marker = None;
let (arg_type, arg_expr) = if child_owned_type(arg_type) {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
// If this looks like a signal type, we automatically convert it with SuperInto and use the props struct as the owner
quote!(with_owner(self.owner.clone(), move || dioxus_core::prelude::SuperInto::super_into(#field_name))),
)
} else if field.builder_attr.auto_into || field.builder_attr.strip_option {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
quote!(dioxus_core::prelude::SuperInto::super_into(#field_name)),
)
} else if field.builder_attr.from_displayable {
(
quote!(impl ::core::fmt::Display),
quote!(#field_name.to_string()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let repeated_fields_error_type_name = syn::Ident::new(
&format!(
"{}_Error_Repeated_field_{}",
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
builder_name.span(),
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let forward_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: self.#name)
})
.chain(
self.has_child_owned_fields()
.then(|| quote!(owner: self.owner)),
);
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
#( #docs )*
#[allow(clippy::type_complexity)]
pub fn #field_name < #marker > (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
#(#forward_fields,)*
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
}
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub enum #repeated_fields_error_type_name {}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #target_generics ),* > #where_clause {
#[deprecated(
note = #repeated_fields_error_message
)]
#[allow(clippy::type_complexity)]
pub fn #field_name< #marker > (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
self
}
}
})
}
pub fn required_field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let StructInfo {
ref name,
ref builder_name,
..
} = self;
let FieldInfo {
name: ref field_name,
..
} = field;
let mut builder_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = &type_param.ident;
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
}
syn::GenericParam::Const(const_param) => {
let ident = &const_param.ident;
syn::parse(quote!(#ident).into()).unwrap()
}
})
.collect();
let mut builder_generics_tuple = empty_type_tuple();
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for f in self.included_fields() {
if f.builder_attr.default.is_some() {
// `f` is not mandatory - it does not have it's own fake `build` method, so `field` will need
// to warn about missing `field` whether or not `f` is set.
assert!(
f.ordinal != field.ordinal,
"`required_field_impl` called for optional field {}",
field.name
);
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
builder_generics_tuple.elems.push_value(f.type_ident());
} else if f.ordinal < field.ordinal {
// Only add a `build` method that warns about missing `field` if `f` is set. If `f` is not set,
// `f`'s `build` method will warn, since it appears earlier in the argument list.
builder_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
} else if f.ordinal == field.ordinal {
builder_generics_tuple.elems.push_value(empty_type());
} else {
// `f` appears later in the argument list after `field`, so if they are both missing we will
// show a warning for `field` and not for `f` - which means this warning should appear whether
// or not `f` is set.
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
builder_generics_tuple.elems.push_value(f.type_ident());
}
builder_generics_tuple.elems.push_punct(Default::default());
}
});
let index_after_lifetime_in_generics = builder_generics
.iter()
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
.count();
builder_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(builder_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let (_, ty_generics, _) = self.generics.split_for_impl();
let early_build_error_type_name = syn::Ident::new(
&format!(
"{}_Error_Missing_required_field_{}",
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
builder_name.span(),
);
let early_build_error_message = format!("Missing required field {field_name}");
Ok(quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub enum #early_build_error_type_name {}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs, clippy::panic)]
impl #impl_generics #builder_name < #( #builder_generics ),* > #where_clause {
#[deprecated(
note = #early_build_error_message
)]
pub fn build(self, _: #early_build_error_type_name) -> #name #ty_generics {
panic!()
}
}
})
}
pub fn build_method_impl(&self) -> TokenStream {
let StructInfo {
ref name,
ref builder_name,
..
} = *self;
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for field in self.included_fields() {
if field.builder_attr.default.is_some() {
let trait_ref = syn::TraitBound {
paren_token: None,
lifetimes: None,
modifier: syn::TraitBoundModifier::None,
path: syn::PathSegment {
ident: self.conversion_helper_trait_name.clone(),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: make_punctuated_single(syn::GenericArgument::Type(
field.ty.clone(),
)),
gt_token: Default::default(),
},
),
}
.into(),
};
let mut generic_param: syn::TypeParam = field.generic_ident.clone().into();
generic_param.bounds.push(trait_ref.into());
g.params
.insert(index_after_lifetime_in_generics, generic_param.into());
}
}
});
let (impl_generics, _, _) = generics.split_for_impl();
let (original_impl_generics, ty_generics, where_clause) =
self.generics.split_for_impl();
let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| {
args.insert(
0,
syn::GenericArgument::Type(
type_tuple(self.included_fields().map(|field| {
if field.builder_attr.default.is_some() {
field.type_ident()
} else {
field.tuplized_type_ty_param()
}
}))
.into(),
),
);
});
let descructuring = self.included_fields().map(|f| f.name);
let helper_trait_name = &self.conversion_helper_trait_name;
// The default of a field can refer to earlier-defined fields, which we handle by
// writing out a bunch of `let` statements first, which can each refer to earlier ones.
// This means that field ordering may actually be significant, which isn’t ideal. We could
// relax that restriction by calculating a DAG of field default dependencies and
// reordering based on that, but for now this much simpler thing is a reasonable approach.
let assignments = self.fields.iter().map(|field| {
let name = &field.name;
if !field.builder_attr.extends.is_empty() {
quote!(let #name = self.#name;)
} else if let Some(ref default) = field.builder_attr.default {
if field.builder_attr.skip {
quote!(let #name = #default;)
} else {
quote!(let #name = #helper_trait_name::into_value(#name, || #default);)
}
} else {
quote!(let #name = #name.0;)
}
});
let field_names = self.fields.iter().map(|field| field.name);
let doc = if self.builder_attr.doc {
match self.builder_attr.build_method_doc {
Some(ref doc) => quote!(#[doc = #doc]),
None => {
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalize the builder and create its [`{name}`] instance");
quote!(#[doc = #doc])
}
}
} else {
quote!()
};
if self.has_child_owned_fields() {
let name = Ident::new(&format!("{}WithOwner", name), name.span());
let original_name = &self.name;
let vis = &self.vis;
let generics_with_bounds = &self.generics;
let where_clause = &self.generics.where_clause;
quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[derive(Clone)]
#vis struct #name #generics_with_bounds #where_clause {
inner: #original_name #ty_generics,
owner: Owner,
}
impl #original_impl_generics PartialEq for #name #ty_generics #where_clause {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl #original_impl_generics #name #ty_generics #where_clause {
/// Create a component from the props.
pub fn into_vcomponent<M: 'static>(
self,
render_fn: impl dioxus_core::prelude::ComponentFunction<#original_name #ty_generics, M>,
) -> dioxus_core::VComponent {
use dioxus_core::prelude::ComponentFunction;
let component_name = ::std::any::type_name_of_val(&render_fn);
dioxus_core::VComponent::new(move |wrapper: Self| render_fn.rebuild(wrapper.inner), self, component_name)
}
}
impl #original_impl_generics dioxus_core::prelude::Properties for #name #ty_generics #where_clause {
type Builder = ();
fn builder() -> Self::Builder {
unreachable!()
}
fn memoize(&mut self, new: &Self) -> bool {
self.inner.memoize(&new.inner)
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
inner: #original_name {
#( #field_names ),*
},
owner: self.owner,
}
}
}
}
} else {
quote!(
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
#( #field_names ),*
}
}
}
)
}
}
}
#[derive(Debug, Default)]
pub struct TypeBuilderAttr {
/// Whether to show docs for the `TypeBuilder` type (rather than hiding them).
pub doc: bool,
/// Docs on the `Type::builder()` method.
pub builder_method_doc: Option<syn::Expr>,
/// Docs on the `TypeBuilder` type. Specifying this implies `doc`, but you can just specify
/// `doc` instead and a default value will be filled in here.
pub builder_type_doc: Option<syn::Expr>,
/// Docs on the `TypeBuilder.build()` method. Specifying this implies `doc`, but you can just
/// specify `doc` instead and a default value will be filled in here.
pub build_method_doc: Option<syn::Expr>,
pub field_defaults: FieldBuilderAttr,
}
impl TypeBuilderAttr {
pub fn new(attrs: &[syn::Attribute]) -> Result<TypeBuilderAttr, Error> {
let mut result = TypeBuilderAttr::default();
for attr in attrs {
if path_to_single_string(attr.path()).as_deref() != Some("builder") {
continue;
}
match &attr.meta {
syn::Meta::List(list) => {
if list.tokens.is_empty() {
continue;
}
}
_ => {
continue;
}
}
let as_expr = attr.parse_args_with(
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
)?;
for expr in as_expr.into_iter() {
result.apply_meta(expr)?;
}
}
Ok(result)
}
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
match expr {
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
"builder_method_doc" => {
self.builder_method_doc = Some(*assign.right);
Ok(())
}
"builder_type_doc" => {
self.builder_type_doc = Some(*assign.right);
self.doc = true;
Ok(())
}
"build_method_doc" => {
self.build_method_doc = Some(*assign.right);
self.doc = true;
Ok(())
}
_ => Err(Error::new_spanned(
&assign,
format!("Unknown parameter {name:?}"),
)),
}
}
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
match name.as_str() {
"doc" => {
self.doc = true;
Ok(())
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown parameter {name:?}"),
)),
}
}
syn::Expr::Call(call) => {
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
path_to_single_string(&path.path)
} else {
None
}
.ok_or_else(|| {
let call_func = &call.func;
let call_func = quote!(#call_func);
Error::new_spanned(
&call.func,
format!("Illegal builder setting group {call_func}"),
)
})?;
match subsetting_name.as_str() {
"field_defaults" => {
for arg in call.args {
self.field_defaults.apply_meta(arg)?;
}
Ok(())
}
_ => Err(Error::new_spanned(
&call.func,
format!("Illegal builder setting group name {subsetting_name}"),
)),
}
}
_ => Err(Error::new_spanned(expr, "Expected (<...>=<...>)")),
}
}
}
}
/// A helper function for paring types with a single generic argument.
fn extract_base_type_without_generics(ty: &Type) -> Option<syn::Path> {
let Type::Path(ty) = ty else {
return None;
};
if ty.qself.is_some() {
return None;
}
let path = &ty.path;
let mut path_segments_without_generics = Vec::new();
let mut generic_arg_count = 0;
for segment in &path.segments {
let mut segment = segment.clone();
match segment.arguments {
PathArguments::AngleBracketed(_) => generic_arg_count += 1,
PathArguments::Parenthesized(_) => {
return None;
}
_ => {}
}
segment.arguments = syn::PathArguments::None;
path_segments_without_generics.push(segment);
}
// If there is more than the type and the single generic argument, it doesn't look like the type we want
if generic_arg_count > 2 {
return None;
}
let path_without_generics = syn::Path {
leading_colon: None,
segments: Punctuated::from_iter(path_segments_without_generics),
};
Some(path_without_generics)
}
/// Returns the type inside the Option wrapper if it exists
fn strip_option(type_: &Type) -> Option<Type> {
if let Type::Path(ty) = &type_ {
let mut segments_iter = ty.path.segments.iter().peekable();
// Strip any leading std||core::option:: prefix
let allowed_segments: &[&[&str]] = &[&["std", "core"], &["option"]];
let mut allowed_segments_iter = allowed_segments.iter();
while let Some(segment) = segments_iter.peek() {
let Some(allowed_segments) = allowed_segments_iter.next() else {
break;
};
if !allowed_segments.contains(&segment.ident.to_string().as_str()) {
break;
}
segments_iter.next();
}
// The last segment should be Option
let option_segment = segments_iter.next()?;
if option_segment.ident == "Option" && segments_iter.next().is_none() {
// It should have a single generic argument
if let PathArguments::AngleBracketed(generic_arg) = &option_segment.arguments {
if let Some(syn::GenericArgument::Type(ty)) = generic_arg.args.first() {
return Some(ty.clone());
}
}
}
}
None
}
/// Remove the Option wrapper from a type
fn remove_option_wrapper(type_: Type) -> Type {
strip_option(&type_).unwrap_or(type_)
}
/// Check if a type should be owned by the child component after conversion
fn child_owned_type(ty: &Type) -> bool {
looks_like_signal_type(ty) || looks_like_callback_type(ty)
}
fn looks_like_signal_type(ty: &Type) -> bool {
match extract_base_type_without_generics(ty) {
Some(path_without_generics) => {
path_without_generics == parse_quote!(dioxus_core::prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(ReadOnlySignal)
}
None => false,
}
}
fn looks_like_callback_type(ty: &Type) -> bool {
let type_without_option = remove_option_wrapper(ty.clone());
match extract_base_type_without_generics(&type_without_option) {
Some(path_without_generics) => {
path_without_generics == parse_quote!(dioxus_core::prelude::EventHandler)
|| path_without_generics == parse_quote!(prelude::EventHandler)
|| path_without_generics == parse_quote!(EventHandler)
|| path_without_generics == parse_quote!(dioxus_core::prelude::Callback)
|| path_without_generics == parse_quote!(prelude::Callback)
|| path_without_generics == parse_quote!(Callback)
}
None => false,
}
}
#[test]
fn test_looks_like_type() {
assert!(!looks_like_signal_type(&parse_quote!(
Option<ReadOnlySignal<i32>>
)));
assert!(looks_like_signal_type(&parse_quote!(ReadOnlySignal<i32>)));
assert!(looks_like_signal_type(
&parse_quote!(ReadOnlySignal<i32, SyncStorage>)
));
assert!(looks_like_signal_type(&parse_quote!(
ReadOnlySignal<Option<i32>, UnsyncStorage>
)));
assert!(looks_like_callback_type(&parse_quote!(
Option<EventHandler>
)));
assert!(looks_like_callback_type(&parse_quote!(
std::option::Option<EventHandler<i32>>
)));
assert!(looks_like_callback_type(&parse_quote!(
Option<EventHandler<MouseEvent>>
)));
assert!(looks_like_callback_type(&parse_quote!(EventHandler<i32>)));
assert!(looks_like_callback_type(&parse_quote!(EventHandler)));
assert!(looks_like_callback_type(&parse_quote!(Callback<i32>)));
assert!(looks_like_callback_type(&parse_quote!(Callback<i32, u32>)));
}
#[test]
fn test_remove_option_wrapper() {
let type_without_option = remove_option_wrapper(parse_quote!(Option<i32>));
assert_eq!(type_without_option, parse_quote!(i32));
let type_without_option = remove_option_wrapper(parse_quote!(Option<Option<i32>>));
assert_eq!(type_without_option, parse_quote!(Option<i32>));
let type_without_option = remove_option_wrapper(parse_quote!(Option<Option<Option<i32>>>));
assert_eq!(type_without_option, parse_quote!(Option<Option<i32>>));
}