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::{Option, RString},
8    Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
9    Visibility,
10};
11use std::fmt::{Error as FmtError, Result as FmtResult, Write};
12use std::{option::Option as StdOption, vec::Vec as StdVec};
13
14/// Implemented on types which can be converted into PHP stubs.
15pub 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
48impl 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
118impl 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
146impl 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
157        write!(buf, "${}", self.name)
158    }
159}
160
161impl ToStub for DataType {
162    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
163        let mut fqdn = "\\".to_owned();
164        write!(
165            buf,
166            "{}",
167            match self {
168                DataType::Bool | DataType::True | DataType::False => "bool",
169                DataType::Long => "int",
170                DataType::Double => "float",
171                DataType::String => "string",
172                DataType::Array => "array",
173                DataType::Object(Some(ty)) => {
174                    fqdn.push_str(ty);
175                    fqdn.as_str()
176                }
177                DataType::Object(None) => "object",
178                DataType::Resource => "resource",
179                DataType::Reference => "reference",
180                DataType::Callable => "callable",
181                DataType::Iterable => "iterable",
182                _ => "mixed",
183            }
184        )
185    }
186}
187
188impl ToStub for DocBlock {
189    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
190        if !self.0.is_empty() {
191            writeln!(buf, "/**")?;
192            for comment in self.0.iter() {
193                writeln!(buf, " *{comment}")?;
194            }
195            writeln!(buf, " */")?;
196        }
197        Ok(())
198    }
199}
200
201impl ToStub for Class {
202    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
203        fn stub<T: ToStub>(items: &[T]) -> impl Iterator<Item = Result<String, FmtError>> + '_ {
204            items
205                .iter()
206                .map(|item| item.to_stub().map(|stub| indent(&stub, 4)))
207        }
208
209        self.docs.fmt_stub(buf)?;
210
211        let (_, name) = split_namespace(self.name.as_ref());
212        write!(buf, "class {name} ")?;
213
214        if let Option::Some(extends) = &self.extends {
215            write!(buf, "extends {extends} ")?;
216        }
217
218        if !self.implements.is_empty() {
219            write!(
220                buf,
221                "implements {} ",
222                self.implements
223                    .iter()
224                    .map(RString::as_str)
225                    .collect::<StdVec<_>>()
226                    .join(", ")
227            )?;
228        }
229
230        writeln!(buf, "{{")?;
231
232        buf.push_str(
233            &stub(&self.constants)
234                .chain(stub(&self.properties))
235                .chain(stub(&self.methods))
236                .collect::<Result<StdVec<_>, FmtError>>()?
237                .join(NEW_LINE_SEPARATOR),
238        );
239
240        writeln!(buf, "}}")
241    }
242}
243
244impl ToStub for Property {
245    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
246        self.docs.fmt_stub(buf)?;
247        self.vis.fmt_stub(buf)?;
248
249        write!(buf, " ")?;
250
251        if self.static_ {
252            write!(buf, "static ")?;
253        }
254        if let Option::Some(ty) = &self.ty {
255            ty.fmt_stub(buf)?;
256        }
257        write!(buf, "${}", self.name)?;
258        if let Option::Some(default) = &self.default {
259            write!(buf, " = {default}")?;
260        }
261        writeln!(buf, ";")
262    }
263}
264
265impl ToStub for Visibility {
266    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
267        write!(
268            buf,
269            "{}",
270            match self {
271                Visibility::Private => "private",
272                Visibility::Protected => "protected",
273                Visibility::Public => "public",
274            }
275        )
276    }
277}
278
279impl ToStub for Method {
280    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
281        self.docs.fmt_stub(buf)?;
282        self.visibility.fmt_stub(buf)?;
283
284        write!(buf, " ")?;
285
286        if matches!(self.ty, MethodType::Static) {
287            write!(buf, "static ")?;
288        }
289
290        write!(
291            buf,
292            "function {}({})",
293            self.name,
294            self.params
295                .iter()
296                .map(ToStub::to_stub)
297                .collect::<Result<StdVec<_>, FmtError>>()?
298                .join(", ")
299        )?;
300
301        if !matches!(self.ty, MethodType::Constructor) {
302            if let Option::Some(retval) = &self.retval {
303                write!(buf, ": ")?;
304                if retval.nullable {
305                    write!(buf, "?")?;
306                }
307                retval.ty.fmt_stub(buf)?;
308            }
309        }
310
311        writeln!(buf, " {{}}")
312    }
313}
314
315impl ToStub for Constant {
316    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
317        self.docs.fmt_stub(buf)?;
318
319        write!(buf, "const {} = ", self.name)?;
320        if let Option::Some(value) = &self.value {
321            write!(buf, "{value}")?;
322        } else {
323            write!(buf, "null")?;
324        }
325        writeln!(buf, ";")
326    }
327}
328
329#[cfg(windows)]
330const NEW_LINE_SEPARATOR: &str = "\r\n";
331#[cfg(not(windows))]
332const NEW_LINE_SEPARATOR: &str = "\n";
333
334/// Takes a class name and splits the namespace off from the actual class name.
335///
336/// # Returns
337///
338/// A tuple, where the first item is the namespace (or [`None`] if not
339/// namespaced), and the second item is the class name.
340fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
341    let idx = class.rfind('\\');
342
343    if let Some(idx) = idx {
344        (Some(&class[0..idx]), &class[idx + 1..])
345    } else {
346        (None, class)
347    }
348}
349
350/// Indents a given string to a given depth. Depth is given in number of spaces
351/// to be appended. Returns a new string with the new indentation. Will not
352/// indent whitespace lines.
353///
354/// # Parameters
355///
356/// * `s` - The string to indent.
357/// * `depth` - The depth to indent the lines to, in spaces.
358///
359/// # Returns
360///
361/// The indented string.
362fn indent(s: &str, depth: usize) -> String {
363    let indent = format!("{:depth$}", "", depth = depth);
364
365    s.split('\n')
366        .map(|line| {
367            let mut result = String::new();
368            if line.chars().any(|c| !c.is_whitespace()) {
369                result.push_str(&indent);
370                result.push_str(line);
371            }
372            result
373        })
374        .collect::<StdVec<_>>()
375        .join(NEW_LINE_SEPARATOR)
376}
377
378#[cfg(test)]
379mod test {
380    use super::split_namespace;
381
382    #[test]
383    pub fn test_split_ns() {
384        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
385        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
386        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
387    }
388
389    #[test]
390    #[cfg(not(windows))]
391    #[allow(clippy::uninlined_format_args)]
392    pub fn test_indent() {
393        use super::indent;
394        use crate::describe::stub::NEW_LINE_SEPARATOR;
395
396        assert_eq!(indent("hello", 4), "    hello");
397        assert_eq!(
398            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
399            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
400        );
401    }
402}