• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 16274060216

14 Jul 2025 05:51PM UTC coverage: 22.552%. Remained the same
16274060216

Pull #514

github

web-flow
Merge 7ee17f41f into 31c9d9968
Pull Request #514: feat(stubs)!: add stubs for `RustClosure`

13 of 15 new or added lines in 3 files covered. (86.67%)

58 existing lines in 3 files now uncovered.

882 of 3911 relevant lines covered (22.55%)

3.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.04
/src/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
use std::vec::Vec as StdVec;
4

5
use crate::{
6
    builders::{ClassBuilder, FunctionBuilder},
7
    constant::IntoConst,
8
    flags::{DataType, MethodFlags, PropertyFlags},
9
    prelude::ModuleBuilder,
10
};
11
use abi::{Option, RString, Str, Vec};
12

13
pub mod abi;
14
mod stub;
15

16
pub use stub::ToStub;
17

18
/// A slice of strings containing documentation comments.
19
pub type DocComments = &'static [&'static str];
20

21
/// Representation of the extension used to generate PHP stubs.
22
#[repr(C)]
23
pub struct Description {
24
    /// Extension description.
25
    pub module: Module,
26
    /// ext-php-rs version.
27
    pub version: &'static str,
28
}
29

30
impl Description {
31
    /// Creates a new description.
32
    ///
33
    /// # Parameters
34
    ///
35
    /// * `module` - The extension module representation.
36
    #[must_use]
37
    pub fn new(module: Module) -> Self {
1✔
38
        Self {
39
            module,
40
            version: crate::VERSION,
41
        }
42
    }
43
}
44

45
/// Represents a set of comments on an export.
46
#[repr(C)]
47
#[derive(Debug, PartialEq)]
48
pub struct DocBlock(pub Vec<Str>);
49

50
impl From<&'static [&'static str]> for DocBlock {
51
    fn from(val: &'static [&'static str]) -> Self {
3✔
52
        Self(
53
            val.iter()
6✔
54
                .map(|s| (*s).into())
13✔
55
                .collect::<StdVec<_>>()
3✔
56
                .into(),
3✔
57
        )
58
    }
59
}
60

61
/// Represents an extension containing a set of exports.
62
#[repr(C)]
63
pub struct Module {
64
    /// Name of the extension.
65
    pub name: RString,
66
    /// Functions exported by the extension.
67
    pub functions: Vec<Function>,
68
    /// Classes exported by the extension.
69
    pub classes: Vec<Class>,
70
    /// Constants exported by the extension.
71
    pub constants: Vec<Constant>,
72
}
73

74
/// Builds a [`Module`] from a [`ModuleBuilder`].
75
/// This is used to generate the PHP stubs for the module.
76
impl From<ModuleBuilder<'_>> for Module {
77
    fn from(builder: ModuleBuilder) -> Self {
1✔
78
        let functions = builder.functions;
2✔
79

80
        #[allow(unused_mut)]
81
        let mut classes = builder
2✔
82
            .classes
1✔
83
            .into_iter()
84
            .map(|c| c().into())
1✔
85
            .collect::<StdVec<_>>();
86

87
        #[cfg(feature = "closure")]
88
        classes.push(Class::closure());
3✔
89

90
        Self {
91
            name: builder.name.into(),
3✔
92
            functions: functions
2✔
93
                .into_iter()
94
                .map(Function::from)
95
                .collect::<StdVec<_>>()
96
                .into(),
97
            classes: classes.into(),
3✔
98
            constants: builder
2✔
99
                .constants
100
                .into_iter()
101
                .map(Constant::from)
102
                .collect::<StdVec<_>>()
103
                .into(),
104
        }
105
    }
106
}
107

108
/// Represents an exported function.
109
#[repr(C)]
110
pub struct Function {
111
    /// Name of the function.
112
    pub name: RString,
113
    /// Documentation comments for the function.
114
    pub docs: DocBlock,
115
    /// Return value of the function.
116
    pub ret: Option<Retval>,
117
    /// Parameters of the function.
118
    pub params: Vec<Parameter>,
119
}
120

