1use 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
21pub trait ToStub {
23 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 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 let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
65
66 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 if let Option::Some(default) = &self.default {
178 write!(buf, " = {default}")?;
179 } else if self.nullable {
180 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
426fn 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
442fn 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 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}