Skip to main content

ext_php_rs/describe/
mod.rs

1//! Types used to describe downstream extensions. Used by the `cargo-php`
2//! CLI application to generate PHP stub files used by IDEs.
3//!
4//! The types live in `ext-php-rs-introspection` so `cargo-php` can read a
5//! [`Description`] without linking the Zend engine. This module re-exports
6//! them and converts the builders into them.
7use std::vec::Vec as StdVec;
8
9#[cfg(feature = "enum")]
10use crate::builders::EnumBuilder;
11use crate::{
12    builders::{ClassBuilder, FunctionBuilder},
13    constant::IntoConst,
14    flags::{ClassFlags, ConstantFlags, MethodFlags, PropertyFlags},
15    prelude::ModuleBuilder,
16};
17
18use ext_php_rs_introspection::abi::{Option, RString};
19pub use ext_php_rs_introspection::*;
20
21/// Builds a [`Module`] from a [`ModuleBuilder`].
22/// This is used to generate the PHP stubs for the module.
23impl From<ModuleBuilder<'_>> for Module {
24    fn from(builder: ModuleBuilder) -> Self {
25        let functions = builder.functions;
26
27        // Include both classes and interfaces in the classes list.
28        // Interfaces are distinguished by `Class::is_interface`.
29        #[allow(unused_mut)]
30        let mut classes = builder
31            .interfaces
32            .into_iter()
33            .chain(builder.classes)
34            .map(|c| c().into())
35            .collect::<StdVec<_>>();
36
37        #[cfg(feature = "closure")]
38        classes.push(Class::closure());
39
40        #[cfg(feature = "enum")]
41        let enums = builder
42            .enums
43            .into_iter()
44            .map(|e| e().into())
45            .collect::<StdVec<_>>();
46        #[cfg(not(feature = "enum"))]
47        let enums = StdVec::new();
48
49        Self {
50            name: builder.name.into(),
51            functions: functions
52                .into_iter()
53                .map(Function::from)
54                .collect::<StdVec<_>>()
55                .into(),
56            classes: classes.into(),
57            enums: enums.into(),
58            constants: builder
59                .constants
60                .into_iter()
61                .map(|(name, value, docs)| constant_from(name, &*value, docs))
62                .collect::<StdVec<_>>()
63                .into(),
64        }
65    }
66}
67
68impl From<FunctionBuilder<'_>> for Function {
69    fn from(val: FunctionBuilder<'_>) -> Self {
70        let ret_allow_null = val.ret_as_null;
71        Function {
72            name: val.name.into(),
73            docs: DocBlock(
74                val.docs
75                    .iter()
76                    .map(|d| (*d).into())
77                    .collect::<StdVec<_>>()
78                    .into(),
79            ),
80            ret: val
81                .retval
82                .map(|r| Retval {
83                    ty: r,
84                    nullable: r != DataType::Mixed && ret_allow_null,
85                })
86                .into(),
87            params: val
88                .args
89                .into_iter()
90                .map(Parameter::from)
91                .collect::<StdVec<_>>()
92                .into(),
93        }
94    }
95}
96
97impl From<ClassBuilder> for Class {
98    fn from(val: ClassBuilder) -> Self {
99        let is_interface =
100            ClassFlags::from_bits_retain(val.get_flags()).contains(ClassFlags::Interface);
101        Self {
102            name: val.name.into(),
103            docs: DocBlock(
104                val.docs
105                    .iter()
106                    .map(|doc| (*doc).into())
107                    .collect::<StdVec<_>>()
108                    .into(),
109            ),
110            extends: val.extends.map(|(_, stub)| stub.into()).into(),
111            implements: val
112                .interfaces
113                .into_iter()
114                .map(|(_, stub)| stub.into())
115                .collect::<StdVec<_>>()
116                .into(),
117            properties: val
118                .properties
119                .into_iter()
120                .map(Property::from)
121                .collect::<StdVec<_>>()
122                .into(),
123            methods: val
124                .methods
125                .into_iter()
126                .map(|(builder, flags)| method_from(builder, flags))
127                .collect::<StdVec<_>>()
128                .into(),
129            constants: val
130                .constants
131                .into_iter()
132                .map(|(name, _, docs, stub, flags)| Constant {
133                    name: name.into(),
134                    value: Option::Some(stub.into()),
135                    docs: docs.into(),
136                    visibility: flags.into(),
137                })
138                .collect::<StdVec<_>>()
139                .into(),
140            is_interface,
141        }
142    }
143}
144
145#[cfg(feature = "enum")]
146impl From<EnumBuilder> for Enum {
147    fn from(val: EnumBuilder) -> Self {
148        Self {
149            name: val.name.into(),
150            docs: DocBlock(
151                val.docs
152                    .iter()
153                    .map(|d| (*d).into())
154                    .collect::<StdVec<_>>()
155                    .into(),
156            ),
157            cases: val
158                .cases
159                .into_iter()
160                .map(EnumCase::from)
161                .collect::<StdVec<_>>()
162                .into(),
163            backing_type: match val.datatype {
164                DataType::Long => Some("int".into()),
165                DataType::String => Some("string".into()),
166                _ => None,
167            }
168            .into(),
169        }
170    }
171}
172
173#[cfg(feature = "enum")]
174impl From<&'static crate::enum_::EnumCase> for EnumCase {
175    fn from(val: &'static crate::enum_::EnumCase) -> Self {
176        Self {
177            name: val.name.into(),
178            docs: DocBlock(
179                val.docs
180                    .iter()
181                    .map(|d| (*d).into())
182                    .collect::<StdVec<_>>()
183                    .into(),
184            ),
185            value: val
186                .discriminant
187                .as_ref()
188                .map(|v| match v {
189                    crate::enum_::Discriminant::Int(i) => i.to_string().into(),
190                    crate::enum_::Discriminant::String(s) => format!("'{s}'").into(),
191                })
192                .into(),
193        }
194    }
195}
196
197impl From<crate::builders::ClassProperty> for Property {
198    fn from(val: crate::builders::ClassProperty) -> Self {
199        let static_ = val.flags.contains(PropertyFlags::Static);
200        let vis = Visibility::from(val.flags);
201        let docs = val.docs.into();
202
203        Self {
204            name: val.name.into(),
205            docs,
206            ty: val.ty.into(),
207            vis,
208            static_,
209            nullable: val.nullable,
210            readonly: val.readonly,
211            default: val.default_stub.map(RString::from).into(),
212        }
213    }
214}
215
216fn method_from(builder: FunctionBuilder<'_>, flags: MethodFlags) -> Method {
217    let ret_allow_null = builder.ret_as_null;
218    Method {
219        name: builder.name.into(),
220        docs: DocBlock(
221            builder
222                .docs
223                .iter()
224                .map(|d| (*d).into())
225                .collect::<StdVec<_>>()
226                .into(),
227        ),
228        retval: builder
229            .retval
230            .map(|r| Retval {
231                ty: r,
232                nullable: r != DataType::Mixed && ret_allow_null,
233            })
234            .into(),
235        params: builder
236            .args
237            .into_iter()
238            .map(Into::into)
239            .collect::<StdVec<_>>()
240            .into(),
241        ty: flags.into(),
242        r#static: flags.contains(MethodFlags::Static),
243        visibility: flags.into(),
244        r#abstract: flags.contains(MethodFlags::Abstract),
245    }
246}
247
248impl From<MethodFlags> for MethodType {
249    fn from(value: MethodFlags) -> Self {
250        if value.contains(MethodFlags::IsConstructor) {
251            return Self::Constructor;
252        }
253        if value.contains(MethodFlags::Static) {
254            return Self::Static;
255        }
256
257        Self::Member
258    }
259}
260
261impl From<PropertyFlags> for Visibility {
262    fn from(value: PropertyFlags) -> Self {
263        if value.contains(PropertyFlags::Protected) {
264            return Self::Protected;
265        }
266        if value.contains(PropertyFlags::Private) {
267            return Self::Private;
268        }
269
270        Self::Public
271    }
272}
273
274impl From<MethodFlags> for Visibility {
275    fn from(value: MethodFlags) -> Self {
276        if value.contains(MethodFlags::Protected) {
277            return Self::Protected;
278        }
279
280        if value.contains(MethodFlags::Private) {
281            return Self::Private;
282        }
283
284        Self::Public
285    }
286}
287
288impl From<ConstantFlags> for Visibility {
289    fn from(value: ConstantFlags) -> Self {
290        if value.contains(ConstantFlags::Protected) {
291            return Self::Protected;
292        }
293        if value.contains(ConstantFlags::Private) {
294            return Self::Private;
295        }
296
297        Self::Public
298    }
299}
300
301fn constant_from(name: String, value: &dyn IntoConst, docs: DocComments) -> Constant {
302    Constant {
303        name: name.into(),
304        value: Option::Some(value.stub_value().into()),
305        docs: docs.into(),
306        visibility: Visibility::Public,
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    #![cfg_attr(windows, feature(abi_vectorcall))]
313    use cfg_if::cfg_if;
314
315    use super::*;
316
317    use crate::{args::Arg, test::test_function};
318
319    #[test]
320    fn test_module_from() {
321        let builder = ModuleBuilder::new("test", "test_version")
322            .function(FunctionBuilder::new("test_function", test_function));
323        let module: Module = builder.into();
324        assert_eq!(module.name, "test".into());
325        assert_eq!(module.functions.len(), 1);
326        cfg_if! {
327            if #[cfg(feature = "closure")] {
328                assert_eq!(module.classes.len(), 1);
329            } else {
330                assert_eq!(module.classes.len(), 0);
331            }
332        }
333        assert_eq!(module.enums.len(), 0);
334        assert_eq!(module.constants.len(), 0);
335    }
336
337    #[test]
338    fn test_function_from() {
339        let builder = FunctionBuilder::new("test_function", test_function)
340            .docs(&["doc1", "doc2"])
341            .arg(Arg::new("foo", DataType::Long))
342            .returns(DataType::Bool, true, true);
343        let function: Function = builder.into();
344        assert_eq!(function.name, "test_function".into());
345        assert_eq!(function.docs.0.len(), 2);
346        assert_eq!(
347            function.params,
348            vec![Parameter {
349                name: "foo".into(),
350                ty: Option::Some(DataType::Long),
351                nullable: false,
352                variadic: false,
353                default: Option::None,
354            }]
355            .into()
356        );
357        assert_eq!(
358            function.ret,
359            Option::Some(Retval {
360                ty: DataType::Bool,
361                nullable: true,
362            })
363        );
364    }
365
366    #[test]
367    fn test_class_from() {
368        let builder = ClassBuilder::new("TestClass")
369            .docs(&["doc1", "doc2"])
370            .extends((|| todo!(), "BaseClass"))
371            .implements((|| todo!(), "Interface1"))
372            .implements((|| todo!(), "Interface2"))
373            .property(crate::builders::ClassProperty {
374                name: "prop1".into(),
375                flags: PropertyFlags::Public,
376                default: None,
377                docs: &["doc1"],
378                ty: None,
379                nullable: false,
380                readonly: false,
381                default_stub: None,
382            })
383            .method(
384                FunctionBuilder::new("test_function", test_function),
385                MethodFlags::Protected,
386            );
387        let class: Class = builder.into();
388
389        assert_eq!(class.name, "TestClass".into());
390        assert!(!class.is_interface);
391        assert_eq!(class.docs.0.len(), 2);
392        assert_eq!(class.extends, Option::Some("BaseClass".into()));
393        assert_eq!(
394            class.implements,
395            vec!["Interface1".into(), "Interface2".into()].into()
396        );
397        assert_eq!(class.properties.len(), 1);
398        assert_eq!(
399            class.properties[0],
400            Property {
401                name: "prop1".into(),
402                docs: DocBlock(vec!["doc1".into()].into()),
403                ty: Option::None,
404                vis: Visibility::Public,
405                static_: false,
406                nullable: false,
407                readonly: false,
408                default: Option::None,
409            }
410        );
411        assert_eq!(class.methods.len(), 1);
412        assert_eq!(
413            class.methods[0],
414            Method {
415                name: "test_function".into(),
416                docs: DocBlock(vec![].into()),
417                ty: MethodType::Member,
418                params: vec![].into(),
419                retval: Option::None,
420                r#static: false,
421                visibility: Visibility::Protected,
422                r#abstract: false
423            }
424        );
425    }
426
427    #[test]
428    fn test_interface_from() {
429        let class: Class = ClassBuilder::new("TestInterface")
430            .flags(ClassFlags::Interface)
431            .into();
432        assert!(class.is_interface);
433    }
434
435    #[test]
436    fn test_property_from() {
437        let property: Property = crate::builders::ClassProperty {
438            name: "test_property".into(),
439            flags: PropertyFlags::Protected,
440            default: None,
441            docs: &["doc1", "doc2"],
442            ty: Some(DataType::String),
443            nullable: true,
444            readonly: false,
445            default_stub: Some("null".into()),
446        }
447        .into();
448        assert_eq!(property.name, "test_property".into());
449        assert_eq!(property.docs.0.len(), 2);
450        assert_eq!(property.vis, Visibility::Protected);
451        assert!(!property.static_);
452        assert!(property.nullable);
453        assert_eq!(property.default, Option::Some("null".into()));
454        assert_eq!(property.ty, Option::Some(DataType::String));
455    }
456
457    #[test]
458    fn test_method_from() {
459        let builder = FunctionBuilder::new("test_method", test_function)
460            .docs(&["doc1", "doc2"])
461            .arg(Arg::new("foo", DataType::Long))
462            .returns(DataType::Bool, true, true);
463        let method = method_from(builder, MethodFlags::Static | MethodFlags::Protected);
464        assert_eq!(method.name, "test_method".into());
465        assert_eq!(method.docs.0.len(), 2);
466        assert_eq!(
467            method.params,
468            vec![Parameter {
469                name: "foo".into(),
470                ty: Option::Some(DataType::Long),
471                nullable: false,
472                variadic: false,
473                default: Option::None,
474            }]
475            .into()
476        );
477        assert_eq!(
478            method.retval,
479            Option::Some(Retval {
480                ty: DataType::Bool,
481                nullable: true,
482            })
483        );
484        assert!(method.r#static);
485        assert_eq!(method.visibility, Visibility::Protected);
486        assert_eq!(method.ty, MethodType::Static);
487    }
488
489    #[test]
490    fn test_ty_from() {
491        let r#static: MethodType = MethodFlags::Static.into();
492        assert_eq!(r#static, MethodType::Static);
493
494        let constructor: MethodType = MethodFlags::IsConstructor.into();
495        assert_eq!(constructor, MethodType::Constructor);
496
497        let member: MethodType = MethodFlags::Public.into();
498        assert_eq!(member, MethodType::Member);
499
500        let mixed: MethodType = (MethodFlags::Protected | MethodFlags::Static).into();
501        assert_eq!(mixed, MethodType::Static);
502
503        let both: MethodType = (MethodFlags::Static | MethodFlags::IsConstructor).into();
504        assert_eq!(both, MethodType::Constructor);
505
506        let empty: MethodType = MethodFlags::empty().into();
507        assert_eq!(empty, MethodType::Member);
508    }
509
510    #[test]
511    fn test_prop_visibility_from() {
512        let private: Visibility = PropertyFlags::Private.into();
513        assert_eq!(private, Visibility::Private);
514
515        let protected: Visibility = PropertyFlags::Protected.into();
516        assert_eq!(protected, Visibility::Protected);
517
518        let public: Visibility = PropertyFlags::Public.into();
519        assert_eq!(public, Visibility::Public);
520
521        let mixed: Visibility = (PropertyFlags::Protected | PropertyFlags::Static).into();
522        assert_eq!(mixed, Visibility::Protected);
523
524        let empty: Visibility = PropertyFlags::empty().into();
525        assert_eq!(empty, Visibility::Public);
526    }
527
528    #[test]
529    fn test_method_visibility_from() {
530        let private: Visibility = MethodFlags::Private.into();
531        assert_eq!(private, Visibility::Private);
532
533        let protected: Visibility = MethodFlags::Protected.into();
534        assert_eq!(protected, Visibility::Protected);
535
536        let public: Visibility = MethodFlags::Public.into();
537        assert_eq!(public, Visibility::Public);
538
539        let mixed: Visibility = (MethodFlags::Protected | MethodFlags::Static).into();
540        assert_eq!(mixed, Visibility::Protected);
541
542        let empty: Visibility = MethodFlags::empty().into();
543        assert_eq!(empty, Visibility::Public);
544    }
545}