Skip to content

Commit 31579a5

Browse files
committed
fix(stubs): Missing property types #184
1 parent 08e3a47 commit 31579a5

8 files changed

Lines changed: 238 additions & 27 deletions

File tree

crates/macros/src/class.rs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,63 @@ use darling::util::Flag;
22
use darling::{FromAttributes, FromMeta, ToTokens};
33
use proc_macro2::TokenStream;
44
use quote::{TokenStreamExt, quote};
5-
use syn::{Attribute, Expr, Fields, ItemStruct};
5+
use syn::{Attribute, Expr, Fields, GenericArgument, ItemStruct, PathArguments, Type};
66

77
use crate::helpers::get_docs;
8+
9+
/// Check if a type is `Option<T>` and return the inner type if so.
10+
fn is_option_type(ty: &Type) -> Option<&Type> {
11+
let Type::Path(type_path) = ty else {
12+
return None;
13+
};
14+
if type_path.qself.is_some() {
15+
return None;
16+
}
17+
let segments = &type_path.path.segments;
18+
if segments.len() != 1 {
19+
return None;
20+
}
21+
let segment = &segments[0];
22+
if segment.ident != "Option" {
23+
return None;
24+
}
25+
let PathArguments::AngleBracketed(args) = &segment.arguments else {
26+
return None;
27+
};
28+
if args.args.len() != 1 {
29+
return None;
30+
}
31+
if let GenericArgument::Type(inner) = &args.args[0] {
32+
return Some(inner);
33+
}
34+
None
35+
}
36+
37+
/// Convert an expression to a PHP-compatible default string for stub generation.
38+
fn expr_to_php_default_string(expr: &Expr) -> String {
39+
// For simple literals, we can convert them directly
40+
// For complex expressions, we use a string representation
41+
match expr {
42+
Expr::Lit(lit) => match &lit.lit {
43+
syn::Lit::Str(s) => format!("'{}'", s.value().replace('\'', "\\'")),
44+
syn::Lit::Int(i) => i.to_string(),
45+
syn::Lit::Float(f) => f.to_string(),
46+
syn::Lit::Bool(b) => if b.value { "true" } else { "false" }.to_string(),
47+
_ => expr.to_token_stream().to_string(),
48+
},
49+
Expr::Array(_) => "[]".to_string(),
50+
Expr::Path(path) => {
51+
// Handle constants like `None`, `true`, `false`
52+
let path_str = path.to_token_stream().to_string();
53+
if path_str == "None" {
54+
"null".to_string()
55+
} else {
56+
path_str
57+
}
58+
}
59+
_ => expr.to_token_stream().to_string(),
60+
}
61+
}
862
use crate::parsing::{PhpNameContext, PhpRename, RenameRule, ident_to_php_name, validate_php_name};
963
use crate::prelude::*;
1064

@@ -192,6 +246,7 @@ fn parse_fields<'a>(fields: impl Iterator<Item = &'a mut syn::Field>) -> Result<
192246

