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

davidcole1340 / ext-php-rs / 16398853116

20 Jul 2025 10:21AM UTC coverage: 26.874% (+1.1%) from 25.766%
16398853116

Pull #489

github

Xenira
test(enum): add enum testcases

Refs: #178
Pull Request #489: feat(enum): add basic enum support

118 of 299 new or added lines in 8 files covered. (39.46%)

1 existing line in 1 file now uncovered.

1115 of 4149 relevant lines covered (26.87%)

5.63 hits per line

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

7.04
/src/describe/stub.rs
1
//! Traits and implementations to convert describe units into PHP stub code.
2

3
use std::{
4
    cmp::Ordering,
5
    collections::HashMap,
6
    fmt::{Error as FmtError, Result as FmtResult, Write},
7
    option::Option as StdOption,
8
    vec::Vec as StdVec,
9
};
10

11
use super::{
12
    abi::{Option, RString},
13
    Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
14
    Visibility,
15
};
16

17
#[cfg(feature = "enum")]
18
use crate::describe::{Enum, EnumCase};
19
use crate::flags::DataType;
20

21
/// Implemented on types which can be converted into PHP stubs.
22
pub trait ToStub {
23
    /// Converts the implementor into PHP code, represented as a PHP stub.
24
    /// Returned as a string.
25
    ///
26
    /// # Returns
27
    ///
28
    /// Returns a string on success.
29
    ///
30
    /// # Errors
31
    ///
32
    /// Returns an error if there was an error writing into the string.
33
    fn to_stub(&self) -> Result<String, FmtError> {
×
34
        let mut buf = String::new();
×
35
        self.fmt_stub(&mut buf)?;
×
36
        Ok(buf)
×
37
    }
38

39
    /// Converts the implementor into PHP code, represented as a PHP stub.
40
    ///
41
    /// # Parameters
42
    ///
43
    /// * `buf` - The buffer to write the PHP code into.
44
    ///
45
    /// # Returns
46
    ///
47
    /// Returns nothing on success.
48
    ///
49
    /// # Errors
50
    ///
51
    /// Returns an error if there was an error writing into the buffer.
52
    fn fmt_stub(&self, buf: &mut String) -> FmtResult;
53
}
54

55
impl ToStub for Module {
56
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
57
        writeln!(buf, "<?php")?;
×
58
        writeln!(buf)?;
×
59
        writeln!(buf, "// Stubs for {}", self.name.as_ref())?;
×
60
        writeln!(buf)?;
×
61

62
        // To account for namespaces we need to group by them. [`None`] as the key
63
        // represents no namespace, while [`Some`] represents a namespace.
64
        let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
×
65

66
        // Inserts a value into the entries hashmap. Takes a key and an entry, creating
67
        // the internal vector if it doesn't already exist.
68
        let mut insert = |ns, entry| {
×
69
            let bucket = entries.entry(ns).or_default();
×
70
            bucket.push(entry);
×
71
        };
72

73
        for c in &*self.constants {
×
74
            let (ns, _) = split_namespace(c.name.as_ref());
×
75
            insert(ns, c.to_stub()?);
×
76
        }
77

78
        for func in &*self.functions {
×
79
            let (ns, _) = split_namespace(func.name.as_ref());
×
80
            insert(ns, func.to_stub()?);
×
81
        }
82

83
        for class in &*self.classes {
×
84
            let (ns, _) = split_namespace(class.name.as_ref());
×
85
            insert(ns, class.to_stub()?);
×
86
        }
87

88
        #[cfg(feature = "enum")]
NEW
89
        for r#enum in &*self.enums {
×
NEW
90
            let (ns, _) = split_namespace(r#enum.name.as_ref());
×
NEW
91
            insert(ns, r#enum.to_stub()?);
×
92
        }
93

94
        let mut entries: StdVec<_> = entries.iter().collect();
×
95
        entries.sort_by(|(l, _), (r, _)| match (l, r) {
×
96
            (None, _) => Ordering::Greater,
×
97
            (_, None) => Ordering::Less,
×
98
            (Some(l), Some(r)) => l.cmp(r),
×
99
        });
100

101
        buf.push_str(
×
102
            &entries
×
103
                .into_iter()
×
104
                .map(|(ns, entries)| {
×
105
                    let mut buf = String::new();
×
106
                    if let Some(ns) = ns {
×
107
                        writeln!(buf, "namespace {ns} {{")?;
×
108
                    } else {
109
                        writeln!(buf, "namespace {{")?;
×
110
                    }
111

112
                    buf.push_str(
×
113
                        &entries
×
114
                            .iter()
×
115
                            .map(|entry| indent(entry, 4))
×
116
                            .collect::<StdVec<_>>()
×
117
                            .join(NEW_LINE_SEPARATOR),
×
118
                    );
119

120
                    writeln!(buf, "}}")?;
×
121
                    Ok(buf)
×
122
                })
123
                .collect::<Result<StdVec<_>, FmtError>>()?
×
124
                .join(NEW_LINE_SEPARATOR),
×
125
        );
126

127
        Ok(())
×
128
    }
129
}
130

