1use 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
13pub trait ToStub {
15 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 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 let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
51
52 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
328fn 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
344fn 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}