193247
result.push(Property {
194248
ident,
249+
ty: &field.ty,
195250
name,
196251
attr,
197252
docs,
@@ -205,6 +260,7 @@ fn parse_fields<'a>(fields: impl Iterator<Item = &'a mut syn::Field>) -> Result<
205260
#[derive(Debug)]
206261
struct Property<'a> {
207262
pub ident: &'a syn::Ident,
263+
pub ty: &'a syn::Type,
208264
pub name: String,
209265
pub attr: PropAttributes,
210266
pub docs: Vec<String>,
@@ -240,6 +296,7 @@ fn generate_registered_class_impl(
240296
let instance_fields = instance_props.iter().map(|prop| {
241297
let name = &prop.name;
242298
let field_ident = prop.ident;
299+
let field_ty = prop.ty;
243300
let flags = prop
244301
.attr
245302
.flags
@@ -248,11 +305,25 @@ fn generate_registered_class_impl(
248305
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
249306
let docs = &prop.docs;
250307

308+
// Determine if the property is nullable (type is Option<T>)
309+
let nullable = is_option_type(field_ty).is_some();
310+
311+
// Get the default value as a PHP-compatible string for stub generation
312+
let default_str = if let Some(default_expr) = &prop.attr.default {
313+
let s = expr_to_php_default_string(default_expr);
314+
quote! { ::std::option::Option::Some(#s) }
315+
} else {
316+
quote! { ::std::option::Option::None }
317+
};
318+
251319
quote! {
252320
(#name, ::ext_php_rs::internal::property::PropertyInfo {
253321
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#field_ident),
254322
flags: #flags,
255-
docs: &[#(#docs,)*]
323+
docs: &[#(#docs,)*],
324+
ty: ::std::option::Option::Some(<#field_ty as ::ext_php_rs::convert::IntoZval>::TYPE),
325+
nullable: #nullable,
326+
default: #default_str,
256327
})
257328
}
258329
});
@@ -262,6 +333,7 @@ fn generate_registered_class_impl(
262333
// const
263334
let static_fields = static_props.iter().map(|prop| {
264335
let name = &prop.name;
336+
let field_ty = prop.ty;
265337
let base_flags = prop
266338
.attr
267339
.flags
@@ -277,11 +349,23 @@ fn generate_registered_class_impl(
277349
quote! { ::std::option::Option::None }
278350
};
279351

352+
// Determine if the property is nullable (type is Option<T>)
353+
let nullable = is_option_type(field_ty).is_some();
354+
355+
// Get the default value as a PHP-compatible string for stub generation
356+
let default_str = if let Some(default_expr) = &prop.attr.default {
357+
let s = expr_to_php_default_string(default_expr);
358+
quote! { ::std::option::Option::Some(#s) }
359+
} else {
360+
quote! { ::std::option::Option::None }
361+
};
362+
280363
// Use from_bits_retain to combine flags in a const context
364+
// Tuple: (name, flags, default_value, docs, type, nullable, default_str)
281365
quote! {
282366
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
283367
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
284-
), #default_value, &[#(#docs,)*] as &[&str])
368+
), #default_value, &[#(#docs,)*] as &[&str], ::std::option::Option::Some(<#field_ty as ::ext_php_rs::convert::IntoZval>::TYPE), #nullable, #default_str)
285369
}
286370
});
287371

@@ -391,8 +475,9 @@ fn generate_registered_class_impl(
391475
}
392476

393477
#[must_use]
394-
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])] {
395-
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str])] = &[#(#static_fields,)*];
478+
#[allow(clippy::type_complexity)]
479+
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], ::std::option::Option<::ext_php_rs::flags::DataType>, bool, ::std::option::Option<&'static str>)] {
480+
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str], ::std::option::Option<::ext_php_rs::flags::DataType>, bool, ::std::option::Option<&'static str>)] = &[#(#static_fields,)*];
396481
STATIC_PROPS
397482
}
398483

crates/macros/src/impl_.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,11 @@ impl<'a> ParsedImpl<'a> {
479479
prop: #prop_expr,
480480
flags: #flags,
481481
docs: &[#(#docs),*],
482+
// Type info not available for getter/setter-based properties
483+
ty: ::std::option::Option::None,
484+
// Nullable and default not available for getter/setter-based properties
485+
nullable: false,
486+
default: ::std::option::Option::None,
482487
}
483488
);
484489
}

crates/macros/tests/expand/class.expanded.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
3737
props
3838
}
3939
#[must_use]
40+
#[allow(clippy::type_complexity)]
4041
fn static_properties() -> &'static [(
4142
&'static str,
4243
::ext_php_rs::flags::PropertyFlags,
4344
::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>,
4445
&'static [&'static str],
46+
::std::option::Option<::ext_php_rs::flags::DataType>,
47+
bool,
48+
::std::option::Option<&'static str>,
4549
)] {
4650
static STATIC_PROPS: &[(
4751
&str,
@@ -50,6 +54,9 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
5054
&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync),
5155
>,
5256
&[&str],
57+
::std::option::Option<::ext_php_rs::flags::DataType>,
58+
bool,
59+
::std::option::Option<&'static str>,
5360
)] = &[];
5461
STATIC_PROPS
5562
}

src/builders/class.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::{
1717
zend_fastcall,
1818
};
1919