121
impl From<FunctionBuilder<'_>> for Function {
122
    fn from(val: FunctionBuilder<'_>) -> Self {
2✔
123
        let ret_allow_null = val.ret_as_null;
4✔
124
        Function {
125
            name: val.name.into(),
4✔
126
            docs: DocBlock(
127
                val.docs
128
                    .iter()
129
                    .map(|d| (*d).into())
130
                    .collect::<StdVec<_>>()
131
                    .into(),
132
            ),
133
            ret: val
2✔
134
                .retval
135
                .map(|r| Retval {
136
                    ty: r,
137
                    nullable: r != DataType::Mixed && ret_allow_null,
138
                })
139
                .into(),
140
            params: val
2✔
141
                .args
142
                .into_iter()
143
                .map(Parameter::from)
144
                .collect::<StdVec<_>>()
145
                .into(),
146
        }
147
    }
148
}
149

150
/// Represents a parameter attached to an exported function or method.
151
#[repr(C)]
152
#[derive(Debug, PartialEq)]
153
pub struct Parameter {
154
    /// Name of the parameter.
155
    pub name: RString,
156
    /// Type of the parameter.
157
    pub ty: Option<DataType>,
158
    /// Whether the parameter is nullable.
159
    pub nullable: bool,
160
    /// Whether the parameter is variadic.
161
    pub variadic: bool,
162
    /// Default value of the parameter.
163
    pub default: Option<RString>,
164
}
165

166
/// Represents an exported class.
167
#[repr(C)]
168
pub struct Class {
169
    /// Name of the class.
170
    pub name: RString,
171
    /// Documentation comments for the class.
172
    pub docs: DocBlock,
173
    /// Name of the class the exported class extends. (Not implemented #326)
174
    pub extends: Option<RString>,
175
    /// Names of the interfaces the exported class implements. (Not implemented
176
    /// #326)
177
    pub implements: Vec<RString>,
178
    /// Properties of the class.
179
    pub properties: Vec<Property>,
180
    /// Methods of the class.
181
    pub methods: Vec<Method>,
182
    /// Constants of the class.
183
    pub constants: Vec<Constant>,
184
}
185

186
#[cfg(feature = "closure")]
187
impl Class {
188
    /// Creates a new class representing a Rust closure used for generating
189
    /// the stubs if the `closure` feature is enabled.
190
    #[must_use]
191
    pub fn closure() -> Self {
1✔
192
        Self {
193
            name: "RustClosure".into(),
3✔
194
            docs: DocBlock(StdVec::new().into()),
2✔
195
            extends: Option::None,
196
            implements: StdVec::new().into(),
3✔
197
            properties: StdVec::new().into(),
3✔
198
            methods: vec![Method {
3✔
199
                name: "__invoke".into(),
200
                docs: DocBlock(StdVec::new().into()),
201
                ty: MethodType::Member,
202
                params: vec![Parameter {
203
                    name: "args".into(),
204
                    ty: Option::Some(DataType::Mixed),
205
                    nullable: false,
206
                    variadic: true,
207
                    default: Option::None,
208
                }]
209
                .into(),
210
                retval: Option::Some(Retval {
211
                    ty: DataType::Mixed,
212
                    nullable: false,
213
                }),
214
                r#static: false,
215
                visibility: Visibility::Public,
216
            }]
217
            .into(),
218
            constants: StdVec::new().into(),
1✔
219
        }
220
    }
221
}
222

