use darling::util::Flag;
use darling::{FromAttributes, FromMeta, ToTokens};
use proc_macro2::TokenStream;
use quote::{TokenStreamExt, quote};
use syn::{Attribute, Expr, Fields, ItemStruct};
use crate::helpers::get_docs;
use crate::parsing::{PhpNameContext, PhpRename, RenameRule, ident_to_php_name, validate_php_name};
use crate::prelude::*;
#[derive(FromAttributes, Debug, Default)]
#[darling(attributes(php), forward_attrs(doc), default)]
pub struct StructAttributes {
#[darling(flatten)]
rename: PhpRename,
modifier: Option<syn::Ident>,
flags: Option<syn::Expr>,
#[darling(rename = "readonly")]
readonly: Flag,
extends: Option<ClassEntryAttribute>,
#[darling(multiple)]
implements: Vec<ClassEntryAttribute>,
attrs: Vec<Attribute>,
}
#[derive(Debug)]
pub enum ClassEntryAttribute {
Explicit { ce: syn::Expr, stub: String },
Type(syn::Path),
}
impl FromMeta for ClassEntryAttribute {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
match item {
syn::Meta::List(list) => {
let tokens_str = list.tokens.to_string();
if tokens_str.contains('=') {
#[derive(FromMeta)]
struct ExplicitForm {
ce: syn::Expr,
stub: String,
}
let explicit: ExplicitForm = FromMeta::from_meta(item)?;
Ok(ClassEntryAttribute::Explicit {
ce: explicit.ce,
stub: explicit.stub,
})
} else {
let path: syn::Path = list.parse_args().map_err(|e| {
darling::Error::custom(format!(
"Expected a type path (e.g., `MyClass`) or explicit form \
(e.g., `ce = expr, stub = \"Name\"`): {e}"
))
})?;
Ok(ClassEntryAttribute::Type(path))
}
}
_ => Err(darling::Error::unsupported_format("expected list format")),
}
}
}
impl ToTokens for ClassEntryAttribute {
fn to_tokens(&self, tokens: &mut TokenStream) {
let token = match self {
ClassEntryAttribute::Explicit { ce, stub } => {
quote! { (#ce, #stub) }
}
ClassEntryAttribute::Type(path) => {
quote! {
(
|| <#path as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
<#path as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
)
}
}
};
tokens.append_all(token);
}
}
pub fn parser(mut input: ItemStruct) -> Result<TokenStream> {
let attr = StructAttributes::from_attributes(&input.attrs)?;
let ident = &input.ident;
let name = attr
.rename
.rename(ident_to_php_name(ident), RenameRule::Pascal);
validate_php_name(&name, PhpNameContext::Class, ident.span())?;
let docs = get_docs(&attr.attrs)?;
let has_derive_default = input.attrs.iter().any(|attr| {
if attr.path().is_ident("derive")
&& let Ok(nested) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
)
{
return nested.iter().any(|path| path.is_ident("Default"));
}
false
});
let has_derive_clone = input.attrs.iter().any(|attr| {
if attr.path().is_ident("derive")
&& let Ok(nested) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
)
{
return nested.iter().any(|path| path.is_ident("Clone"));
}
false
});
input.attrs.retain(|attr| !attr.path().is_ident("php"));
let fields = match &mut input.fields {
Fields::Named(fields) => parse_fields(fields.named.iter_mut())?,
_ => vec![],
};
let class_impl = generate_registered_class_impl(
ident,
&name,
attr.modifier.as_ref(),
attr.extends.as_ref(),
&attr.implements,
&fields,
attr.flags.as_ref(),
attr.readonly.is_present(),
&docs,
has_derive_default,
has_derive_clone,
);
Ok(quote! {
#input
#class_impl
::ext_php_rs::class_derives!(#ident);
})
}
#[derive(FromAttributes, Debug, Default)]
#[darling(attributes(php), forward_attrs(doc), default)]
struct PropAttributes {
prop: Flag,
#[darling(rename = "static")]
static_: Flag,
#[darling(flatten)]
rename: PhpRename,
flags: Option<Expr>,
default: Option<Expr>,
attrs: Vec<Attribute>,
}
fn parse_fields<'a>(fields: impl Iterator<Item = &'a mut syn::Field>) -> Result<Vec<Property<'a>>> {
let mut result = vec![];
for field in fields {
let attr = PropAttributes::from_attributes(&field.attrs)?;
if attr.prop.is_present() {
let ident = field
.ident
.as_ref()
.ok_or_else(|| err!("Only named fields can be properties."))?;
let docs = get_docs(&attr.attrs)?;
field.attrs.retain(|attr| !attr.path().is_ident("php"));
let name = attr
.rename
.rename(ident_to_php_name(ident), RenameRule::Camel);
validate_php_name(&name, PhpNameContext::Property, ident.span())?;
result.push(Property {
ident,
ty: &field.ty,
name,
attr,
docs,
});
}
}
Ok(result)
}
#[derive(Debug)]
struct Property<'a> {
pub ident: &'a syn::Ident,
pub ty: &'a syn::Type,
pub name: String,
pub attr: PropAttributes,
pub docs: Vec<String>,
}
impl Property<'_> {
pub fn is_static(&self) -> bool {
self.attr.static_.is_present()
}
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn generate_registered_class_impl(
ident: &syn::Ident,
class_name: &str,
modifier: Option<&syn::Ident>,
extends: Option<&ClassEntryAttribute>,
implements: &[ClassEntryAttribute],
fields: &[Property],
flags: Option<&syn::Expr>,
readonly: bool,
docs: &[String],
has_derive_default: bool,
has_derive_clone: bool,
) -> TokenStream {
let modifier = modifier.option_tokens();
let (instance_props, static_props): (Vec<_>, Vec<_>) =
fields.iter().partition(|prop| !prop.is_static());
let field_prop_count = instance_props.len();
let field_prop_data: Vec<(TokenStream, TokenStream)> = instance_props
.iter()
.enumerate()
.map(|(i, prop)| {
let name = &prop.name;
let field_ident = prop.ident;
let field_ty = prop.ty;
let flags = prop
.attr
.flags
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
let docs = &prop.docs;
let getter_name = syn::Ident::new(&format!("__prop_get_{i}"), field_ident.span());
let setter_name = syn::Ident::new(&format!("__prop_set_{i}"), field_ident.span());
let fns = quote! {
fn #getter_name(
this: &#ident,
__zv: &mut ::ext_php_rs::types::Zval,
) -> ::ext_php_rs::exception::PhpResult {
use ::ext_php_rs::convert::IntoZval as _;
this.#field_ident.clone().set_zval(__zv, false)
.map_err(|e| format!("Failed to get property value: {e:?}"))?;
Ok(())
}
fn #setter_name(
this: &mut #ident,
__zv: &::ext_php_rs::types::Zval,
) -> ::ext_php_rs::exception::PhpResult {
use ::ext_php_rs::convert::FromZval as _;
let val = <#field_ty as ::ext_php_rs::convert::FromZval>::from_zval(__zv)
.ok_or_else(|| {
let ty = __zv.get_type();
format!("Failed to set property: could not convert from {ty:?}")
})?;
this.#field_ident = val;
Ok(())
}
};
let descriptor = quote! {
::ext_php_rs::internal::property::PropertyDescriptor {
name: #name,
get: ::std::option::Option::Some(#getter_name),
set: ::std::option::Option::Some(#setter_name),
flags: #flags,
docs: &[#(#docs,)*],
ty: <#field_ty as ::ext_php_rs::convert::IntoZval>::TYPE,
nullable: <#field_ty as ::ext_php_rs::convert::IntoZval>::NULLABLE,
readonly: false,
}
};
(fns, descriptor)
})
.collect();
let field_prop_fns: Vec<&TokenStream> = field_prop_data.iter().map(|(f, _)| f).collect();
let field_prop_descriptors: Vec<&TokenStream> =
field_prop_data.iter().map(|(_, d)| d).collect();
let static_fields = static_props.iter().map(|prop| {
let name = &prop.name;
let base_flags = prop
.attr
.flags
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
let docs = &prop.docs;
let default_value = if let Some(expr) = &prop.attr.default {
quote! { ::std::option::Option::Some(&#expr as &'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)) }
} else {
quote! { ::std::option::Option::None }
};
quote! {
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
), #default_value, &[#(#docs,)*] as &[&str])
}
});
let flags = match (flags, readonly) {
(Some(flags), true) => {
quote! {
{
#[cfg(not(php82))]
compile_error!("The `readonly` class attribute requires PHP 8.2 or later");
#[cfg(php82)]
{
::ext_php_rs::flags::ClassFlags::from_bits_retain(
(#flags).bits() | ::ext_php_rs::flags::ClassFlags::ReadonlyClass.bits()
)
}
#[cfg(not(php82))]
{ #flags }
}
}
}
(Some(flags), false) => flags.to_token_stream(),
(None, true) => {
quote! {
{
#[cfg(not(php82))]
compile_error!("The `readonly` class attribute requires PHP 8.2 or later");
#[cfg(php82)]
{ ::ext_php_rs::flags::ClassFlags::ReadonlyClass }
#[cfg(not(php82))]
{ ::ext_php_rs::flags::ClassFlags::empty() }
}
}
}
(None, false) => quote! { ::ext_php_rs::flags::ClassFlags::empty() },
};
let docs = quote! {
#(#docs,)*
};
let extends = if let Some(extends) = extends {
quote! {
Some(#extends)
}
} else {
quote! { None }
};
let implements = implements.iter().map(|imp| {
quote! { #imp }
});
let default_init_impl = generate_default_init_impl(ident, has_derive_default);
let clone_obj_impl = generate_clone_obj_impl(ident, has_derive_clone);
quote! {
impl ::ext_php_rs::class::RegisteredClass for #ident {
const CLASS_NAME: &'static str = #class_name;
const BUILDER_MODIFIER: ::std::option::Option<
fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder
> = #modifier;
const EXTENDS: ::std::option::Option<
::ext_php_rs::class::ClassEntryInfo
> = #extends;
const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[
#(#implements,)*
];
const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags;
const DOC_COMMENTS: &'static [&'static str] = &[
#docs
];
#[inline]
fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> {
#(#field_prop_fns)*
static FIELD_PROPS: [
::ext_php_rs::internal::property::PropertyDescriptor<#ident>; #field_prop_count
] = [
#(#field_prop_descriptors,)*
];
static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> =
::ext_php_rs::class::ClassMetadata::new(&FIELD_PROPS);
&METADATA
}
#[must_use]
fn static_properties() -> &'static [(&'static str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &'static [&'static str])] {
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str])] = &[#(#static_fields,)*];
STATIC_PROPS
}
#[inline]
fn method_properties() -> &'static [::ext_php_rs::internal::property::PropertyDescriptor<Self>] {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_method_props()
}
#[inline]
fn method_builders() -> ::std::vec::Vec<
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
> {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_methods()
}
#[inline]
fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<Self>> {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constructor()
}
#[inline]
fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] {
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
}
#[inline]
fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> {
let my_type_id = ::std::any::TypeId::of::<Self>();
::ext_php_rs::inventory::iter::<::ext_php_rs::internal::class::InterfaceRegistration>()
.filter(|reg| reg.class_type_id == my_type_id)
.map(|reg| (reg.interface_getter)())
.collect()
}
#[inline]
fn interface_method_implementations() -> ::std::vec::Vec<(
::ext_php_rs::builders::FunctionBuilder<'static>,
::ext_php_rs::flags::MethodFlags,
)> {
use ::ext_php_rs::internal::class::InterfaceMethodsProvider;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_interface_methods()
}
#default_init_impl
#clone_obj_impl
}
}
}
fn generate_clone_obj_impl(_ident: &syn::Ident, has_derive_clone: bool) -> TokenStream {
if has_derive_clone {
quote! {
#[inline]
#[must_use]
fn clone_obj(&self) -> ::std::option::Option<Self> {
::std::option::Option::Some(::std::clone::Clone::clone(self))
}
}
} else {
quote! {}
}
}
fn generate_default_init_impl(ident: &syn::Ident, has_derive_default: bool) -> TokenStream {
if has_derive_default {
quote! {
#[inline]
#[must_use]
fn default_init() -> ::std::option::Option<Self> {
::std::option::Option::Some(<#ident as ::std::default::Default>::default())
}
}
} else {
quote! {}
}
}