131
impl ToStub for Function {
132
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
133
        self.docs.fmt_stub(buf)?;
×
134

135
        let (_, name) = split_namespace(self.name.as_ref());
×
136
        write!(
×
137
            buf,
×
138
            "function {}({})",
139
            name,
140
            self.params
×
141
                .iter()
×
142
                .map(ToStub::to_stub)
×
143
                .collect::<Result<StdVec<_>, FmtError>>()?
×
144
                .join(", ")
×
145
        )?;
146

147
        if let Option::Some(retval) = &self.ret {
×
148
            write!(buf, ": ")?;
×
149
            if retval.nullable {
×
150
                write!(buf, "?")?;
×
151
            }
152
            retval.ty.fmt_stub(buf)?;
×
153
        }
154

155
        writeln!(buf, " {{}}")
×
156
    }
157
}
158

159
impl ToStub for Parameter {
160
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
161
        if let Option::Some(ty) = &self.ty {
×
162
            if self.nullable {
×
163
                write!(buf, "?")?;
×
164
            }
165

166
            ty.fmt_stub(buf)?;
×
167
            write!(buf, " ")?;
×
168
        }
169

170
        write!(buf, "${}", self.name)
×
171
    }
172
}
173

174
impl ToStub for DataType {
175
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
176
        let mut fqdn = "\\".to_owned();
×
177
        write!(
×
178
            buf,
×
179
            "{}",
180
            match self {
×
181
                DataType::Bool | DataType::True | DataType::False => "bool",
×
182
                DataType::Long => "int",
×
183
                DataType::Double => "float",
×
184
                DataType::String => "string",
×
185
                DataType::Array => "array",
×
186
                DataType::Object(Some(ty)) => {
×
187
                    fqdn.push_str(ty);
×
188
                    fqdn.as_str()
×
189
                }
190
                DataType::Object(None) => "object",
×
191
                DataType::Resource => "resource",
×
192
                DataType::Reference => "reference",
×
193
                DataType::Callable => "callable",
×
194
                DataType::Iterable => "iterable",
×
195
                _ => "mixed",
×
196
            }
197
        )
198
    }
199
}
200

201
impl ToStub for DocBlock {
202
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
203
        if !self.0.is_empty() {
×
204
            writeln!(buf, "/**")?;
×
205
            for comment in self.0.iter() {
×
206
                writeln!(buf, " *{comment}")?;
×
207
            }
208
            writeln!(buf, " */")?;
×
209
        }
210
        Ok(())
×
211
    }
212
}
213

214
impl ToStub for Class {
215
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
216
        fn stub<T: ToStub>(items: &[T]) -> impl Iterator<Item = Result<String, FmtError>> + '_ {
×
217
            items
×
218
                .iter()
219
                .map(|item| item.to_stub().map(|stub| indent(&stub, 4)))
×
220
        }
221

222
        self.docs.fmt_stub(buf)?;
×
223

224
        let (_, name) = split_namespace(self.name.as_ref());
×
225
        write!(buf, "class {name} ")?;
×
226

227
        if let Option::Some(extends) = &self.extends {
×
228
            write!(buf, "extends {extends} ")?;
×
229
        }
230

231
        if !self.implements.is_empty() {
×
232
            write!(
×
233
                buf,
×
234
                "implements {} ",
235
                self.implements
×
236
                    .iter()
×
237
                    .map(RString::as_str)
×
238
                    .collect::<StdVec<_>>()
×
239
                    .join(", ")
×
240
            )?;
241
        }
242

243
        writeln!(buf, "{{")?;
×
244

245
        buf.push_str(
×
246
            &stub(&self.constants)
×
247
                .chain(stub(&self.properties))
×
248
                .chain(stub(&self.methods))
×
249
                .collect::<Result<StdVec<_>, FmtError>>()?
×
250
                .join(NEW_LINE_SEPARATOR),
×
251
        );
252

253
        writeln!(buf, "}}")
×
254
    }
255
}
256

257
#[cfg(feature = "enum")]
258
impl ToStub for Enum {
NEW
259
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
NEW
260
        self.docs.fmt_stub(buf)?;
×
261

NEW
262
        let (_, name) = split_namespace(self.name.as_ref());
×
NEW
263
        write!(buf, "enum {name}")?;
×
264

NEW
265
        if let Option::Some(backing_type) = &self.backing_type {
×
NEW
266
            write!(buf, ": {backing_type}")?;
×
267
        }
268

NEW
269
        writeln!(buf, " {{")?;
×
270

NEW
271
        for case in self.cases.iter() {
×
NEW
272
            case.fmt_stub(buf)?;
×
273
        }
274

NEW
275
        writeln!(buf, "}}")
×
276
    }
277
}
278

279
#[cfg(feature = "enum")]
280
impl ToStub for EnumCase {
NEW
281
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
NEW
282
        self.docs.fmt_stub(buf)?;
×
283

NEW
284
        write!(buf, "  case {}", self.name)?;
×
NEW
285
        if let Option::Some(value) = &self.value {
×
NEW
286
            write!(buf, " = {value}")?;
×
287
        }
NEW
288
        writeln!(buf, ";")
×
289
    }
