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

davidcole1340 / ext-php-rs / 18244744117

04 Oct 2025 01:08PM UTC coverage: 30.759% (+3.0%) from 27.728%
18244744117

Pull #533

github

web-flow
Merge 0936605ac into 113ef33a0
Pull Request #533: Feat/interface impl

68 of 152 new or added lines in 9 files covered. (44.74%)

3 existing lines in 2 files now uncovered.

1326 of 4311 relevant lines covered (30.76%)

7.67 hits per line

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

6.51
/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::{ClassFlags, 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")]
89
        for r#enum in &*self.enums {
×
90
            let (ns, _) = split_namespace(r#enum.name.as_ref());
×
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
        if self.variadic {
×
171
            write!(buf, "...")?;
×
172
        }
173

174
        write!(buf, "${}", self.name)
×
175
    }
176
}
177

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

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

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

226
        self.docs.fmt_stub(buf)?;
×
227

228
        let (_, name) = split_namespace(self.name.as_ref());
×
NEW
229
        let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty());
×
NEW
230
        let is_interface = flags.contains(ClassFlags::Interface);
×
231

NEW
232
        if is_interface {
×
NEW
233
            write!(buf, "interface {name} ")?;
×
234
        } else {
NEW
235
            write!(buf, "class {name} ")?;
×
236
        }
237

238
        if let Option::Some(extends) = &self.extends {
×
239
            write!(buf, "extends {extends} ")?;
×
240
        }
241

NEW
242
        if !self.implements.is_empty() && !is_interface {
×
243
            write!(
×
244
                buf,
×
245
                "implements {} ",
246
                self.implements
×
247
                    .iter()
×
248
                    .map(RString::as_str)
×
249
                    .collect::<StdVec<_>>()
×
250
                    .join(", ")
×
251
            )?;
252
        }
253

NEW
254
        if !self.implements.is_empty() && is_interface {
×
NEW
255
            write!(
×
NEW
256
                buf,
×
257
                "extends {} ",
NEW
258
                self.implements
×
NEW
259
                    .iter()
×
NEW
260
                    .map(RString::as_str)
×
NEW
261
                    .collect::<StdVec<_>>()
×
NEW
262
                    .join(", ")
×
263
            )?;
264
        }
265

UNCOV
266
        writeln!(buf, "{{")?;
×
267

268
        buf.push_str(
×
269
            &stub(&self.constants)
×
270
                .chain(stub(&self.properties))
×
271
                .chain(stub(&self.methods))
×
272
                .collect::<Result<StdVec<_>, FmtError>>()?
×
273
                .join(NEW_LINE_SEPARATOR),
×
274
        );
275

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

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

285
        let (_, name) = split_namespace(self.name.as_ref());
×
286
        write!(buf, "enum {name}")?;
×
287

288
        if let Option::Some(backing_type) = &self.backing_type {
×
289
            write!(buf, ": {backing_type}")?;
×
290
        }
291

292
        writeln!(buf, " {{")?;
×
293

294
        for case in self.cases.iter() {
×
295
            case.fmt_stub(buf)?;
×
296
        }
297

298
        writeln!(buf, "}}")
×
299
    }
300
}
301

302
#[cfg(feature = "enum")]
303
impl ToStub for EnumCase {
304
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
305
        self.docs.fmt_stub(buf)?;
×
306

307
        write!(buf, "  case {}", self.name)?;
×
308
        if let Option::Some(value) = &self.value {
×
309
            write!(buf, " = {value}")?;
×
310
        }
311
        writeln!(buf, ";")
×
312
    }
313
}
314

315
impl ToStub for Property {
316
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
317
        self.docs.fmt_stub(buf)?;
×
318
        self.vis.fmt_stub(buf)?;
×
319

320
        write!(buf, " ")?;
×
321

322
        if self.static_ {
×
323
            write!(buf, "static ")?;
×
324
        }
325
        if let Option::Some(ty) = &self.ty {
×
326
            ty.fmt_stub(buf)?;
×
327
        }
328
        write!(buf, "${}", self.name)?;
×
329
        if let Option::Some(default) = &self.default {
×
330
            write!(buf, " = {default}")?;
×
331
        }
332
        writeln!(buf, ";")
×
333
    }
334
}
335

