1use 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
14pub trait ToStub {
16 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 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 let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
58
59 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
334fn 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
350fn 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}