290
}
291

292
impl ToStub for Property {
293
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
294
        self.docs.fmt_stub(buf)?;
×
295
        self.vis.fmt_stub(buf)?;
×
296

297
        write!(buf, " ")?;
×
298

299
        if self.static_ {
×
300
            write!(buf, "static ")?;
×
301
        }
302
        if let Option::Some(ty) = &self.ty {
×
303
            ty.fmt_stub(buf)?;
×
304
        }
305
        write!(buf, "${}", self.name)?;
×
306
        if let Option::Some(default) = &self.default {
×
307
            write!(buf, " = {default}")?;
×
308
        }
309
        writeln!(buf, ";")
×
310
    }
311
}
312

313
impl ToStub for Visibility {
314
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
315
        write!(
×
316
            buf,
×
317
            "{}",
318
            match self {
×
319
                Visibility::Private => "private",
×
320
                Visibility::Protected => "protected",
×
321
                Visibility::Public => "public",
×
322
            }
323
        )
324
    }
325
}
326

327
impl ToStub for Method {
328
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
329
        self.docs.fmt_stub(buf)?;
×
330
        self.visibility.fmt_stub(buf)?;
×
331

332
        write!(buf, " ")?;
×
333

334
        if matches!(self.ty, MethodType::Static) {
×
335
            write!(buf, "static ")?;
×
336
        }
337

338
        write!(
×
339
            buf,
×
340
            "function {}({})",
341
            self.name,
342
            self.params
×
343
                .iter()
×
344
                .map(ToStub::to_stub)
×
345
                .collect::<Result<StdVec<_>, FmtError>>()?
×
346
                .join(", ")
×
347
        )?;
348

349
        if !matches!(self.ty, MethodType::Constructor) {
×
350
            if let Option::Some(retval) = &self.retval {
×
351
                write!(buf, ": ")?;
×
352
                if retval.nullable {
×
353
                    write!(buf, "?")?;
×
354
                }
355
                retval.ty.fmt_stub(buf)?;
×
356
            }
357
        }
358

359
        writeln!(buf, " {{}}")
×
360
    }
361
}
362

363
impl ToStub for Constant {
364
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
365
        self.docs.fmt_stub(buf)?;
×
366

367
        write!(buf, "const {} = ", self.name)?;
×
368
        if let Option::Some(value) = &self.value {
×
369
            write!(buf, "{value}")?;
×
370
        } else {
371
            write!(buf, "null")?;
×
372
        }
373
        writeln!(buf, ";")
×
374
    }
375
}
376

377
#[cfg(windows)]
378
const NEW_LINE_SEPARATOR: &str = "\r\n";
379
#[cfg(not(windows))]
380
const NEW_LINE_SEPARATOR: &str = "\n";
381

382
/// Takes a class name and splits the namespace off from the actual class name.
383
///
384
/// # Returns
385
///
386
/// A tuple, where the first item is the namespace (or [`None`] if not
387
/// namespaced), and the second item is the class name.
388
fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
3✔
389
    let idx = class.rfind('\\');
9✔
390

391
    if let Some(idx) = idx {
5✔
392
        (Some(&class[0..idx]), &class[idx + 1..])
393
    } else {
394
        (None, class)
1✔
395
    }
396
}
397

398
/// Indents a given string to a given depth. Depth is given in number of spaces
399
/// to be appended. Returns a new string with the new indentation. Will not
400
/// indent whitespace lines.
401
///
402
/// # Parameters
403
///
404
/// * `s` - The string to indent.
405
/// * `depth` - The depth to indent the lines to, in spaces.
406
///
407
/// # Returns
408
///
409
/// The indented string.
410
fn indent(s: &str, depth: usize) -> String {
2✔
411
    let indent = format!("{:depth$}", "", depth = depth);
6✔
412

413
    s.split('\n')
4✔
414
        .map(|line| {
6✔
415
            let mut result = String::new();
8✔
416
            if line.chars().any(|c| !c.is_whitespace()) {
17✔
417
                result.push_str(&indent);
12✔
418
                result.push_str(line);
6✔
419
            }
420
            result
4✔
421
        })
422
        .collect::<StdVec<_>>()
423
        .join(NEW_LINE_SEPARATOR)
4✔
424
}
425

426
#[cfg(test)]
427
mod test {
428
    use super::split_namespace;
429

430
    #[test]
431
    pub fn test_split_ns() {
432
        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
433
        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
434
        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
435
    }
436

437
    #[test]
438
    #[cfg(not(windows))]
439
    #[allow(clippy::uninlined_format_args)]
440
    pub fn test_indent() {
441
        use super::indent;
442
        use crate::describe::stub::NEW_LINE_SEPARATOR;
443

444
        assert_eq!(indent("hello", 4), "    hello");
445
        assert_eq!(
446
            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
447
            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
448
        );
449
    }
450
}
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