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}
177
178impl ToStub for DataType {
179 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
180 let mut fqdn = "\\".to_owned();
181 write!(
182 buf,
183 "{}",
184 match self {
185 DataType::Bool | DataType::True | DataType::False => "bool",
186 DataType::Long => "int",
187 DataType::Double => "float",
188 DataType::String => "string",
189 DataType::Array => "array",
190 DataType::Object(Some(ty)) => {
191 fqdn.push_str(ty);
192 fqdn.as_str()
193 }
194 DataType::Object(None) => "object",
195 DataType::Resource => "resource",
196 DataType::Reference => "reference",
197 DataType::Callable => "callable",
198 DataType::Iterable => "iterable",
199 _ => "mixed",
200 }
201 )
202 }
203}
204
205impl ToStub for DocBlock {
206 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
207 if !self.0.is_empty() {
208 writeln!(buf, "/**")?;
209 for comment in self.0.iter() {
210 writeln!(buf, " *{comment}")?;
211 }
212 writeln!(buf, " */")?;
213 }
214 Ok(())
215 }
216}
217
218impl ToStub for Class {
219 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
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 self.docs.fmt_stub(buf)?;
227
228 let (_, name) = split_namespace(self.name.as_ref());
229 let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty());
230 let is_interface = flags.contains(ClassFlags::Interface);
231
232 if is_interface {
233 write!(buf, "interface {name} ")?;
234 } else {
235 write!(buf, "class {name} ")?;
236 }
237
238 if let Option::Some(extends) = &self.extends {
239 write!(buf, "extends {extends} ")?;
240 }
241
242 if !self.implements.is_empty() && !is_interface {
243 write!(
244 buf,
245 "implements {} ",
246 self.implements
247 .iter()
248 .map(RString::as_str)
249 .collect::<StdVec<_>>()
250 .join(", ")
251 )?;
252 }
253
254 if !self.implements.is_empty() && is_interface {
255 write!(
256 buf,
257 "extends {} ",
258 self.implements
259 .iter()
260 .map(RString::as_str)
261 .collect::<StdVec<_>>()
262 .join(", ")
263 )?;
264 }
265
266 writeln!(buf, "{{")?;
267
268 buf.push_str(
269 &stub(&self.constants)
270 .chain(stub(&self.properties))
271 .chain(stub(&self.methods))
272 .collect::<Result<StdVec<_>, FmtError>>()?
273 .join(NEW_LINE_SEPARATOR),
274 );
275
276 writeln!(buf, "}}")
277 }
278}
279
280#[cfg(feature = "enum")]
281impl ToStub for Enum {
282 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
283 self.docs.fmt_stub(buf)?;
284
285 let (_, name) = split_namespace(self.name.as_ref());
286 write!(buf, "enum {name}")?;
287
288 if let Option::Some(backing_type) = &self.backing_type {
289 write!(buf, ": {backing_type}")?;
290 }
291
292 writeln!(buf, " {{")?;
293
294 for case in self.cases.iter() {
295 case.fmt_stub(buf)?;
296 }
297
298 writeln!(buf, "}}")
299 }
300}
301
302#[cfg(feature = "enum")]
303impl ToStub for EnumCase {
304 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
305 self.docs.fmt_stub(buf)?;
306
307 write!(buf, " case {}", self.name)?;
308 if let Option::Some(value) = &self.value {
309 write!(buf, " = {value}")?;
310 }
311 writeln!(buf, ";")
312 }
313}
314
315impl ToStub for Property {
316 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
317 self.docs.fmt_stub(buf)?;
318 self.vis.fmt_stub(buf)?;
319
320 write!(buf, " ")?;
321
322 if self.static_ {
323 write!(buf, "static ")?;
324 }
325 if let Option::Some(ty) = &self.ty {
326 ty.fmt_stub(buf)?;
327 }
328 write!(buf, "${}", self.name)?;
329 if let Option::Some(default) = &self.default {
330 write!(buf, " = {default}")?;
331 }
332 writeln!(buf, ";")
333 }
334}
335
336impl ToStub for Visibility {
337 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
338 write!(
339 buf,
340 "{}",
341 match self {
342 Visibility::Private => "private",
343 Visibility::Protected => "protected",
344 Visibility::Public => "public",
345 }
346 )
347 }
348}
349
350impl ToStub for Method {
351 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
352 self.docs.fmt_stub(buf)?;
353 self.visibility.fmt_stub(buf)?;
354
355 write!(buf, " ")?;
356
357 if matches!(self.ty, MethodType::Static) {
358 write!(buf, "static ")?;
359 }
360
361 write!(
362 buf,
363 "function {}({})",
364 self.name,
365 self.params
366 .iter()
367 .map(ToStub::to_stub)
368 .collect::<Result<StdVec<_>, FmtError>>()?
369 .join(", ")
370 )?;
371
372 if !matches!(self.ty, MethodType::Constructor)
373 && let Option::Some(retval) = &self.retval
374 {
375 write!(buf, ": ")?;
376 if retval.nullable {
377 write!(buf, "?")?;
378 }
379 retval.ty.fmt_stub(buf)?;
380 }
381
382 if self.r#abstract {
383 writeln!(buf, ";")
384 } else {
385 writeln!(buf, " {{}}")
386 }
387 }
388}
389
390impl ToStub for Constant {
391 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
392 self.docs.fmt_stub(buf)?;
393
394 write!(buf, "const {} = ", self.name)?;
395 if let Option::Some(value) = &self.value {
396 write!(buf, "{value}")?;
397 } else {
398 write!(buf, "null")?;
399 }
400 writeln!(buf, ";")
401 }
402}
403
404#[cfg(windows)]
405const NEW_LINE_SEPARATOR: &str = "\r\n";
406#[cfg(not(windows))]
407const NEW_LINE_SEPARATOR: &str = "\n";
408
409fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
416 let idx = class.rfind('\\');
417
418 if let Some(idx) = idx {
419 (Some(&class[0..idx]), &class[idx + 1..])
420 } else {
421 (None, class)
422 }
423}
424
425fn indent(s: &str, depth: usize) -> String {
438 let indent = format!("{:depth$}", "", depth = depth);
439
440 s.split('\n')
441 .map(|line| {
442 let mut result = String::new();
443 if line.chars().any(|c| !c.is_whitespace()) {
444 result.push_str(&indent);
445 result.push_str(line);
446 }
447 result
448 })
449 .collect::<StdVec<_>>()
450 .join(NEW_LINE_SEPARATOR)
451}
452
453#[cfg(test)]
454mod test {
455 use super::split_namespace;
456
457 #[test]
458 pub fn test_split_ns() {
459 assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
460 assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
461 assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
462 }
463
464 #[test]
465 #[cfg(not(windows))]
466 #[allow(clippy::uninlined_format_args)]
467 pub fn test_indent() {
468 use super::indent;
469 use crate::describe::stub::NEW_LINE_SEPARATOR;
470
471 assert_eq!(indent("hello", 4), " hello");
472 assert_eq!(
473 indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
474 format!(" hello{nl} world{nl}", nl = NEW_LINE_SEPARATOR)
475 );
476 }
477}