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

davidcole1340 / ext-php-rs / 16273052792

14 Jul 2025 05:01PM UTC coverage: 22.552% (+0.2%) from 22.325%
16273052792

Pull #511

github

Xenira
feat(stubs): add stubs for `RustClosure`

Fixes: #373
Pull Request #511: feat(stubs): add stubs for `RustClosure`

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

1 existing line in 1 file 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

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

3
use crate::flags::DataType;
4
use std::{cmp::Ordering, collections::HashMap};
5

6
use super::{
7
    abi::{Option, RString},
8
    Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
9
    Visibility,
10
};
11
use std::fmt::{Error as FmtError, Result as FmtResult, Write};
12
use std::{option::Option as StdOption, vec::Vec as StdVec};
13

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

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

48
impl ToStub for Module {
49
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
50
        writeln!(buf, "<?php")?;
×
51
        writeln!(buf)?;
×
52
        writeln!(buf, "// Stubs for {}", self.name.as_ref())?;
×
53
        writeln!(buf)?;
×
54

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

59
        // Inserts a value into the entries hashmap. Takes a key and an entry, creating
60
        // the internal vector if it doesn't already exist.
61
        let mut insert = |ns, entry| {
×
62
            let bucket = entries.entry(ns).or_default();
×
63
            bucket.push(entry);
×
64
        };
65

66
        for c in &*self.constants {
×
67
            let (ns, _) = split_namespace(c.name.as_ref());
×
68
            insert(ns, c.to_stub()?);
×
69
        }
70

71
        for func in &*self.functions {
×
72
            let (ns, _) = split_namespace(func.name.as_ref());
×
73
            insert(ns, func.to_stub()?);
×
74
        }
75

76
        for class in &*self.classes {
×
77
            let (ns, _) = split_namespace(class.name.as_ref());
×
78
            insert(ns, class.to_stub()?);
×
79
        }
80

81
        let mut entries: StdVec<_> = entries.iter().collect();
×
82
        entries.sort_by(|(l, _), (r, _)| match (l, r) {
×
83
            (None, _) => Ordering::Greater,
×
84
            (_, None) => Ordering::Less,
×
85
            (Some(l), Some(r)) => l.cmp(r),
×
86
        });
87

88
        buf.push_str(
×
89
            &entries
×
90
                .into_iter()
×
91
                .map(|(ns, entries)| {
×
92
                    let mut buf = String::new();
×
93
                    if let Some(ns) = ns {
×
94
                        writeln!(buf, "namespace {ns} {{")?;
×
95
                    } else {
96
                        writeln!(buf, "namespace {{")?;
×
97
                    }
98

99
                    buf.push_str(
×
100
                        &entries
×
101
                            .iter()
×
102
                            .map(|entry| indent(entry, 4))
×
103
                            .collect::<StdVec<_>>()
×
104
                            .join(NEW_LINE_SEPARATOR),
×
105
                    );
106

107
                    writeln!(buf, "}}")?;
×
108
                    Ok(buf)
×
109
                })
110
                .collect::<Result<StdVec<_>, FmtError>>()?
×
111
                .join(NEW_LINE_SEPARATOR),
×
112
        );
113

114
        Ok(())
×
115
    }
116
}
117

118
impl ToStub for Function {
119
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
120
        self.docs.fmt_stub(buf)?;
×
121

122
        let (_, name) = split_namespace(self.name.as_ref());
×
123
        write!(
×
124
            buf,
×
125
            "function {}({})",
126
            name,
127
            self.params
×
128
                .iter()
×
129
                .map(ToStub::to_stub)
×
130
                .collect::<Result<StdVec<_>, FmtError>>()?
×
131
                .join(", ")
×
132
        )?;
133

134
        if let Option::Some(retval) = &self.ret {
×
135
            write!(buf, ": ")?;
×
136
            if retval.nullable {
×
137
                write!(buf, "?")?;
×
138
            }
139
            retval.ty.fmt_stub(buf)?;
×
140
        }
141

142
        writeln!(buf, " {{}}")
×
143
    }
144
}
145

146
impl ToStub for Parameter {
147
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
148
        if let Option::Some(ty) = &self.ty {
×
149
            if self.nullable {
×
150
                write!(buf, "?")?;
×
151
            }
152

153
            ty.fmt_stub(buf)?;
×
154
            write!(buf, " ")?;
×
155
        }
156

NEW
157
        if self.variadic {
×
NEW
158
            write!(buf, "...")?;
×
159
        }
160

UNCOV
161
        write!(buf, "${}", self.name)
×
162
    }
163
}
164

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

192
impl ToStub for DocBlock {
193
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
194
        if !self.0.is_empty() {
×
195
            writeln!(buf, "/**")?;
×
196
            for comment in self.0.iter() {
×
197
                writeln!(buf, " *{comment}")?;
×
198
            }
199
            writeln!(buf, " */")?;
×
200
        }
201
        Ok(())
×
202
    }
203
}
204

205
impl ToStub for Class {
206
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
207
        fn stub<T: ToStub>(items: &[T]) -> impl Iterator<Item = Result<String, FmtError>> + '_ {
×
208
            items
×
209
                .iter()
210
                .map(|item| item.to_stub().map(|stub| indent(&stub, 4)))
×
211
        }
212

213
        self.docs.fmt_stub(buf)?;
×
214

215
        let (_, name) = split_namespace(self.name.as_ref());
×
216
        write!(buf, "class {name} ")?;
×
217

218
        if let Option::Some(extends) = &self.extends {
×
219
            write!(buf, "extends {extends} ")?;
×
220
        }
221