223
impl From<ClassBuilder> for Class {
224
    fn from(val: ClassBuilder) -> Self {
1✔
225
        Self {
226
            name: val.name.into(),
2✔
227
            docs: DocBlock(
228
                val.docs
229
                    .iter()
230
                    .map(|doc| (*doc).into())
231
                    .collect::<StdVec<_>>()
232
                    .into(),
233
            ),
234
            extends: val.extends.map(|(_, stub)| stub.into()).into(),
5✔
235
            implements: val
1✔
236
                .interfaces
237
                .into_iter()
238
                .map(|(_, stub)| stub.into())
239
                .collect::<StdVec<_>>()
240
                .into(),
241
            properties: val
1✔
242
                .properties
243
                .into_iter()
244
                .map(Property::from)
245
                .collect::<StdVec<_>>()
246
                .into(),
247
            methods: val
1✔
248
                .methods
249
                .into_iter()
250
                .map(Method::from)
251
                .collect::<StdVec<_>>()
252
                .into(),
253
            constants: val
1✔
254
                .constants
255
                .into_iter()
256
                .map(|(name, _, docs)| (name, docs))
257
                .map(Constant::from)
258
                .collect::<StdVec<_>>()
259
                .into(),
260
        }
261
    }
262
}
263

264
/// Represents a property attached to an exported class.
265
#[repr(C)]
266
#[derive(Debug, PartialEq)]
267
pub struct Property {
268
    /// Name of the property.
269
    pub name: RString,
270
    /// Documentation comments for the property.
271
    pub docs: DocBlock,
272
    /// Type of the property (Not implemented #376)
273
    pub ty: Option<DataType>,
274
    /// Visibility of the property.
275
    pub vis: Visibility,
276
    /// Whether the property is static.
277
    pub static_: bool,
278
    /// Whether the property is nullable. (Not implemented #376)
279
    pub nullable: bool,
280
    /// Default value of the property. (Not implemented #376)
281
    pub default: Option<RString>,
282
}
283

284
impl From<(String, PropertyFlags, DocComments)> for Property {
285
    fn from(value: (String, PropertyFlags, DocComments)) -> Self {
2✔
286
        let (name, flags, docs) = value;
8✔
287
        let static_ = flags.contains(PropertyFlags::Static);
6✔
288
        let vis = Visibility::from(flags);
6✔
289
        // TODO: Implement ty #376
290
        let ty = abi::Option::None;
4✔
291
        // TODO: Implement default #376
292
        let default = abi::Option::<abi::RString>::None;
4✔
293
        // TODO: Implement nullable #376
294
        let nullable = false;
4✔
295
        let docs = docs.into();
6✔
296

297
        Self {
298
            name: name.into(),
6✔
299
            docs,
300
            ty,
301
            vis,
302
            static_,
303
            nullable,
304
            default,
305
        }
306
    }
307
}
308

309
/// Represents a method attached to an exported class.
310
#[repr(C)]
311
#[derive(Debug, PartialEq)]
312
pub struct Method {
313
    /// Name of the method.
314
    pub name: RString,
315
    /// Documentation comments for the method.
316
    pub docs: DocBlock,
317
    /// Type of the method.
318
    pub ty: MethodType,
319
    /// Parameters of the method.
320
    pub params: Vec<Parameter>,
321
    /// Return value of the method.
322
    pub retval: Option<Retval>,
323
    /// Whether the method is static.
324
    pub r#static: bool,
325
    /// Visibility of the method.
326
    pub visibility: Visibility,
327
}
328

