Skip to content

Commit ca5a27e

Browse files
committed
fix(stubs): Missing property types #184
1 parent a5bf383 commit ca5a27e

File tree

12 files changed

+334
-40
lines changed

12 files changed

+334
-40
lines changed

crates/cli/src/ext.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ use std::path::PathBuf;
22

33
use anyhow::{Context, Result};
44
use ext_php_rs::describe::Description;
5+
6+
#[cfg(unix)]
57
use libloading::os::unix::{Library, RTLD_LAZY, RTLD_LOCAL, Symbol};
68

9+
#[cfg(windows)]
10+
use libloading::{Library, Symbol};
11+
712
#[allow(improper_ctypes_definitions)]
813
pub struct Ext {
914
// These need to be here to keep the libraries alive. The extension library needs to be alive
1015
// to access the describe function. Missing here is the lifetime on `Symbol<'a, fn() ->
1116
// Module>` where `ext_lib: 'a`.
1217
#[allow(dead_code)]
1318
ext_lib: Library,
19+
#[cfg(unix)]
1420
describe_fn: Symbol<extern "C" fn() -> Description>,
21+
#[cfg(windows)]
22+
describe_fn: Symbol<'static, extern "C" fn() -> Description>,
1523
}
1624

1725
impl Ext {
@@ -25,10 +33,15 @@ impl Ext {
2533
.with_context(|| "Failed to load extension library")?;
2634

2735
// On other Unix platforms, RTLD_LAZY | RTLD_LOCAL is sufficient
28-
#[cfg(not(target_os = "macos"))]
36+
#[cfg(all(unix, not(target_os = "macos")))]
2937
let ext_lib = unsafe { Library::open(Some(ext_path), RTLD_LAZY | RTLD_LOCAL) }
3038
.with_context(|| "Failed to load extension library")?;
3139

40+
// On Windows, use the standard Library::new
41+
#[cfg(windows)]
42+
let ext_lib = unsafe { Library::new(ext_path) }
43+
.with_context(|| "Failed to load extension library")?;
44+
3245
let describe_fn = unsafe {
3346
ext_lib
3447
.get(b"ext_php_rs_describe_module")

crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,74 @@ namespace {
1212

1313
const HELLO_WORLD = 100;
1414

15+
/**
16+
* A simple test class demonstrating ext-php-rs features.
17+
*
18+
* This class showcases property definitions, constants, and various
19+
* method types including constructors and static methods.
20+
*/
1521
class TestClass {
1622
const NEW_CONSTANT_NAME = 5;
1723

1824
const SOME_OTHER_STR = 'Hello, world!';
1925

20-
public $a;
26+
public int $a;
2127

22-
public $b;
28+
public int $b;
2329

2430
/**
25-
* @param int $a
26-
* @param int $b
31+
* Creates a new TestClass instance.
32+
*
33+
* Both values are incremented by 10 before being stored.
34+
*
35+
* @param int $a First value to store
36+
* @param int $b Second value to store
2737
*/
2838
public function __construct(int $a, int $b) {}
2939

3040
/**
31-
* @return \TestClass
41+
* Demonstrates the builder pattern by returning self.
42+
*
43+
* @return \TestClass Returns the same instance for method chaining.
3244
*/
3345
public function builderPattern(): \TestClass {}
3446

3547
/**
36-
* @param int $a
37-
* @param int $test
48+
* Tests camelCase conversion and default parameter values.
49+
*
50+
* @param int $a First parameter with default value 5
51+
* @param int $test Second parameter with default value 100
3852
* @return void
3953
*/
4054
public function testCamelCase(int $a = 5, int $test = 100): void {}
4155

4256
/**
43-
* @return int
57+
* Returns a static value.
58+
*
59+
* @return int Always returns 5.
4460
*/
4561
public static function x(): int {}
4662
}
4763

4864
/**
49-
* @param object $z
50-
* @return int
65+
* Demonstrates ZvalConvert derive macro usage.
66+
*
67+
* @param object $z An object that will be converted from a PHP value
68+
* @return int Always returns 5.
5169
*/
5270
function get_zval_convert(object $z): int {}
5371

5472
/**
55-
* @return string
73+
* Returns a friendly greeting.
74+
*
75+
* @return string The string "Hello, world!".
5676
*/
5777
function hello_world(): string {}
5878

5979
/**
60-
* @return \TestClass
80+
* Creates a new TestClass instance with default values.
81+
*
82+
* @return \TestClass A TestClass with a=1 and b=2.
6183
*/
6284
function new_class(): \TestClass {}
6385
}

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
}

0 commit comments

Comments
 (0)