Skip to content

Commit 27f49f4

Browse files
committed
fix(stubs): Constants' values are now properly transferred #186
1 parent 6b192e8 commit 27f49f4

File tree

4 files changed

+128
-11
lines changed

4 files changed

+128
-11
lines changed

src/builders/class.rs

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

20-
type ConstantEntry = (String, Box<dyn FnOnce() -> Result<Zval>>, DocComments);
20+
/// A constant entry: (name, value_closure, docs, stub_value)
21+
type ConstantEntry = (String, Box<dyn FnOnce() -> Result<Zval>>, DocComments, String);
2122

2223
/// Builder for registering a class in PHP.
2324
#[must_use]
@@ -137,8 +138,11 @@ impl ClassBuilder {
137138
value: impl IntoZval + 'static,
138139
docs: DocComments,
139140
) -> Result<Self> {
141+
// Convert to Zval first to get stub value
142+
let zval = value.into_zval(true)?;
143+
let stub = crate::convert::zval_to_stub(&zval);
140144
self.constants
141-
.push((name.into(), Box::new(|| value.into_zval(true)), docs));
145+
.push((name.into(), Box::new(|| Ok(zval)), docs, stub));
142146
Ok(self)
143147
}
144148

@@ -163,9 +167,10 @@ impl ClassBuilder {
163167
value: &'static dyn IntoZvalDyn,
164168
docs: DocComments,
165169
) -> Result<Self> {
170+
let stub = value.stub_value();
166171
let value = Rc::new(value);
167172
self.constants
168-
.push((name.into(), Box::new(move || value.as_zval(true)), docs));
173+
.push((name.into(), Box::new(move || value.as_zval(true)), docs, stub));
169174
Ok(self)
170175
}
171176

@@ -368,7 +373,7 @@ impl ClassBuilder {
368373
}
369374
}
370375

371-
for (name, value, _) in self.constants {
376+
for (name, value, _, _) in self.constants {
372377
let value = Box::into_raw(Box::new(value()?));
373378
unsafe {
374379
zend_declare_class_constant(

src/constant.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use crate::ffi::{
1313

1414
/// Implemented on types which can be registered as a constant in PHP.
1515
pub trait IntoConst: Debug {
16+
/// Returns the PHP stub representation of this constant value.
17+
///
18+
/// This is used when generating PHP stub files for IDE autocompletion.
19+
/// The returned string should be a valid PHP literal (e.g., `"hello"`,
20+
/// `42`, `true`).
21+
fn stub_value(&self) -> String;
22+
1623
/// Registers a global module constant in PHP, with the value as the content
1724
/// of self. This function _must_ be called in the module startup
1825
/// function, which is called after the module is initialized. The
@@ -89,6 +96,10 @@ pub trait IntoConst: Debug {
8996
}
9097

9198
impl IntoConst for String {
99+
fn stub_value(&self) -> String {
100+
self.as_str().stub_value()
101+
}
102+
92103
fn register_constant_flags(
93104
&self,
94105
name: &str,
@@ -101,6 +112,17 @@ impl IntoConst for String {
101112
}
102113

103114
impl IntoConst for &str {
115+
fn stub_value(&self) -> String {
116+
// Escape special characters for PHP string literal
117+
let escaped = self
118+
.replace('\\', "\\\\")
119+
.replace('\'', "\\'")
120+
.replace('\n', "\\n")
121+
.replace('\r', "\\r")
122+
.replace('\t', "\\t");
123+
format!("'{escaped}'")
124+
}
125+
104126
fn register_constant_flags(
105127
&self,
106128
name: &str,
@@ -133,6 +155,10 @@ impl IntoConst for &str {
133155
}
134156

135157
impl IntoConst for bool {
158+
fn stub_value(&self) -> String {
159+
if *self { "true" } else { "false" }.to_string()
160+
}
161+
136162
fn register_constant_flags(
137163
&self,
138164
name: &str,
@@ -169,6 +195,10 @@ impl IntoConst for bool {
169195
macro_rules! into_const_num {
170196
($type: ty, $fn: expr) => {
171197
impl IntoConst for $type {
198+
fn stub_value(&self) -> String {
199+
self.to_string()
200+
}
201+
172202
fn register_constant_flags(
173203
&self,
174204
name: &str,

src/convert.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,85 @@ pub trait IntoZvalDyn {
253253

254254
/// Returns the PHP type of the type.
255255
fn get_type(&self) -> DataType;
256+
257+
/// Returns the PHP stub representation of this value.
258+
///
259+
/// This is used when generating PHP stub files for IDE autocompletion.
260+
/// The returned string should be a valid PHP literal.
261+
fn stub_value(&self) -> String {
262+
// Default implementation - convert to zval and format
263+
match self.as_zval(false) {
264+
Ok(zval) => zval_to_stub(&zval),
265+
Err(_) => "null".to_string(),
266+
}
267+
}
268+
}
269+
270+
/// Converts a Zval to its PHP stub representation.
271+
#[must_use]
272+
#[allow(clippy::match_same_arms)]
273+
pub fn zval_to_stub(zval: &Zval) -> String {
274+
use crate::flags::DataType;
275+
276+
match zval.get_type() {
277+
DataType::Null | DataType::Undef => "null".to_string(),
278+
DataType::True => "true".to_string(),
279+
DataType::False => "false".to_string(),
280+
DataType::Long => zval.long().map_or_else(|| "null".to_string(), |v| v.to_string()),
281+
DataType::Double => zval.double().map_or_else(|| "null".to_string(), |v| v.to_string()),
282+
DataType::String => {
283+
if let Some(s) = zval.str() {
284+
let escaped = s
285+
.replace('\\', "\\\\")
286+
.replace('\'', "\\'")
287+
.replace('\n', "\\n")
288+
.replace('\r', "\\r")
289+
.replace('\t', "\\t");
290+
format!("'{escaped}'")
291+
} else {
292+
"null".to_string()
293+
}
294+
}
295+
DataType::Array => {
296+
#[allow(clippy::explicit_iter_loop)]
297+
if let Some(arr) = zval.array() {
298+
// Check if array has sequential numeric keys starting from 0
299+
let is_sequential = arr.iter().enumerate().all(|(i, (key, _))| {
300+
matches!(key, crate::types::ArrayKey::Long(idx) if idx == i as i64)
301+
});
302+
303+
let mut parts = Vec::new();
304+
for (key, val) in arr.iter() {
305+
let val_str = zval_to_stub(val);
306+
if is_sequential {
307+
parts.push(val_str);
308+
} else {
309+
match key {
310+
crate::types::ArrayKey::Long(idx) => {
311+
parts.push(format!("{idx} => {val_str}"));
312+
}
313+
crate::types::ArrayKey::String(key) => {
314+
let key_escaped = key
315+
.replace('\\', "\\\\")
316+
.replace('\'', "\\'");
317+
parts.push(format!("'{key_escaped}' => {val_str}"));
318+
}
319+
crate::types::ArrayKey::Str(key) => {
320+
let key_escaped = key
321+
.replace('\\', "\\\\")
322+
.replace('\'', "\\'");
323+
parts.push(format!("'{key_escaped}' => {val_str}"));
324+
}
325+
}
326+
}
327+
}
328+
format!("[{}]", parts.join(", "))
329+
} else {
330+
"[]".to_string()
331+
}
332+
}
333+
_ => "null".to_string(),
334+
}
256335
}
257336

258337
impl<T: IntoZval + Clone> IntoZvalDyn for T {

src/describe/mod.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,11 @@ impl From<ClassBuilder> for Class {
270270
constants: val
271271
.constants
272272
.into_iter()
273-
.map(|(name, _, docs)| (name, docs))
274-
.map(Constant::from)
273+
.map(|(name, _, docs, stub)| Constant {
274+
name: name.into(),
275+
value: Option::Some(stub.into()),
276+
docs: docs.into(),
277+
})
275278
.collect::<StdVec<_>>()
276279
.into(),
277280
flags,
@@ -385,9 +388,9 @@ impl From<(String, PropertyFlags, DocComments)> for Property {
385388
let static_ = flags.contains(PropertyFlags::Static);
386389
let vis = Visibility::from(flags);
387390
// TODO: Implement ty #376
388-
let ty = abi::Option::None;
391+
let ty = Option::None;
389392
// TODO: Implement default #376
390-
let default = abi::Option::<abi::RString>::None;
393+
let default = Option::<RString>::None;
391394
// TODO: Implement nullable #376
392395
let nullable = false;
393396
let docs = docs.into();
@@ -552,18 +555,18 @@ impl From<(String, DocComments)> for Constant {
552555
let (name, docs) = val;
553556
Constant {
554557
name: name.into(),
555-
value: abi::Option::None,
558+
value: Option::None,
556559
docs: docs.into(),
557560
}
558561
}
559562
}
560563

561564
impl From<(String, Box<dyn IntoConst + Send>, DocComments)> for Constant {
562565
fn from(val: (String, Box<dyn IntoConst + Send + 'static>, DocComments)) -> Self {
563-
let (name, _, docs) = val;
566+
let (name, value, docs) = val;
564567
Constant {
565568
name: name.into(),
566-
value: abi::Option::None,
569+
value: Option::Some(value.stub_value().into()),
567570
docs: docs.into(),
568571
}
569572
}

0 commit comments

Comments
 (0)