329
impl From<(FunctionBuilder<'_>, MethodFlags)> for Method {
330
    fn from(val: (FunctionBuilder<'_>, MethodFlags)) -> Self {
2✔
331
        let (builder, flags) = val;
6✔
332
        let ret_allow_null = builder.ret_as_null;
4✔
333
        Method {
334
            name: builder.name.into(),
4✔
335
            docs: DocBlock(
336
                builder
337
                    .docs
338
                    .iter()
339
                    .map(|d| (*d).into())
340
                    .collect::<StdVec<_>>()
341
                    .into(),
342
            ),
343
            retval: builder
2✔
344
                .retval
345
                .map(|r| Retval {
346
                    ty: r,
347
                    nullable: r != DataType::Mixed && ret_allow_null,
348
                })
349
                .into(),
350
            params: builder
2✔
351
                .args
352
                .into_iter()
353
                .map(Into::into)
354
                .collect::<StdVec<_>>()
355
                .into(),
356
            ty: flags.into(),
4✔
357
            r#static: flags.contains(MethodFlags::Static),
4✔
358
            visibility: flags.into(),
4✔
359
        }
360
    }
361
}
362

363
/// Represents a value returned from a function or method.
364
#[repr(C)]
365
#[derive(Debug, PartialEq)]
366
pub struct Retval {
367
    /// Type of the return value.
368
    pub ty: DataType,
369
    /// Whether the return value is nullable.
370
    pub nullable: bool,
371
}
372

373
/// Enumerator used to differentiate between methods.
374
#[repr(C)]
375
#[derive(Clone, Copy, Debug, PartialEq)]
376
pub enum MethodType {
377
    /// A member method.
378
    Member,
379
    /// A static method.
380
    Static,
381
    /// A constructor.
382
    Constructor,
383
}
384

385
impl From<MethodFlags> for MethodType {
386
    fn from(value: MethodFlags) -> Self {
8✔
387
        if value.contains(MethodFlags::IsConstructor) {
16✔
388
            return Self::Constructor;
2✔
389
        }
390
        if value.contains(MethodFlags::Static) {
391
            return Self::Static;
3✔
392
        }
393

394
        Self::Member
395
    }
396
}
397

398
/// Enumerator used to differentiate between different method and property
399
/// visibilties.
400
#[repr(C)]
401
#[derive(Clone, Copy, Debug, PartialEq)]
402
pub enum Visibility {
403
    /// Private visibility.
404
    Private,
405
    /// Protected visibility.
406
    Protected,
407
    /// Public visibility.
408
    Public,
409
}
410

411
impl From<PropertyFlags> for Visibility {
412
    fn from(value: PropertyFlags) -> Self {
7✔
413
        if value.contains(PropertyFlags::Protected) {
14✔
414
            return Self::Protected;
3✔
415
        }
416
        if value.contains(PropertyFlags::Private) {
417
            return Self::Private;
1✔
418
        }
419

420
        Self::Public
421
    }
422
}
423

424
impl From<MethodFlags> for Visibility {
425
    fn from(value: MethodFlags) -> Self {
7✔
426
        if value.contains(MethodFlags::Protected) {
14✔
427
            return Self::Protected;
4✔
428
        }
429

430
        if value.contains(MethodFlags::Private) {
431
            return Self::Private;
1✔
432
        }
433

434
        Self::Public
435
    }
436
}
437

438
/// Represents an exported constant, stand alone or attached to a class.
439
#[repr(C)]
440
pub struct Constant {
441
    /// Name of the constant.
442
    pub name: RString,
443
    /// Documentation comments for the constant.
444
    pub docs: DocBlock,
445
    /// Value of the constant.
446
    pub value: Option<RString>,
447
}
448

449
impl From<(String, DocComments)> for Constant {
UNCOV
450
    fn from(val: (String, DocComments)) -> Self {
×
UNCOV
451
        let (name, docs) = val;
×
452
        Constant {
UNCOV
453
            name: name.into(),
×
454
            value: abi::Option::None,
UNCOV
455
            docs: docs.into(),
×
456
        }
457
    }
458
}
459

460
impl From<(String, Box<dyn IntoConst + Send>, DocComments)> for Constant {
UNCOV
461
    fn from(val: (String, Box<dyn IntoConst + Send + 'static>, DocComments)) -> Self {
×
UNCOV
462
        let (name, _, docs) = val;
×
463
        Constant {
UNCOV
464
            name: name.into(),
×
465
            value: abi::Option::None,
UNCOV
466
            docs: docs.into(),
×
467
        }
468
    }
469
}
470

471
#[cfg(test)]
472
mod tests {
473
    #![cfg_attr(windows, feature(abi_vectorcall))]
474
    use cfg_if::cfg_if;
475

476
    use super::*;
477

478
    use crate::{args::Arg, test::test_function};
479

480
    #[test]
481
    fn test_new_description() {
482
        let module = Module {
483
            name: "test".into(),
484
            functions: vec![].into(),
485
            classes: vec![].into(),
486
            constants: vec![].into(),
487
        };
488

489
        let description = Description::new(module);
490
        assert_eq!(description.version, crate::VERSION);
491
        assert_eq!(description.module.name, "test".into());
492
    }
493

494
    #[test]
495
    fn test_doc_block_from() {
496
        let docs: &'static [&'static str] = &["doc1", "doc2"];
497
        let docs: DocBlock = docs.into();
498
        assert_eq!(docs.0.len(), 2);
499
        assert_eq!(docs.0[0], "doc1".into());
500
        assert_eq!(docs.0[1], "doc2".into());
501
    }
502

503
    #[test]
504
    fn test_module_from() {
505
        let builder = ModuleBuilder::new("test", "test_version")
506
            .function(FunctionBuilder::new("test_function", test_function));
507
        let module: Module = builder.into();
508
        assert_eq!(module.name, "test".into());
509
        assert_eq!(module.functions.len(), 1);
510
        cfg_if! {
511
            if #[cfg(feature = "closure")] {
512
                assert_eq!(module.classes.len(), 1);
513
            } else {
514
                assert_eq!(module.classes.len(), 0);
515
            }
516
        }
517
        assert_eq!(module.constants.len(), 0);
518
    }
519

520
    #[test]
521
    fn test_function_from() {
522
        let builder = FunctionBuilder::new("test_function", test_function)
523
            .docs(&["doc1", "doc2"])
524
            .arg(Arg::new("foo", DataType::Long))
525
            .returns(DataType::Bool, true, true);
526
        let function: Function = builder.into();
527
        assert_eq!(function.name, "test_function".into());
528
        assert_eq!(function.docs.0.len(), 2);
529
        assert_eq!(
530
            function.params,
531
            vec![Parameter {
532
                name: "foo".into(),
533
                ty: Option::Some(DataType::Long),
534
                nullable: false,
535
                variadic: false,
536
                default: Option::None,
537
            }]
538
            .into()
539
        );
540
        assert_eq!(
541
            function.ret,
542
            Option::Some(Retval {
543
                ty: DataType::Bool,
544
                nullable: true,
545
            })
546
        );
547
    }
548

549
    #[test]
550
    fn test_class_from() {
551
        let builder = ClassBuilder::new("TestClass")
552
            .docs(&["doc1", "doc2"])
553
            .extends((|| todo!(), "BaseClass"))
554
            .implements((|| todo!(), "Interface1"))
555
            .implements((|| todo!(), "Interface2"))
556
            .property("prop1", PropertyFlags::Public, &["doc1"])
557
            .method(
558
                FunctionBuilder::new("test_function", test_function),
559
                MethodFlags::Protected,
560
            );
561
        let class: Class = builder.into();
562

563
        assert_eq!(class.name, "TestClass".into());
564
        assert_eq!(class.docs.0.len(), 2);
565
        assert_eq!(class.extends, Option::Some("BaseClass".into()));
566
        assert_eq!(
567
            class.implements,
568
            vec!["Interface1".into(), "Interface2".into()].into()
569
        );
570
        assert_eq!(class.properties.len(), 1);
571
        assert_eq!(
572
            class.properties[0],
573
            Property {
574
                name: "prop1".into(),
575
                docs: DocBlock(vec!["doc1".into()].into()),
576
                ty: Option::None,
577
                vis: Visibility::Public,
578
                static_: false,
579
                nullable: false,
580
                default: Option::None,
581
            }
582
        );
583
        assert_eq!(class.methods.len(), 1);
584
        assert_eq!(
585
            class.methods[0],
586
            Method {
587
                name: "test_function".into(),
588
                docs: DocBlock(vec![].into()),
589
                ty: MethodType::Member,
590
                params: vec![].into(),
591
                retval: Option::None,
592
                r#static: false,
593
                visibility: Visibility::Protected,
594
            }
595
        );
596
    }
597

598
    #[test]
599
    fn test_property_from() {
600
        let docs: &'static [&'static str] = &["doc1", "doc2"];
601
        let property: Property =
602
            ("test_property".to_string(), PropertyFlags::Protected, docs).into();
603
        assert_eq!(property.name, "test_property".into());
604
        assert_eq!(property.docs.0.len(), 2);
605
        assert_eq!(property.vis, Visibility::Protected);
606
        assert!(!property.static_);
607
        assert!(!property.nullable);
608
    }
