use darling::FromAttributes;
use darling::util::{Flag, SpannedValue};
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::{BTreeSet, HashMap};
use syn::{Expr, Ident, ItemImpl};
use crate::constant::PhpConstAttribute;
use crate::function::{Args, CallType, Function, MethodReceiver};
use crate::helpers::get_docs;
use crate::parsing::{
NameSet, PhpNameContext, PhpRename, RenameRule, Visibility, ident_to_php_name,
reject_php_attrs, validate_php_name,
};
use crate::prelude::*;
#[derive(Debug)]
enum MethodTy {
Normal,
Constructor,
Getter,
Setter,
Abstract,
}
#[derive(FromAttributes, Debug, Default)]
#[darling(attributes(php), default)]
pub struct PhpImpl {
change_method_case: Option<RenameRule>,
change_constant_case: Option<RenameRule>,
}
pub fn parser(mut input: ItemImpl) -> Result<TokenStream> {
let args = PhpImpl::from_attributes(&input.attrs)?;
input.attrs.retain(|attr| !attr.path().is_ident("php"));
let path = match &*input.self_ty {
syn::Type::Path(ty) => &ty.path,
_ => {
bail!(input.self_ty => "The `#[php_impl]` attribute is only valid for struct implementations.")
}
};
let mut parsed = ParsedImpl::new(
path,
args.change_method_case.unwrap_or(RenameRule::Camel),
args.change_constant_case
.unwrap_or(RenameRule::ScreamingSnake),
);
parsed.parse(input.items.iter_mut())?;
let php_class_impl = parsed.generate_php_class_impl();
Ok(quote::quote! {
#input
#php_class_impl
})
}
#[derive(Debug)]
struct MethodArgs {
name: String,
optional: Option<Ident>,
defaults: HashMap<Ident, Expr>,
vis: Visibility,
vis_span: Option<proc_macro2::Span>,
ty: MethodTy,
is_final: bool,
}
#[derive(FromAttributes, Default, Debug)]
#[darling(default, attributes(php), forward_attrs(doc))]
pub struct PhpFunctionImplAttribute {
#[darling(flatten)]
rename: PhpRename,
defaults: HashMap<Ident, Expr>,
optional: Option<Ident>,
vis: Option<SpannedValue<Visibility>>,
attrs: Vec<syn::Attribute>,
getter: Flag,
setter: Flag,
constructor: Flag,
#[darling(rename = "abstract")]
abstract_method: Flag,
#[darling(rename = "final")]
final_method: Flag,
}
impl MethodArgs {
#[allow(clippy::similar_names)]
fn new(name: String, attr: PhpFunctionImplAttribute) -> Result<Self> {
let is_constructor = name == "__construct" || attr.constructor.is_present();
let is_getter = attr.getter.is_present();
let is_setter = attr.setter.is_present();
let is_abstract = attr.abstract_method.is_present();
let is_final = attr.final_method.is_present();
let abstract_span = attr.abstract_method.span();
let final_span = attr.final_method.span();
if is_getter && is_setter {
bail!(attr.setter.span() => "A method cannot be both a getter and a setter.");
}
if is_constructor {
if is_abstract {
bail!(abstract_span => "Constructors cannot be abstract.");
}
if is_final {
bail!(final_span => "Constructors cannot be final.");
}
}
if is_getter {
if is_abstract {
bail!(abstract_span => "Getters cannot be abstract.");
}
if is_final {
bail!(final_span => "Getters cannot be final.");
}
}
if is_setter {
if is_abstract {
bail!(abstract_span => "Setters cannot be abstract.");
}
if is_final {
bail!(final_span => "Setters cannot be final.");
}
}
if is_abstract {
if is_final {
bail!(final_span => "Methods cannot be both abstract and final.");
}
if let Some(vis) = attr.vis.filter(|vis| **vis == Visibility::Private) {
bail!(vis.span() => "Abstract methods cannot be private.");
}
}
let ty = if is_constructor {
MethodTy::Constructor
} else if is_getter {
MethodTy::Getter
} else if is_setter {
MethodTy::Setter
} else if is_abstract {
MethodTy::Abstract
} else {
MethodTy::Normal
};
if let Some(kind) = match ty {
MethodTy::Getter => Some("Getters"),
MethodTy::Setter => Some("Setters"),
_ => None,
} {
let mut keys: Vec<&Ident> = attr.defaults.keys().collect();
keys.sort();
if let Some(key) = keys.first() {
bail!(key => "{kind} cannot have `defaults`; they map to a PHP property.");
}
if let Some(optional) = &attr.optional {
bail!(optional => "{kind} cannot have `optional`; they map to a PHP property.");
}
}
Ok(Self {
name,
optional: attr.optional,
defaults: attr.defaults,
vis_span: attr.vis.as_ref().map(SpannedValue::span),
vis: attr.vis.map_or(Visibility::Public, |vis| *vis),
ty,
is_final,
})
}
}
#[derive(Debug)]
struct PropGroup<'a> {
name: String,
getter: Option<&'a syn::Ident>,
setter: Option<&'a syn::Ident>,
vis: Visibility,
docs: Vec<String>,
value_ty: Option<&'a syn::Type>,
}
#[derive(Debug)]
struct ParsedImpl<'a> {
path: &'a syn::Path,
change_method_case: RenameRule,
change_constant_case: RenameRule,
functions: Vec<FnBuilder>,
constructor: Option<(Function<'a>, Option<Visibility>)>,
constants: Vec<Constant<'a>>,
has_abstract_methods: bool,
properties: Vec<PropGroup<'a>>,
method_names: NameSet,
constant_names: NameSet,
}
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum MethodModifier {
Abstract,
Static,
Final,
}
impl quote::ToTokens for MethodModifier {
fn to_tokens(&self, tokens: &mut TokenStream) {
match *self {
Self::Abstract => quote! { ::ext_php_rs::flags::MethodFlags::Abstract },
Self::Static => quote! { ::ext_php_rs::flags::MethodFlags::Static },
Self::Final => quote! { ::ext_php_rs::flags::MethodFlags::Final },
}
.to_tokens(tokens);
}
}
#[derive(Debug)]
pub struct FnBuilder {
pub builder: TokenStream,
pub vis: Visibility,
pub modifiers: BTreeSet<MethodModifier>,
}
#[derive(Debug)]
pub struct Constant<'a> {
pub name: String,
pub ident: &'a syn::Ident,
pub docs: Vec<String>,
pub vis: Visibility,
}
impl<'a> ParsedImpl<'a> {
fn new(path: &'a syn::Path, rename_methods: RenameRule, rename_constants: RenameRule) -> Self {
Self {
path,
change_method_case: rename_methods,
change_constant_case: rename_constants,
functions: Vec::default(),
constructor: Option::default(),
constants: Vec::default(),
has_abstract_methods: false,
properties: Vec::default(),
method_names: NameSet::case_insensitive(),
constant_names: NameSet::case_sensitive(),
}
}
fn parse_property_method(
&mut self,
method: &'a syn::ImplItemFn,
opts: &MethodArgs,
rename: &PhpRename,
docs: Vec<String>,
) -> Result<()> {
let is_getter = matches!(opts.ty, MethodTy::Getter);
let method_name = ident_to_php_name(&method.sig.ident);
let prefix = if is_getter { "get_" } else { "set_" };
let prop_name = rename.rename(
method_name.strip_prefix(prefix).unwrap_or(&method_name),
RenameRule::Camel,
);
validate_php_name(
&prop_name,
PhpNameContext::Property,
method.sig.ident.span(),
)?;
let value_ty = match (is_getter, &method.sig.output) {
(true, syn::ReturnType::Type(_, ty)) => Some(ty.as_ref()),
(false, _) => method.sig.inputs.iter().nth(1).and_then(|arg| match arg {
syn::FnArg::Typed(pat) => Some(pat.ty.as_ref()),
syn::FnArg::Receiver(_) => None,
}),
_ => None,
};
let idx = self
.properties
.iter()
.position(|g| g.name == prop_name)
.unwrap_or_else(|| {
self.properties.push(PropGroup {
name: prop_name,
getter: None,
setter: None,
vis: opts.vis,
docs: Vec::new(),
value_ty: None,
});
self.properties.len() - 1
});
let group = &mut self.properties[idx];
let kind = if is_getter { "getter" } else { "setter" };
let slot = if is_getter {
&mut group.getter
} else {
&mut group.setter
};
if slot.is_some() {
bail!(method.sig.ident => "Property `{}` already has a {kind}.", group.name);
}
if group.vis != opts.vis {
let span = opts.vis_span.unwrap_or_else(|| method.sig.ident.span());
bail!(span => "The getter and setter of property `{}` must have the same visibility.", group.name);
}
*slot = Some(&method.sig.ident);
if (is_getter && value_ty.is_some()) || group.value_ty.is_none() {
group.value_ty = value_ty;
}
if (is_getter && !docs.is_empty()) || group.docs.is_empty() {
group.docs = docs;
}
Ok(())
}
fn parse(&mut self, items: impl Iterator<Item = &'a mut syn::ImplItem>) -> Result<()> {
for items in items {
match items {
syn::ImplItem::Const(c) => {
let attr = PhpConstAttribute::from_attributes(&c.attrs)?;
let name = attr
.rename
.rename(ident_to_php_name(&c.ident), self.change_constant_case);
validate_php_name(&name, PhpNameContext::Constant, c.ident.span())?;
if self.constant_names.insert(&name).is_err() {
bail!(c.ident => "PHP constant `{name}` is already declared in this block. Rename one of them with `#[php(name = \"...\")]`.");
}
let docs = get_docs(&attr.attrs)?;
c.attrs.retain(|attr| !attr.path().is_ident("php"));
self.constants.push(Constant {
name,
ident: &c.ident,
docs,
vis: attr.vis.map_or(Visibility::Public, |vis| *vis),
});
}
syn::ImplItem::Fn(method) => {
let mut attr = PhpFunctionImplAttribute::from_attributes(&method.attrs)?;
let name = attr.rename.rename_method(
ident_to_php_name(&method.sig.ident),
self.change_method_case,
);
validate_php_name(&name, PhpNameContext::Method, method.sig.ident.span())?;
let docs = get_docs(&attr.attrs)?;
method.attrs.retain(|attr| !attr.path().is_ident("php"));
let rename = std::mem::take(&mut attr.rename);
let opts = MethodArgs::new(name, attr)?;
if matches!(opts.ty, MethodTy::Getter | MethodTy::Setter) {
self.parse_property_method(method, &opts, &rename, docs)?;
continue;
}
let php_name = if matches!(opts.ty, MethodTy::Constructor) {
"__construct"
} else {
opts.name.as_str()
};
if let Err(earlier) = self.method_names.insert(php_name) {
bail!(method.sig.ident => "PHP method `{php_name}` is already declared as `{earlier}` in this block. PHP method names ignore case. Rename one of them with `#[php(name = \"...\")]`.");
}
let is_constructor = matches!(opts.ty, MethodTy::Constructor);
let mut args =
Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?;
let receiver = if args.receiver.is_some() {
MethodReceiver::Class
} else if !is_constructor && args.take_self_object()? {
MethodReceiver::ZendClassObject
} else {
MethodReceiver::Static
};
let func = Function::new(&method.sig, opts.name, args, opts.optional, docs)?;
let mut modifiers: BTreeSet<MethodModifier> = BTreeSet::new();
if is_constructor {
if self.constructor.replace((func, opts.vis.into())).is_some() {
bail!(method => "Only one constructor can be provided per class.");
}
} else {
if matches!(receiver, MethodReceiver::Static) {
modifiers.insert(MethodModifier::Static);
}
let call_type = CallType::Method {
class: self.path,
receiver,
};
let is_abstract = matches!(opts.ty, MethodTy::Abstract);
if is_abstract {
modifiers.insert(MethodModifier::Abstract);
self.has_abstract_methods = true;
}
if opts.is_final {
modifiers.insert(MethodModifier::Final);
}
let builder = if is_abstract {
func.abstract_function_builder()
} else {
func.function_builder(&call_type)
};
self.functions.push(FnBuilder {
builder,
vis: opts.vis,
modifiers,
});
}
}
syn::ImplItem::Type(t) => {
reject_php_attrs(&t.attrs, "associated types")?;
}
_ => {}
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn generate_php_class_impl(&self) -> TokenStream {
let path = &self.path;
let functions = &self.functions;
let constructor = self
.constructor
.as_ref()
.map(|(func, vis)| func.constructor_meta(self.path, vis.as_ref()))
.option_tokens();
let constants = self.constants.iter().map(|c| {
let name = &c.name;
let ident = c.ident;
let docs = &c.docs;
let flags = c
.vis
.flag_tokens("e! { ::ext_php_rs::flags::ConstantFlags });
quote! {
(#name, &#path::#ident, &[#(#docs),*], #flags)
}
});
let abstract_check = if self.has_abstract_methods {
quote! {
const _: () = assert!(
<#path as ::ext_php_rs::class::RegisteredClass>::FLAGS
.contains(::ext_php_rs::flags::ClassFlags::Abstract),
"Abstract methods can only be defined in abstract classes. \
Add `#[php(flags = ClassFlags::Abstract)]` to the class definition."
);
}
} else {
quote! {}
};
let method_prop_data: Vec<(Vec<TokenStream>, TokenStream)> = self
.properties
.iter()
.enumerate()
.map(|(i, group)| {
let prop_name = &group.name;
let flags = match group.vis {
Visibility::Public => quote! { ::ext_php_rs::flags::PropertyFlags::Public },
Visibility::Protected => {
quote! { ::ext_php_rs::flags::PropertyFlags::Protected }
}
Visibility::Private => quote! { ::ext_php_rs::flags::PropertyFlags::Private },
};
let docs = &group.docs;
let readonly = group.getter.is_some() && group.setter.is_none();
let type_tokens = group.value_ty.map_or_else(
|| {
quote! {
ty: ::ext_php_rs::flags::DataType::Mixed,
nullable: false,
}
},
|vty| {
quote! {
ty: <#vty as ::ext_php_rs::convert::IntoZval>::TYPE,
nullable: <#vty as ::ext_php_rs::convert::IntoZval>::NULLABLE,
}
},
);
let mut fn_defs = Vec::new();
let getter_ref = if let Some(getter_ident) = group.getter {
let getter_fn_name =
syn::Ident::new(&format!("__method_get_{i}"), getter_ident.span());
fn_defs.push(quote! {
fn #getter_fn_name(
this: &#path,
__zv: &mut ::ext_php_rs::types::Zval,
) -> ::ext_php_rs::exception::PhpResult {
use ::ext_php_rs::convert::IntoZval as _;
let value = #path::#getter_ident(this);
value.set_zval(__zv, false)
.map_err(|e| format!("Failed to return property value: {e:?}"))?;
Ok(())
}
});
quote! { ::std::option::Option::Some(#getter_fn_name) }
} else {
quote! { ::std::option::Option::None }
};
let setter_ref = if let Some(setter_ident) = group.setter {
let setter_fn_name =
syn::Ident::new(&format!("__method_set_{i}"), setter_ident.span());
if let Some(vty) = group.value_ty {
fn_defs.push(quote! {
fn #setter_fn_name(
this: &mut #path,
__zv: &::ext_php_rs::types::Zval,
) -> ::ext_php_rs::exception::PhpResult {
use ::ext_php_rs::convert::FromZval as _;
let val = <#vty as ::ext_php_rs::convert::FromZval>::from_zval(__zv)
.ok_or("Unable to convert property value into required type.")?;
#path::#setter_ident(this, val);
Ok(())
}
});
} else {
fn_defs.push(quote! {
fn #setter_fn_name(
this: &mut #path,
__zv: &::ext_php_rs::types::Zval,
) -> ::ext_php_rs::exception::PhpResult {
use ::ext_php_rs::convert::FromZval as _;
let val = ::ext_php_rs::convert::FromZval::from_zval(__zv)
.ok_or("Unable to convert property value into required type.")?;
#path::#setter_ident(this, val);
Ok(())
}
});
}
quote! { ::std::option::Option::Some(#setter_fn_name) }
} else {
quote! { ::std::option::Option::None }
};
let descriptor = quote! {
::ext_php_rs::internal::property::PropertyDescriptor {
name: #prop_name,
get: #getter_ref,
set: #setter_ref,
flags: #flags,
docs: &[#(#docs),*],
#type_tokens
readonly: #readonly,
}
};
(fn_defs, descriptor)
})
.collect();
let method_prop_fn_defs: Vec<&TokenStream> = method_prop_data
.iter()
.flat_map(|(fns, _)| fns.iter())
.collect();
let method_prop_descriptors: Vec<&TokenStream> =
method_prop_data.iter().map(|(_, d)| d).collect();
let method_prop_count = method_prop_descriptors.len();
quote! {
#abstract_check
impl ::ext_php_rs::internal::class::PhpClassImpl<#path>
for ::ext_php_rs::internal::class::PhpClassImplCollector<#path>
{
fn get_methods(self) -> ::std::vec::Vec<
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
> {
vec![#(#functions),*]
}
fn get_method_props(self) -> &'static [::ext_php_rs::internal::property::PropertyDescriptor<#path>] {
#(#method_prop_fn_defs)*
static METHOD_PROPS: [
::ext_php_rs::internal::property::PropertyDescriptor<#path>; #method_prop_count
] = [
#(#method_prop_descriptors,)*
];
&METHOD_PROPS
}
fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> {
#constructor
}
fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str], ::ext_php_rs::flags::ConstantFlags)] {
&[#(#constants),*]
}
}
}
}
}
impl quote::ToTokens for FnBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
let builder = &self.builder;
let mut flags = vec![];
let vis = &self.vis;
flags.push(quote! { #vis });
for flag in &self.modifiers {
flags.push(quote! { #flag });
}
quote! {
(#builder, #(#flags)|*)
}
.to_tokens(tokens);
}
}
#[cfg(test)]
mod tests {
use quote::{ToTokens, quote};
use super::{BTreeSet, FnBuilder, MethodModifier};
use crate::parsing::Visibility;
#[test]
fn modifiers_render_in_a_fixed_order() {
let render = |inserted: [MethodModifier; 2]| {
FnBuilder {
builder: quote! { b },
vis: Visibility::Public,
modifiers: inserted.into_iter().collect::<BTreeSet<_>>(),
}
.to_token_stream()
.to_string()
};
let expected = quote! {
(b, ::ext_php_rs::flags::MethodFlags::Public
| ::ext_php_rs::flags::MethodFlags::Abstract
| ::ext_php_rs::flags::MethodFlags::Static)
}
.to_string();
assert_eq!(
render([MethodModifier::Static, MethodModifier::Abstract]),
expected
);
assert_eq!(
render([MethodModifier::Abstract, MethodModifier::Static]),
expected
);
}
}