ext_php_rs/describe/
stub.rs

1//! Traits and implementations to convert describe units into PHP stub code.
2
3use crate::flags::DataType;
4use std::{cmp::Ordering, collections::HashMap};
5
6use super::{
7    abi::*, Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
8    Visibility,
9};
10use std::fmt::{Error as FmtError, Result as FmtResult, Write};
11use std::{option::Option as StdOption, vec::Vec as StdVec};
12
13/// Implemented on types which can be converted into PHP stubs.
14pub trait ToStub {
15    /// Converts the implementor into PHP code, represented as a PHP stub.
16    /// Returned as a string.
17    ///
18    /// # Returns
19    ///
20    /// Returns a string on success. Returns an error if there was an error
21    /// writing into the string.
22    fn to_stub(&self) -> Result<String, FmtError> {
23        let mut buf = String::new();
24        self.fmt_stub(&mut buf)?;
25        Ok(buf)
26    }
27
28    /// Converts the implementor into PHP code, represented as a PHP stub.
29    ///
30    /// # Parameters
31    ///
32    /// * `buf` - The buffer to write the PHP code into.
33    ///
34    /// # Returns
35    ///
36    /// Returns nothing on success. Returns an error if there was an error
37    /// writing into the buffer.
38    fn fmt_stub(&self, buf: &mut String) -> FmtResult;
39}
40
41impl ToStub for Module {
42    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
43        writeln!(buf, "<?php")?;
44        writeln!(buf)?;
45        writeln!(buf, "// Stubs for {}", self.name)?;
46        writeln!(buf)?;
47
48        // To account for namespaces we need to group by them. [`None`] as the key
49        // represents no namespace, while [`Some`] represents a namespace.
50        let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
51
52        // Inserts a value into the entries hashmap. Takes a key and an entry, creating
53        // the internal vector if it doesn't already exist.
54        let mut insert = |ns, entry| {
55            let bucket = entries.entry(ns).or_default();
56            bucket.push(entry);
57        };
58
59        for c in &*self.constants {
60            let (ns, _) = split_namespace(c.name.as_ref());
61            insert(ns, c.to_stub()?);
62        }
63
64        for func in &*self.functions {
65            let (ns, _) = split_namespace(func.name.as_ref());
66            insert(ns, func.to_stub()?);
67        }
68
69        for class in &*self.classes {
70            let (ns, _) = split_namespace(class.name.as_ref());
71            insert(ns, class.to_stub()?);
72        }
73
74        let mut entries: StdVec<_> = entries.iter().collect();
75        entries.sort_by(|(l, _), (r, _)| match (l, r) {
76            (None, _) => Ordering::Greater,
77            (_, None) => Ordering::Less,
78            (Some(l), Some(r)) => l.cmp(r),
79        });
80
81        buf.push_str(
82            &entries
83                .into_iter()
84                .map(|(ns, entries)| {
85                    let mut buf = String::new();
86                    if let Some(ns) = ns {
87                        writeln!(buf, "namespace {ns} {{")?;
88                    } else {
89                        writeln!(buf, "namespace {{")?;
90                    }
91
92                    buf.push_str(
93                        &entries
94                            .iter()
95                            .map(|entry| indent(entry, 4))
96                            .collect::<StdVec<_>>()
97                            .join(NEW_LINE_SEPARATOR),
98                    );
99
100                    writeln!(buf, "}}")?;
101                    Ok(buf)
102                })
103                .collect::<Result<StdVec<_>, FmtError>>()?
104                .join(NEW_LINE_SEPARATOR),
105        );
106
107        Ok(())
108    }
109}
110
111impl ToStub for Function {
112    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
113        self.docs.fmt_stub(buf)?;
114
115        let (_, name) = split_namespace(self.name.as_ref());
116        write!(
117            buf,
118            "function {}({})",
119            name,
120            self.params
121                .iter()
122                .map(ToStub::to_stub)
123                .collect::<Result<StdVec<_>, FmtError>>()?
124                .join(", ")
125        )?;
126
127        if let Option::Some(retval) = &self.ret {
128            write!(buf, ": ")?;
129            if retval.nullable {
130                write!(buf, "?")?;
131            }
132            retval.ty.fmt_stub(buf)?;
133        }
134
135        writeln!(buf, " {{}}")
136    }
137}
138
139impl ToStub for Parameter {
140    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
141        if let Option::Some(ty) = &self.ty {
142            if self.nullable {
143                write!(buf, "?")?;
144            }
145
146            ty.fmt_stub(buf)?;
147            write!(buf, " ")?;
148        }
149
150        write!(buf, "${}", self.name)
151    }
152}
153
154impl ToStub for DataType {
155    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
156        let mut fqdn = "\\".to_owned();
157        write!(
158            buf,
159            "{}",
160            match self {
161                DataType::True | DataType::False => "bool",
162                DataType::Long => "int",
163                DataType::Double => "float",
164                DataType::String => "string",
165                DataType::Array => "array",
166                DataType::Object(Some(ty)) => {
167                    fqdn.push_str(ty);
168                    fqdn.as_str()
169                }
170                DataType::Object(None) => "object",
171                DataType::Resource => "resource",
172                DataType::Reference => "reference",
173                DataType::Callable => "callable",
174                DataType::Bool => "bool",
175                DataType::Iterable => "iterable",
176                _ => "mixed",
177            }
178        )
179    }
180}
181
182impl ToStub for DocBlock {
183    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
184        if !self.0.is_empty() {
185            writeln!(buf, "/**")?;
186            for comment in self.0.iter() {
187                writeln!(buf, " *{comment}")?;
188            }
189            writeln!(buf, " */")?;
190        }
191        Ok(())
192    }
193}
194
195impl ToStub for Class {
196    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
197        self.docs.fmt_stub(buf)?;
198
199        let (_, name) = split_namespace(self.name.as_ref());
200        write!(buf, "class {name} ")?;
201
202        if let Option::Some(extends) = &self.extends {
203            write!(buf, "extends {extends} ")?;
204        }
205
206        if !self.implements.is_empty() {
207            write!(
208                buf,
209                "implements {} ",
210                self.implements
211                    .iter()
212                    .map(|s| s.str())
213                    .collect::<StdVec<_>>()
214                    .join(", ")
215            )?;
216        }
217
218        writeln!(buf, "{{")?;
219
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        buf.push_str(
227            &stub(&self.constants)
228                .chain(stub(&self.properties))
229                .chain(stub(&self.methods))
230                .collect::<Result<StdVec<_>, FmtError>>()?
231                .join(NEW_LINE_SEPARATOR),
232        );
233
234        writeln!(buf, "}}")
235    }
236}
237
238impl ToStub for Property {
239    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
240        self.docs.fmt_stub(buf)?;
241        self.vis.fmt_stub(buf)?;
242
243        write!(buf, " ")?;
244
245        if self.static_ {
246            write!(buf, "static ")?;
247        }
248        if let Option::Some(ty) = &self.ty {
249            ty.fmt_stub(buf)?;
250        }
251        write!(buf, "${}", self.name)?;
252        if let Option::Some(default) = &self.default {
253            write!(buf, " = {default}")?;
254        }
255        writeln!(buf, ";")
256    }
257}
258
259impl ToStub for Visibility {
260    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
261        write!(
262            buf,
263            "{}",
264            match self {
265                Visibility::Private => "private",
266                Visibility::Protected => "protected",
267                Visibility::Public => "public",
268            }
269        )
270    }
271}
272
273impl ToStub for Method {
274    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
275        self.docs.fmt_stub(buf)?;
276        self.visibility.fmt_stub(buf)?;
277
278        write!(buf, " ")?;
279
280        if matches!(self.ty, MethodType::Static) {
281            write!(buf, "static ")?;
282        }
283
284        write!(
285            buf,
286            "function {}({})",
287            self.name,
288            self.params
289                .iter()
290                .map(ToStub::to_stub)
291                .collect::<Result<StdVec<_>, FmtError>>()?
292                .join(", ")
293        )?;
294
295        if !matches!(self.ty, MethodType::Constructor) {
296            if let Option::Some(retval) = &self.retval {
297                write!(buf, ": ")?;
298                if retval.nullable {
299                    write!(buf, "?")?;
300                }
301                retval.ty.fmt_stub(buf)?;
302            }
303        }
304
305        writeln!(buf, " {{}}")
306    }
307}
308
309impl ToStub for Constant {
310    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
311        self.docs.fmt_stub(buf)?;
312
313        write!(buf, "const {} = ", self.name)?;
314        if let Option::Some(value) = &self.value {
315            write!(buf, "{value}")?;
316        } else {
317            write!(buf, "null")?;
318        }
319        writeln!(buf, ";")
320    }
321}
322
323#[cfg(windows)]
324const NEW_LINE_SEPARATOR: &str = "\r\n";
325#[cfg(not(windows))]
326const NEW_LINE_SEPARATOR: &str = "\n";
327
328/// Takes a class name and splits the namespace off from the actual class name.
329///
330/// # Returns
331///
332/// A tuple, where the first item is the namespace (or [`None`] if not
333/// namespaced), and the second item is the class name.
334fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
335    let idx = class.rfind('\\');
336
337    if let Some(idx) = idx {
338        (Some(&class[0..idx]), &class[idx + 1..])
339    } else {
340        (None, class)
341    }
342}
343
344/// Indents a given string to a given depth. Depth is given in number of spaces
345/// to be appended. Returns a new string with the new indentation. Will not
346/// indent whitespace lines.
347///
348/// # Parameters
349///
350/// * `s` - The string to indent.
351/// * `depth` - The depth to indent the lines to, in spaces.
352///
353/// # Returns
354///
355/// The indented string.
356fn indent(s: &str, depth: usize) -> String {
357    let indent = format!("{:depth$}", "", depth = depth);
358
359    s.split('\n')
360        .map(|line| {
361            let mut result = String::new();
362            if line.chars().any(|c| !c.is_whitespace()) {
363                result.push_str(&indent);
364                result.push_str(line);
365            }
366            result
367        })
368        .collect::<StdVec<_>>()
369        .join(NEW_LINE_SEPARATOR)
370}
371
372#[cfg(test)]
373mod test {
374    use super::split_namespace;
375
376    #[test]
377    pub fn test_split_ns() {
378        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
379        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
380        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
381    }
382
383    #[test]
384    #[cfg(not(windows))]
385    #[allow(clippy::uninlined_format_args)]
386    pub fn test_indent() {
387        use super::indent;
388        use crate::describe::stub::NEW_LINE_SEPARATOR;
389
390        assert_eq!(indent("hello", 4), "    hello");
391        assert_eq!(
392            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
393            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
394        );
395    }
396}