222
        if !self.implements.is_empty() {
×
223
            write!(
×
224
                buf,
×
225
                "implements {} ",
226
                self.implements
×
227
                    .iter()
×
228
                    .map(RString::as_str)
×
229
                    .collect::<StdVec<_>>()
×
230
                    .join(", ")
×
231
            )?;
232
        }
233

234
        writeln!(buf, "{{")?;
×
235

236
        buf.push_str(
×
237
            &stub(&self.constants)
×
238
                .chain(stub(&self.properties))
×
239
                .chain(stub(&self.methods))
×
240
                .collect::<Result<StdVec<_>, FmtError>>()?
×
241
                .join(NEW_LINE_SEPARATOR),
×
242
        );
243

244
        writeln!(buf, "}}")
×
245
    }
246
}
247

248
impl ToStub for Property {
249
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
250
        self.docs.fmt_stub(buf)?;
×
251
        self.vis.fmt_stub(buf)?;
×
252

253
        write!(buf, " ")?;
×
254

255
        if self.static_ {
×
256
            write!(buf, "static ")?;
×
257
        }
258
        if let Option::Some(ty) = &self.ty {
×
259
            ty.fmt_stub(buf)?;
×
260
        }
261
        write!(buf, "${}", self.name)?;
×
262
        if let Option::Some(default) = &self.default {
×
263
            write!(buf, " = {default}")?;
×
264
        }
265
        writeln!(buf, ";")
×
266
    }
267
}
268

269
impl ToStub for Visibility {
270
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
271
        write!(
×
272
            buf,
×
273
            "{}",
274
            match self {
×
275
                Visibility::Private => "private",
×
276
                Visibility::Protected => "protected",
×
277
                Visibility::Public => "public",
×
278
            }
279
        )
280
    }
281
}
282

283
impl ToStub for Method {
284
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
285
        self.docs.fmt_stub(buf)?;
×
286
        self.visibility.fmt_stub(buf)?;
×
287

288
        write!(buf, " ")?;
×
289

290
        if matches!(self.ty, MethodType::Static) {
×
291
            write!(buf, "static ")?;
×
292
        }
293

294
        write!(
×
295
            buf,
×
296
            "function {}({})",
297
            self.name,
298
            self.params
×
299
                .iter()
×
300
                .map(ToStub::to_stub)
×
301
                .collect::<Result<StdVec<_>, FmtError>>()?
×
302
                .join(", ")
×
303
        )?;
304

305
        if !matches!(self.ty, MethodType::Constructor) {
×
306
            if let Option::Some(retval) = &self.retval {
×
307
                write!(buf, ": ")?;
×
308
                if retval.nullable {
×
309
                    write!(buf, "?")?;
×
310
                }
311
                retval.ty.fmt_stub(buf)?;
×
312
            }
313
        }
314

315
        writeln!(buf, " {{}}")
×
316
    }
317
}
318

319
impl ToStub for Constant {
320
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
×
321
        self.docs.fmt_stub(buf)?;
×
322

323
        write!(buf, "const {} = ", self.name)?;
×
324
        if let Option::Some(value) = &self.value {
×
325
            write!(buf, "{value}")?;
×
326
        } else {
327
            write!(buf, "null")?;
×
328
        }
329
        writeln!(buf, ";")
×
330
    }
331
}
332

333
#[cfg(windows)]
334
const NEW_LINE_SEPARATOR: &str = "\r\n";
335
#[cfg(not(windows))]
336
const NEW_LINE_SEPARATOR: &str = "\n";
337

338
/// Takes a class name and splits the namespace off from the actual class name.
339
///
340
/// # Returns
341
///
342
/// A tuple, where the first item is the namespace (or [`None`] if not
343
/// namespaced), and the second item is the class name.
344
fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
3✔
345
    let idx = class.rfind('\\');
9✔
346

347
    if let Some(idx) = idx {
5✔
348
        (Some(&class[0..idx]), &class[idx + 1..])
349
    } else {
350
        (None, class)
1✔
351
    }
352
}
353

354
/// Indents a given string to a given depth. Depth is given in number of spaces
355
/// to be appended. Returns a new string with the new indentation. Will not
356
/// indent whitespace lines.
357
///
358
/// # Parameters
359
///
360
/// * `s` - The string to indent.
361
/// * `depth` - The depth to indent the lines to, in spaces.
362
///
363
/// # Returns
364
///
365
/// The indented string.
366
fn indent(s: &str, depth: usize) -> String {
2✔
367
    let indent = format!("{:depth$}", "", depth = depth);
6✔
368

369
    s.split('\n')
4✔
370
        .map(|line| {
6✔
371
            let mut result = String::new();
8✔
372
            if line.chars().any(|c| !c.is_whitespace()) {
17✔
373
                result.push_str(&indent);
12✔
374
                result.push_str(line);
6✔
375
            }
376
            result
4✔
377
        })
378
        .collect::<StdVec<_>>()
379
        .join(NEW_LINE_SEPARATOR)
4✔
380
}
381

382
#[cfg(test)]
383
mod test {
384
    use super::split_namespace;
385

386
    #[test]
387
    pub fn test_split_ns() {
388
        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
389
        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
390
        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
391
    }
392

393
    #[test]
394
    #[cfg(not(windows))]
395
    #[allow(clippy::uninlined_format_args)]
396
    pub fn test_indent() {
397
        use super::indent;
398
        use crate::describe::stub::NEW_LINE_SEPARATOR;
399

400
        assert_eq!(indent("hello", 4), "    hello");
401
        assert_eq!(
402
            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
403
            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
404
        );
405
    }
406
}
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