336
impl ToStub for Visibility {
337
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
338
        write!(
×
339
            buf,
×
340
            "{}",
341
            match self {
×
342
                Visibility::Private => "private",
×
343
                Visibility::Protected => "protected",
×
344
                Visibility::Public => "public",
×
345
            }
346
        )
347
    }
348
}
349

350
impl ToStub for Method {
351
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
352
        self.docs.fmt_stub(buf)?;
×
353
        self.visibility.fmt_stub(buf)?;
×
354

355
        write!(buf, " ")?;
×
356

357
        if matches!(self.ty, MethodType::Static) {
×
358
            write!(buf, "static ")?;
×
359
        }
360

361
        write!(
×
362
            buf,
×
363
            "function {}({})",
364
            self.name,
365
            self.params
×
366
                .iter()
×
367
                .map(ToStub::to_stub)
×
368
                .collect::<Result<StdVec<_>, FmtError>>()?
×
369
                .join(", ")
×
370
        )?;
371

372
        if !matches!(self.ty, MethodType::Constructor) {
×
373
            if let Option::Some(retval) = &self.retval {
×
374
                write!(buf, ": ")?;
×
375
                if retval.nullable {
×
376
                    write!(buf, "?")?;
×
377
                }
378
                retval.ty.fmt_stub(buf)?;
×
379
            }
380
        }
381

NEW
382
        if self.r#abstract {
×
NEW
383
            writeln!(buf, ";")
×
384
        } else {
NEW
385
            writeln!(buf, " {{}}")
×
386
        }
387
    }
388
}
389

390
impl ToStub for Constant {
391
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
392
        self.docs.fmt_stub(buf)?;
×
393

394
        write!(buf, "const {} = ", self.name)?;
×
395
        if let Option::Some(value) = &self.value {
×
396
            write!(buf, "{value}")?;
×
397
        } else {
398
            write!(buf, "null")?;
×
399
        }
400
        writeln!(buf, ";")
×
401
    }
402
}
403

404
#[cfg(windows)]
405
const NEW_LINE_SEPARATOR: &str = "\r\n";
406
#[cfg(not(windows))]
407
const NEW_LINE_SEPARATOR: &str = "\n";
408

409
/// Takes a class name and splits the namespace off from the actual class name.
410
///
411
/// # Returns
412
///
413
/// A tuple, where the first item is the namespace (or [`None`] if not
414
/// namespaced), and the second item is the class name.
415
fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
3✔
416
    let idx = class.rfind('\\');
9✔
417

418
    if let Some(idx) = idx {
5✔
419
        (Some(&class[0..idx]), &class[idx + 1..])
420
    } else {
421
        (None, class)
1✔
422
    }
423
}
424

425
/// Indents a given string to a given depth. Depth is given in number of spaces
426
/// to be appended. Returns a new string with the new indentation. Will not
427
/// indent whitespace lines.
428
///
429
/// # Parameters
430
///
431
/// * `s` - The string to indent.
432
/// * `depth` - The depth to indent the lines to, in spaces.
433
///
434
/// # Returns
435
///
436
/// The indented string.
437
fn indent(s: &str, depth: usize) -> String {
2✔
438
    let indent = format!("{:depth$}", "", depth = depth);
6✔
439

440
    s.split('\n')
4✔
441
        .map(|line| {
6✔
442
            let mut result = String::new();
8✔
443
            if line.chars().any(|c| !c.is_whitespace()) {
17✔
444
                result.push_str(&indent);
12✔
445
                result.push_str(line);
6✔
446
            }
447
            result
4✔
448
        })
449
        .collect::<StdVec<_>>()
450
        .join(NEW_LINE_SEPARATOR)
4✔
451
}
452

453
#[cfg(test)]
454
mod test {
455
    use super::split_namespace;
456

457
    #[test]
458
    pub fn test_split_ns() {
459
        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
460
        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
461
        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
462
    }
463

464
    #[test]
465
    #[cfg(not(windows))]
466
    #[allow(clippy::uninlined_format_args)]
467
    pub fn test_indent() {
468
        use super::indent;
469
        use crate::describe::stub::NEW_LINE_SEPARATOR;
470

471
        assert_eq!(indent("hello", 4), "    hello");
472
        assert_eq!(
473
            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
474
            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
475
        );
476
    }
477
}
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