Skip to content

Commit 50d3577

Browse files
committed
fix(stubs): Constants' values are now properly transferred #186
1 parent b511af6 commit 50d3577

File tree

5 files changed

+147
-24
lines changed

5 files changed

+147
-24
lines changed

build.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,16 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<S
231231
// Add macOS SDK path for system headers (stdlib.h, etc.)
232232
// Required for libclang 19+ with preserve_none calling convention support
233233
#[cfg(target_os = "macos")]
234+
if let Ok(sdk_path) = std::process::Command::new("xcrun")
235+
.args(["--show-sdk-path"])
236+
.output()
237+
&& sdk_path.status.success()
234238
{
235-
if let Ok(sdk_path) = std::process::Command::new("xcrun")
236-
.args(["--show-sdk-path"])
237-
.output()
238-
{
239-
if sdk_path.status.success() {
240-
let path = String::from_utf8_lossy(&sdk_path.stdout);
241-
let path = path.trim();
242-
bindgen = bindgen
243-
.clang_arg(format!("-isysroot{}", path))
244-
.clang_arg(format!("-I{}/usr/include", path));
245-
}
246-
}
239+
let path = String::from_utf8_lossy(&sdk_path.stdout);
240+
let path = path.trim();
241+
bindgen = bindgen
242+
.clang_arg(format!("-isysroot{path}"))
243+
.clang_arg(format!("-I{path}/usr/include"));
247244
}
248245

249246
bindgen = bindgen

src/builders/class.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ 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 = (
22+
String,
23+
Box<dyn FnOnce() -> Result<Zval>>,
24+
DocComments,
25+
String,
26+
);
2127
type PropertyDefault = Option<Box<dyn FnOnce() -> Result<Zval>>>;
2228

2329
/// Builder for registering a class in PHP.
@@ -140,8 +146,11 @@ impl ClassBuilder {
140146
value: impl IntoZval + 'static,
141147
docs: DocComments,
142148
) -> Result<Self> {
149+
// Convert to Zval first to get stub value
150+
let zval = value.into_zval(true)?;
151+
let stub = crate::convert::zval_to_stub(&zval);
143152
self.constants
144-
.push((name.into(), Box::new(|| value.into_zval(true)), docs));
153+
.push((name.into(), Box::new(|| Ok(zval)), docs, stub));
145154
Ok(self)
146155
}
147156

@@ -166,9 +175,14 @@ impl ClassBuilder {
166175
value: &'static dyn IntoZvalDyn,
167176
docs: DocComments,
168177
) -> Result<Self> {
178+
let stub = value.stub_value();
169179
let value = Rc::new(value);
170-
self.constants
171-
.push((name.into(), Box::new(move || value.as_zval(true)), docs));
180+
self.constants.push((
181+
name.into(),
182+
Box::new(move || value.as_zval(true)),
183+
docs,
184+
stub,
185+
));
172186
Ok(self)
173187
}
174188

@@ -375,7 +389,7 @@ impl ClassBuilder {
375389
}
376390
}
377391

378-
for (name, value, _) in self.constants {
392+
for (name, value, _, _) in self.constants {
379393
let value = Box::into_raw(Box::new(value()?));
380394
unsafe {
381395
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
281+
.long()
282+
.map_or_else(|| "null".to_string(), |v| v.to_string()),
283+
DataType::Double => zval
284+
.double()
285+
.map_or_else(|| "null".to_string(), |v| v.to_string()),
286+
DataType::String => {
287+
if let Some(s) = zval.str() {
288+
let escaped = s
289+
.replace('\\', "\\\\")
290+
.replace('\'', "\\'")
291+
.replace('\n', "\\n")
292+
.replace('\r', "\\r")
293+
.replace('\t', "\\t");
294+
format!("'{escaped}'")
295+
} else {
296+
"null".to_string()
297+
}
298+
}
299+
DataType::Array => {
300+
#[allow(clippy::explicit_iter_loop)]
301+
if let Some(arr) = zval.array() {
302+
// Check if array has sequential numeric keys starting from 0
303+
let is_sequential = arr.iter().enumerate().all(|(i, (key, _))| {
304+
matches!(key, crate::types::ArrayKey::Long(idx) if i64::try_from(i).is_ok_and(|ii| idx == ii))
305+
});
306+
307+
let mut parts = Vec::new();
308+
for (key, val) in arr.iter() {
309+
let val_str = zval_to_stub(val);
310+
if is_sequential {
311+
parts.push(val_str);
312+
} else {
313+
match key {
314+
crate::types::ArrayKey::Long(idx) => {
315+
parts.push(format!("{idx} => {val_str}"));
316+
}
317+
crate::types::ArrayKey::String(key) => {
318+
let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
319+
parts.push(format!("'{key_escaped}' => {val_str}"));
320+
}
321+
crate::types::ArrayKey::Str(key) => {
322+
let key_escaped = key.replace('\\', "\\\\").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<D> From<(String, PropertyFlags, D, 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)