609

610
    #[test]
611
    fn test_method_from() {
612
        let builder = FunctionBuilder::new("test_method", test_function)
613
            .docs(&["doc1", "doc2"])
614
            .arg(Arg::new("foo", DataType::Long))
615
            .returns(DataType::Bool, true, true);
616
        let method: Method = (builder, MethodFlags::Static | MethodFlags::Protected).into();
617
        assert_eq!(method.name, "test_method".into());
618
        assert_eq!(method.docs.0.len(), 2);
619
        assert_eq!(
620
            method.params,
621
            vec![Parameter {
622
                name: "foo".into(),
623
                ty: Option::Some(DataType::Long),
624
                nullable: false,
625
                variadic: false,
626
                default: Option::None,
627
            }]
628
            .into()
629
        );
630
        assert_eq!(
631
            method.retval,
632
            Option::Some(Retval {
633
                ty: DataType::Bool,
634
                nullable: true,
635
            })
636
        );
637
        assert!(method.r#static);
638
        assert_eq!(method.visibility, Visibility::Protected);
639
        assert_eq!(method.ty, MethodType::Static);
640
    }
641

642
    #[test]
643
    fn test_ty_from() {
644
        let r#static: MethodType = MethodFlags::Static.into();
645
        assert_eq!(r#static, MethodType::Static);
646

647
        let constructor: MethodType = MethodFlags::IsConstructor.into();
648
        assert_eq!(constructor, MethodType::Constructor);
649

650
        let member: MethodType = MethodFlags::Public.into();
651
        assert_eq!(member, MethodType::Member);
652

653
        let mixed: MethodType = (MethodFlags::Protected | MethodFlags::Static).into();
654
        assert_eq!(mixed, MethodType::Static);
655

656
        let both: MethodType = (MethodFlags::Static | MethodFlags::IsConstructor).into();
657
        assert_eq!(both, MethodType::Constructor);
658

659
        let empty: MethodType = MethodFlags::empty().into();
660
        assert_eq!(empty, MethodType::Member);
661
    }
662

663
    #[test]
664
    fn test_prop_visibility_from() {
665
        let private: Visibility = PropertyFlags::Private.into();
666
        assert_eq!(private, Visibility::Private);
667

668
        let protected: Visibility = PropertyFlags::Protected.into();
669
        assert_eq!(protected, Visibility::Protected);
670

671
        let public: Visibility = PropertyFlags::Public.into();
672
        assert_eq!(public, Visibility::Public);
673

674
        let mixed: Visibility = (PropertyFlags::Protected | PropertyFlags::Static).into();
675
        assert_eq!(mixed, Visibility::Protected);
676

677
        let empty: Visibility = PropertyFlags::empty().into();
678
        assert_eq!(empty, Visibility::Public);
679
    }
680

681
    #[test]
682
    fn test_method_visibility_from() {
683
        let private: Visibility = MethodFlags::Private.into();
684
        assert_eq!(private, Visibility::Private);
685

686
        let protected: Visibility = MethodFlags::Protected.into();
687
        assert_eq!(protected, Visibility::Protected);
688

689
        let public: Visibility = MethodFlags::Public.into();
690
        assert_eq!(public, Visibility::Public);
691

692
        let mixed: Visibility = (MethodFlags::Protected | MethodFlags::Static).into();
693
        assert_eq!(mixed, Visibility::Protected);
694

695
        let empty: Visibility = MethodFlags::empty().into();
696
        assert_eq!(empty, Visibility::Public);
697
    }
698
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc