From 50d35775976629f148c5182ef2ecad90d5982f67 Mon Sep 17 00:00:00 2001 From: Vasily Zorin Date: Sat, 20 Dec 2025 22:12:04 +0700 Subject: [PATCH] fix(stubs): Constants' values are now properly transferred #186 --- build.rs | 21 +++++------- src/builders/class.rs | 24 ++++++++++--- src/constant.rs | 30 ++++++++++++++++ src/convert.rs | 79 +++++++++++++++++++++++++++++++++++++++++++ src/describe/mod.rs | 17 ++++++---- 5 files changed, 147 insertions(+), 24 deletions(-) diff --git a/build.rs b/build.rs index c740b3c4ed..ccfd2deb8f 100644 --- a/build.rs +++ b/build.rs @@ -231,19 +231,16 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Result>, DocComments); +/// A constant entry: (name, `value_closure`, docs, `stub_value`) +type ConstantEntry = ( + String, + Box Result>, + DocComments, + String, +); type PropertyDefault = Option Result>>; /// Builder for registering a class in PHP. @@ -140,8 +146,11 @@ impl ClassBuilder { value: impl IntoZval + 'static, docs: DocComments, ) -> Result { + // Convert to Zval first to get stub value + let zval = value.into_zval(true)?; + let stub = crate::convert::zval_to_stub(&zval); self.constants - .push((name.into(), Box::new(|| value.into_zval(true)), docs)); + .push((name.into(), Box::new(|| Ok(zval)), docs, stub)); Ok(self) } @@ -166,9 +175,14 @@ impl ClassBuilder { value: &'static dyn IntoZvalDyn, docs: DocComments, ) -> Result { + let stub = value.stub_value(); let value = Rc::new(value); - self.constants - .push((name.into(), Box::new(move || value.as_zval(true)), docs)); + self.constants.push(( + name.into(), + Box::new(move || value.as_zval(true)), + docs, + stub, + )); Ok(self) } @@ -375,7 +389,7 @@ impl ClassBuilder { } } - for (name, value, _) in self.constants { + for (name, value, _, _) in self.constants { let value = Box::into_raw(Box::new(value()?)); unsafe { zend_declare_class_constant( diff --git a/src/constant.rs b/src/constant.rs index 56baeeca59..dc5d852e53 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -13,6 +13,13 @@ use crate::ffi::{ /// Implemented on types which can be registered as a constant in PHP. pub trait IntoConst: Debug { + /// Returns the PHP stub representation of this constant value. + /// + /// This is used when generating PHP stub files for IDE autocompletion. + /// The returned string should be a valid PHP literal (e.g., `"hello"`, + /// `42`, `true`). + fn stub_value(&self) -> String; + /// Registers a global module constant in PHP, with the value as the content /// of self. This function _must_ be called in the module startup /// function, which is called after the module is initialized. The @@ -89,6 +96,10 @@ pub trait IntoConst: Debug { } impl IntoConst for String { + fn stub_value(&self) -> String { + self.as_str().stub_value() + } + fn register_constant_flags( &self, name: &str, @@ -101,6 +112,17 @@ impl IntoConst for String { } impl IntoConst for &str { + fn stub_value(&self) -> String { + // Escape special characters for PHP string literal + let escaped = self + .replace('\\', "\\\\") + .replace('\'', "\\'") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t"); + format!("'{escaped}'") + } + fn register_constant_flags( &self, name: &str, @@ -133,6 +155,10 @@ impl IntoConst for &str { } impl IntoConst for bool { + fn stub_value(&self) -> String { + if *self { "true" } else { "false" }.to_string() + } + fn register_constant_flags( &self, name: &str, @@ -169,6 +195,10 @@ impl IntoConst for bool { macro_rules! into_const_num { ($type: ty, $fn: expr) => { impl IntoConst for $type { + fn stub_value(&self) -> String { + self.to_string() + } + fn register_constant_flags( &self, name: &str, diff --git a/src/convert.rs b/src/convert.rs index 69aa75f2f6..57db0b430e 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -253,6 +253,85 @@ pub trait IntoZvalDyn { /// Returns the PHP type of the type. fn get_type(&self) -> DataType; + + /// Returns the PHP stub representation of this value. + /// + /// This is used when generating PHP stub files for IDE autocompletion. + /// The returned string should be a valid PHP literal. + fn stub_value(&self) -> String { + // Default implementation - convert to zval and format + match self.as_zval(false) { + Ok(zval) => zval_to_stub(&zval), + Err(_) => "null".to_string(), + } + } +} + +/// Converts a Zval to its PHP stub representation. +#[must_use] +#[allow(clippy::match_same_arms)] +pub fn zval_to_stub(zval: &Zval) -> String { + use crate::flags::DataType; + + match zval.get_type() { + DataType::Null | DataType::Undef => "null".to_string(), + DataType::True => "true".to_string(), + DataType::False => "false".to_string(), + DataType::Long => zval + .long() + .map_or_else(|| "null".to_string(), |v| v.to_string()), + DataType::Double => zval + .double() + .map_or_else(|| "null".to_string(), |v| v.to_string()), + DataType::String => { + if let Some(s) = zval.str() { + let escaped = s + .replace('\\', "\\\\") + .replace('\'', "\\'") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t"); + format!("'{escaped}'") + } else { + "null".to_string() + } + } + DataType::Array => { + #[allow(clippy::explicit_iter_loop)] + if let Some(arr) = zval.array() { + // Check if array has sequential numeric keys starting from 0 + let is_sequential = arr.iter().enumerate().all(|(i, (key, _))| { + matches!(key, crate::types::ArrayKey::Long(idx) if i64::try_from(i).is_ok_and(|ii| idx == ii)) + }); + + let mut parts = Vec::new(); + for (key, val) in arr.iter() { + let val_str = zval_to_stub(val); + if is_sequential { + parts.push(val_str); + } else { + match key { + crate::types::ArrayKey::Long(idx) => { + parts.push(format!("{idx} => {val_str}")); + } + crate::types::ArrayKey::String(key) => { + let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'"); + parts.push(format!("'{key_escaped}' => {val_str}")); + } + crate::types::ArrayKey::Str(key) => { + let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'"); + parts.push(format!("'{key_escaped}' => {val_str}")); + } + } + } + } + format!("[{}]", parts.join(", ")) + } else { + "[]".to_string() + } + } + _ => "null".to_string(), + } } impl IntoZvalDyn for T { diff --git a/src/describe/mod.rs b/src/describe/mod.rs index d86cc5f202..b6e1b434cc 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -270,8 +270,11 @@ impl From for Class { constants: val .constants .into_iter() - .map(|(name, _, docs)| (name, docs)) - .map(Constant::from) + .map(|(name, _, docs, stub)| Constant { + name: name.into(), + value: Option::Some(stub.into()), + docs: docs.into(), + }) .collect::>() .into(), flags, @@ -385,9 +388,9 @@ impl From<(String, PropertyFlags, D, DocComments)> for Property { let static_ = flags.contains(PropertyFlags::Static); let vis = Visibility::from(flags); // TODO: Implement ty #376 - let ty = abi::Option::None; + let ty = Option::None; // TODO: Implement default #376 - let default = abi::Option::::None; + let default = Option::::None; // TODO: Implement nullable #376 let nullable = false; let docs = docs.into(); @@ -552,7 +555,7 @@ impl From<(String, DocComments)> for Constant { let (name, docs) = val; Constant { name: name.into(), - value: abi::Option::None, + value: Option::None, docs: docs.into(), } } @@ -560,10 +563,10 @@ impl From<(String, DocComments)> for Constant { impl From<(String, Box, DocComments)> for Constant { fn from(val: (String, Box, DocComments)) -> Self { - let (name, _, docs) = val; + let (name, value, docs) = val; Constant { name: name.into(), - value: abi::Option::None, + value: Option::Some(value.stub_value().into()), docs: docs.into(), } }