20+
use crate::flags::DataType;
21+
2022
/// A constant entry: (name, `value_closure`, docs, `stub_value`)
2123
type ConstantEntry = (
2224
String,
@@ -25,6 +27,16 @@ type ConstantEntry = (
2527
String,
2628
);
2729
type PropertyDefault = Option<Box<dyn FnOnce() -> Result<Zval>>>;
30+
/// A property entry: (name, flags, default, docs, type, nullable, `default_str`).
31+
type PropertyEntry = (
32+
String,
33+
PropertyFlags,
34+
PropertyDefault,
35+
DocComments,
36+
Option<DataType>,
37+
bool,
38+
Option<&'static str>,
39+
);
2840

2941
/// Builder for registering a class in PHP.
3042
#[must_use]
@@ -35,7 +47,7 @@ pub struct ClassBuilder {
3547
pub(crate) interfaces: Vec<ClassEntryInfo>,
3648
pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
3749
object_override: Option<unsafe extern "C" fn(class_type: *mut ClassEntry) -> *mut ZendObject>,
38-
pub(crate) properties: Vec<(String, PropertyFlags, PropertyDefault, DocComments)>,
50+
pub(crate) properties: Vec<PropertyEntry>,
3951
pub(crate) constants: Vec<ConstantEntry>,
4052
register: Option<fn(&'static mut ClassEntry)>,
4153
pub(crate) docs: DocComments,
@@ -114,14 +126,22 @@ impl ClassBuilder {
114126
/// * `flags` - Flags relating to the property. See [`PropertyFlags`].
115127
/// * `default` - Optional default value for the property.
116128
/// * `docs` - Documentation comments for the property.
129+
/// * `ty` - Optional type for stub generation.
130+
/// * `nullable` - Whether the property is nullable.
131+
/// * `default_str` - Default value as PHP string for stub generation.
132+
#[allow(clippy::too_many_arguments)]
117133
pub fn property<T: Into<String>>(
118134
mut self,
119135
name: T,
120136
flags: PropertyFlags,
121137
default: PropertyDefault,
122138
docs: DocComments,
139+
ty: Option<DataType>,
140+
nullable: bool,
141+
default_str: Option<&'static str>,
123142
) -> Self {
124-
self.properties.push((name.into(), flags, default, docs));
143+
self.properties
144+
.push((name.into(), flags, default, docs, ty, nullable, default_str));
125145
self
126146
}
127147

@@ -396,7 +416,7 @@ impl ClassBuilder {
396416
unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) };
397417
}
398418

399-
for (name, flags, default, _) in self.properties {
419+
for (name, flags, default, _, _ty, _nullable, _default_str) in self.properties {
400420
let mut default_zval = match default {
401421
Some(f) => f()?,
402422
None => Zval::new(),
@@ -482,13 +502,23 @@ mod tests {
482502

483503
#[test]
484504
fn test_property() {
485-
let class =
486-
ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, None, &["Doc 1"]);
505+
let class = ClassBuilder::new("Foo").property(
506+
"bar",
507+
PropertyFlags::Public,
508+
None,
509+
&["Doc 1"],
510+
None,
511+
false,
512+
None,
513+
);
487514
assert_eq!(class.properties.len(), 1);
488515
assert_eq!(class.properties[0].0, "bar");
489516
assert_eq!(class.properties[0].1, PropertyFlags::Public);
490517
assert!(class.properties[0].2.is_none());
491518
assert_eq!(class.properties[0].3, &["Doc 1"] as DocComments);
519+
assert!(class.properties[0].4.is_none());
520+
assert!(!class.properties[0].5); // nullable
521+
assert!(class.properties[0].6.is_none()); // default_str
492522
}
493523

494524
#[test]

src/builders/module.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,14 +418,30 @@ impl ModuleBuilder<'_> {
418418
.expect("Failed to register constant");
419419
}
420420
for (name, prop_info) in T::get_properties() {
421-
builder = builder.property(name, prop_info.flags, None, prop_info.docs);
421+
builder = builder.property(
422+
name,
423+
prop_info.flags,
424+
None,
425+
prop_info.docs,
426+
prop_info.ty,
427+
prop_info.nullable,
428+
prop_info.default,
429+
);
422430
}
423-
for (name, flags, default, docs) in T::static_properties() {
431+
for (name, flags, default, docs, ty, nullable, default_str) in T::static_properties() {
424432
let default_fn = default.map(|v| {
425433
Box::new(move || v.as_zval(true))
426434
as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
427435
});
428-
builder = builder.property(*name, *flags, default_fn, docs);
436+
builder = builder.property(
437+
*name,
438+
*flags,
439+
default_fn,
440+
docs,
441+
*ty,
442+
*nullable,
443+
*default_str,
444+
);
429445
}
430446
if let Some(modifier) = T::BUILDER_MODIFIER {
431447
builder = modifier(builder);

src/class.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,22 @@ pub trait RegisteredClass: Sized + 'static {
7676
/// Returns the static properties provided by the class.
7777
///
7878
/// Static properties are declared at the class level and managed by PHP,
79-
/// not by Rust handlers. Each tuple contains (name, flags, default, docs).
80-
/// The default value is optional - `None` means null default.
79+
/// not by Rust handlers. Each tuple contains:
80+
/// (name, flags, `default_value`, docs, type, nullable, `default_str`).
81+
/// - `default_value` is the actual PHP default value (None means null)
82+
/// - `type` is for stub generation
83+
/// - `nullable` indicates if the property accepts null
84+
/// - `default_str` is the PHP-compatible default string for stubs
8185
#[must_use]
86+
#[allow(clippy::type_complexity)]
8287
fn static_properties() -> &'static [(
8388
&'static str,
8489
PropertyFlags,
8590
Option<&'static (dyn IntoZvalDyn + Sync)>,
8691
DocComments,
92+
Option<crate::flags::DataType>,
93+
bool,
94+
Option<&'static str>,
8795
)] {
8896
&[]
8997
}

0 commit comments

Comments
 (0)