Skip to main content

ext_php_rs/describe/
stub.rs

1//! Traits and implementations to convert describe units into PHP stub code.
2
3use 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
11use super::{
12    Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
13    Visibility,
14    abi::{Option, RString},
15};
16
17#[cfg(feature = "enum")]
18use crate::describe::{Enum, EnumCase};
19use crate::flags::{ClassFlags, DataType};
20
21/// Implemented on types which can be converted into PHP stubs.
22pub 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
55impl 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
131impl 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
159impl 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        // Add default value to stub
177        if let Option::Some(default) = &self.default {
178            write!(buf, " = {default}")?;
179        } else if self.nullable {
180            // For nullable parameters without explicit default, add = null
181            // This makes Option<T> parameters truly optional in PHP
182            write!(buf, " = null")?;
183        }
184
185        Ok(())
186    }
187}
188
189impl ToStub for DataType {
190    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
191        let mut fqdn = "\\".to_owned();
192        write!(
193            buf,
194            "{}",
195            match self {
196                DataType::Bool | DataType::True | DataType::False => "bool",
197                DataType::Long => "int",
198                DataType::Double => "float",
199                DataType::String => "string",
200                DataType::Array => "array",
201                DataType::Object(Some(ty)) => {
202                    fqdn.push_str(ty);
203                    fqdn.as_str()
204                }
205                DataType::Object(None) => "object",
206                DataType::Resource => "resource",
207                DataType::Reference => "reference",
208                DataType::Callable => "callable",
209                DataType::Iterable => "iterable",
210                DataType::Void => "void",
211                DataType::Null => "null",
212                DataType::Mixed
213                | DataType::Undef
214                | DataType::Ptr
215                | DataType::Indirect
216                | DataType::ConstantExpression => "mixed",
217            }
218        )
219    }
220}
221
222impl ToStub for DocBlock {
223    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
224        if !self.0.is_empty() {
225            writeln!(buf, "/**")?;
226            for comment in self.0.iter() {
227                writeln!(buf, " *{comment}")?;
228            }
229            writeln!(buf, " */")?;
230        }
231        Ok(())
232    }
233}
234
235impl ToStub for Class {
236    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
237        fn stub<T: ToStub>(items: &[T]) -> impl Iterator<Item = Result<String, FmtError>> + '_ {
238            items
239                .iter()
240                .map(|item| item.to_stub().map(|stub| indent(&stub, 4)))
241        }
242
243        self.docs.fmt_stub(buf)?;
244
245        let (_, name) = split_namespace(self.name.as_ref());
246        let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty());
247        let is_interface = flags.contains(ClassFlags::Interface);
248
249        if is_interface {
250            write!(buf, "interface {name} ")?;
251        } else {
252            write!(buf, "class {name} ")?;
253        }
254
255        if let Option::Some(extends) = &self.extends {
256            write!(buf, "extends {extends} ")?;
257        }
258
259        if !self.implements.is_empty() && !is_interface {
260            write!(
261                buf,
262                "implements {} ",
263                self.implements
264                    .iter()
265                    .map(RString::as_str)
266                    .collect::<StdVec<_>>()
267                    .join(", ")
268            )?;
269        }
270
271        if !self.implements.is_empty() && is_interface {
272            write!(
273                buf,
274                "extends {} ",
275                self.implements
276                    .iter()
277                    .map(RString::as_str)
278                    .collect::<StdVec<_>>()
279                    .join(", ")
280            )?;
281        }
282
283        writeln!(buf, "{{")?;
284
285        buf.push_str(
286            &stub(&self.constants)
287                .chain(stub(&self.properties))
288                .chain(stub(&self.methods))
289                .collect::<Result<StdVec<_>, FmtError>>()?
290                .join(NEW_LINE_SEPARATOR),
291        );
292
293        writeln!(buf, "}}")
294    }
295}
296
297#[cfg(feature = "enum")]
298impl ToStub for Enum {
299    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
300        self.docs.fmt_stub(buf)?;
301
302        let (_, name) = split_namespace(self.name.as_ref());
303        write!(buf, "enum {name}")?;
304
305        if let Option::Some(backing_type) = &self.backing_type {
306            write!(buf, ": {backing_type}")?;
307        }
308
309        writeln!(buf, " {{")?;
310
311        for case in self.cases.iter() {
312            case.fmt_stub(buf)?;
313        }
314
315        writeln!(buf, "}}")
316    }
317}
318
319#[cfg(feature = "enum")]
320impl ToStub for EnumCase {
321    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
322        self.docs.fmt_stub(buf)?;
323
324        write!(buf, "  case {}", self.name)?;
325        if let Option::Some(value) = &self.value {
326            write!(buf, " = {value}")?;
327        }
328        writeln!(buf, ";")
329    }
330}
331
332impl ToStub for Property {
333    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
334        self.docs.fmt_stub(buf)?;
335        self.vis.fmt_stub(buf)?;
336
337        write!(buf, " ")?;
338
339        if self.static_ {
340            write!(buf, "static ")?;
341        }
342        if let Option::Some(ty) = &self.ty {
343            ty.fmt_stub(buf)?;
344        }
345        write!(buf, "${}", self.name)?;
346        if let Option::Some(default) = &self.default {
347            write!(buf, " = {default}")?;
348        }
349        writeln!(buf, ";")
350    }
351}
352
353impl ToStub for Visibility {
354    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
355        write!(
356            buf,
357            "{}",
358            match self {
359                Visibility::Private => "private",
360                Visibility::Protected => "protected",
361                Visibility::Public => "public",
362            }
363        )
364    }
365}
366
367impl ToStub for Method {
368    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
369        self.docs.fmt_stub(buf)?;
370        self.visibility.fmt_stub(buf)?;
371
372        write!(buf, " ")?;
373
374        if matches!(self.ty, MethodType::Static) {
375            write!(buf, "static ")?;
376        }
377
378        write!(
379            buf,
380            "function {}({})",
381            self.name,
382            self.params
383                .iter()
384                .map(ToStub::to_stub)
385                .collect::<Result<StdVec<_>, FmtError>>()?
386                .join(", ")
387        )?;
388
389        if !matches!(self.ty, MethodType::Constructor)
390            && let Option::Some(retval) = &self.retval
391        {
392            write!(buf, ": ")?;
393            if retval.nullable {
394                write!(buf, "?")?;
395            }
396            retval.ty.fmt_stub(buf)?;
397        }
398
399        if self.r#abstract {
400            writeln!(buf, ";")
401        } else {
402            writeln!(buf, " {{}}")
403        }
404    }
405}
406
407impl ToStub for Constant {
408    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
409        self.docs.fmt_stub(buf)?;
410
411        write!(buf, "const {} = ", self.name)?;
412        if let Option::Some(value) = &self.value {
413            write!(buf, "{value}")?;
414        } else {
415            write!(buf, "null")?;
416        }
417        writeln!(buf, ";")
418    }
419}
420
421#[cfg(windows)]
422const NEW_LINE_SEPARATOR: &str = "\r\n";
423#[cfg(not(windows))]
424const NEW_LINE_SEPARATOR: &str = "\n";
425
426/// Takes a class name and splits the namespace off from the actual class name.
427///
428/// # Returns
429///
430/// A tuple, where the first item is the namespace (or [`None`] if not
431/// namespaced), and the second item is the class name.
432fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
433    let idx = class.rfind('\\');
434
435    if let Some(idx) = idx {
436        (Some(&class[0..idx]), &class[idx + 1..])
437    } else {
438        (None, class)
439    }
440}
441
442/// Indents a given string to a given depth. Depth is given in number of spaces
443/// to be appended. Returns a new string with the new indentation. Will not
444/// indent whitespace lines.
445///
446/// # Parameters
447///
448/// * `s` - The string to indent.
449/// * `depth` - The depth to indent the lines to, in spaces.
450///
451/// # Returns
452///
453/// The indented string.
454fn indent(s: &str, depth: usize) -> String {
455    let indent = format!("{:depth$}", "", depth = depth);
456
457    s.split('\n')
458        .map(|line| {
459            let mut result = String::new();
460            if line.chars().any(|c| !c.is_whitespace()) {
461                result.push_str(&indent);
462                result.push_str(line);
463            }
464            result
465        })
466        .collect::<StdVec<_>>()
467        .join(NEW_LINE_SEPARATOR)
468}
469
470#[cfg(test)]
471mod test {
472    use super::{ToStub, split_namespace};
473    use crate::flags::DataType;
474
475    #[test]
476    pub fn test_split_ns() {
477        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
478        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
479        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
480    }
481
482    #[test]
483    #[cfg(not(windows))]
484    #[allow(clippy::uninlined_format_args)]
485    pub fn test_indent() {
486        use super::indent;
487        use crate::describe::stub::NEW_LINE_SEPARATOR;
488
489        assert_eq!(indent("hello", 4), "    hello");
490        assert_eq!(
491            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
492            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
493        );
494    }
495
496    #[test]
497    #[allow(clippy::unwrap_used)]
498    pub fn test_datatype_to_stub() {
499        // Test that all DataType variants produce correct PHP type strings
500        assert_eq!(DataType::Void.to_stub().unwrap(), "void");
501        assert_eq!(DataType::Null.to_stub().unwrap(), "null");
502        assert_eq!(DataType::Bool.to_stub().unwrap(), "bool");
503        assert_eq!(DataType::True.to_stub().unwrap(), "bool");
504        assert_eq!(DataType::False.to_stub().unwrap(), "bool");
505        assert_eq!(DataType::Long.to_stub().unwrap(), "int");
506        assert_eq!(DataType::Double.to_stub().unwrap(), "float");
507        assert_eq!(DataType::String.to_stub().unwrap(), "string");
508        assert_eq!(DataType::Array.to_stub().unwrap(), "array");
509        assert_eq!(DataType::Object(None).to_stub().unwrap(), "object");
510        assert_eq!(
511            DataType::Object(Some("Foo\\Bar")).to_stub().unwrap(),
512            "\\Foo\\Bar"
513        );
514        assert_eq!(DataType::Resource.to_stub().unwrap(), "resource");
515        assert_eq!(DataType::Callable.to_stub().unwrap(), "callable");
516        assert_eq!(DataType::Iterable.to_stub().unwrap(), "iterable");
517        assert_eq!(DataType::Mixed.to_stub().unwrap(), "mixed");
518        assert_eq!(DataType::Undef.to_stub().unwrap(), "mixed");
519        assert_eq!(DataType::Ptr.to_stub().unwrap(), "mixed");
520        assert_eq!(DataType::Indirect.to_stub().unwrap(), "mixed");
521        assert_eq!(DataType::ConstantExpression.to_stub().unwrap(), "mixed");
522        assert_eq!(DataType::Reference.to_stub().unwrap(), "reference");
523    }
524}