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, Retval,
13 Visibility,
14 abi::{Option, RString, Str},
15};
16
17#[cfg(feature = "enum")]
18use crate::describe::{Enum, EnumCase};
19use crate::flags::{ClassFlags, DataType};
20
21#[derive(Default)]
23struct ParsedRustDoc {
24 summary: StdVec<String>,
26 params: HashMap<String, String>,
29 param_types: HashMap<String, String>,
33 returns: StdOption<String>,
35 errors: StdVec<String>,
37}
38
39fn parse_rustdoc(docs: &[Str]) -> ParsedRustDoc {
41 let mut result = ParsedRustDoc::default();
42 let mut current_section: StdOption<&str> = None;
43 let mut section_content: StdVec<String> = StdVec::new();
44
45 for line in docs {
46 let line = line.as_ref();
47 let trimmed = line.trim();
48
49 if trimmed.starts_with("# ") {
51 finalize_section(&mut result, current_section, §ion_content);
53 section_content.clear();
54
55 let section_name = trimmed.strip_prefix("# ").unwrap_or(trimmed);
57 current_section = Some(section_name);
58 } else if current_section.is_some() {
59 section_content.push(line.to_string());
61 } else {
62 result.summary.push(line.to_string());
64 }
65 }
66
67 finalize_section(&mut result, current_section, §ion_content);
69
70 result
71}
72
73fn finalize_section(result: &mut ParsedRustDoc, section: StdOption<&str>, content: &[String]) {
75 let Some(section_name) = section else {
76 return;
77 };
78
79 match section_name {
80 "Arguments" => {
81 for line in content {
83 let trimmed = line.trim();
84 let item = trimmed
85 .strip_prefix("* ")
86 .or_else(|| trimmed.strip_prefix("- "));
87 if let Some(item) = item
90 && let Some((name, desc)) = parse_param_line(item.trim())
91 {
92 result.params.insert(name, desc);
93 }
94 }
95 }
96 "Returns" => {
97 let desc: String = content
99 .iter()
100 .map(|s| s.trim())
101 .filter(|s| !s.is_empty())
102 .collect::<StdVec<_>>()
103 .join(" ");
104 if !desc.is_empty() {
105 result.returns = Some(desc);
106 }
107 }
108 "Errors" => {
109 for line in content {
111 let trimmed = line.trim();
112 if !trimmed.is_empty() {
113 result.errors.push(trimmed.to_string());
114 }
115 }
116 }
117 "Parameters" => {
118 for line in content {
121 let trimmed = line.trim();
122 let item = trimmed
123 .strip_prefix("* ")
124 .or_else(|| trimmed.strip_prefix("- "));
125 if let Some(item) = item
126 && let Some((name, ty, desc)) = parse_typed_param_line(item.trim())
127 {
128 result.param_types.insert(name.clone(), ty);
129 if !desc.is_empty() {
130 result.params.insert(name, desc);
131 }
132 }
133 }
134 }
135 _ => {}
137 }
138}
139
140fn parse_param_line(line: &str) -> StdOption<(String, String)> {
146 if let Some(rest) = line.strip_prefix('`')
148 && let Some(end_tick) = rest.find('`')
149 {
150 let name = &rest[..end_tick];
151 let after_tick = &rest[end_tick + 1..];
153 let desc = after_tick
154 .trim()
155 .strip_prefix('-')
156 .or_else(|| after_tick.trim().strip_prefix(':'))
157 .map_or_else(|| after_tick.trim(), str::trim);
158 let name = name.strip_prefix('$').unwrap_or(name);
160 return Some((name.to_string(), desc.to_string()));
161 }
162
163 if let Some(sep_pos) = line.find(" - ") {
165 let name = line[..sep_pos].trim();
166 let desc = line[sep_pos + 3..].trim();
167 let name = name.strip_prefix('$').unwrap_or(name);
169 return Some((name.to_string(), desc.to_string()));
170 }
171
172 None
173}
174
175fn parse_typed_param_line(line: &str) -> StdOption<(String, String, String)> {
179 let rest = line.strip_prefix('`')?;
182 let end_tick = rest.find('`')?;
183 let name = rest[..end_tick].to_string();
184
185 let after_name = rest[end_tick + 1..].trim();
187 let after_colon = after_name.strip_prefix(':')?.trim();
188
189 let type_rest = after_colon.strip_prefix('`')?;
191 let type_end_tick = type_rest.find('`')?;
192 let ty = type_rest[..type_end_tick].to_string();
193
194 let desc = type_rest[type_end_tick + 1..].trim().to_string();
196
197 let name = name.strip_prefix('$').unwrap_or(&name).to_string();
199
200 Some((name, ty, desc))
201}
202
203fn format_phpdoc(
213 docs: &DocBlock,
214 params: &[Parameter],
215 ret: StdOption<&Retval>,
216 buf: &mut String,
217) -> Result<HashMap<String, String>, FmtError> {
218 if docs.0.is_empty() && params.is_empty() && ret.is_none() {
219 return Ok(HashMap::new());
220 }
221
222 let parsed = parse_rustdoc(&docs.0);
223
224 let has_summary = parsed.summary.iter().any(|s| !s.trim().is_empty());
226 let has_params = !params.is_empty();
227 let has_return = ret.is_some();
228 let has_errors = !parsed.errors.is_empty();
229
230 if !has_summary && !has_params && !has_return && !has_errors {
231 return Ok(parsed.param_types);
232 }
233
234 writeln!(buf, "/**")?;
235
236 let summary_lines: StdVec<_> = parsed
238 .summary
239 .iter()
240 .rev()
241 .skip_while(|s| s.trim().is_empty())
242 .collect::<StdVec<_>>()
243 .into_iter()
244 .rev()
245 .collect();
246
247 for line in &summary_lines {
248 writeln!(buf, " *{line}")?;
249 }
250
251 if !summary_lines.is_empty() && (has_params || has_return || has_errors) {
253 writeln!(buf, " *")?;
254 }
255
256 for param in params {
258 let type_str = if let Some(type_override) = parsed.param_types.get(param.name.as_ref()) {
260 extract_php_type(type_override)
262 } else {
263 match ¶m.ty {
264 Option::Some(ty) => datatype_to_phpdoc(ty, param.nullable),
265 Option::None => "mixed".to_string(),
266 }
267 };
268
269 let desc = parsed.params.get(param.name.as_ref()).cloned();
270 if let Some(desc) = desc {
271 writeln!(buf, " * @param {type_str} ${} {desc}", param.name)?;
272 } else {
273 writeln!(buf, " * @param {type_str} ${}", param.name)?;
274 }
275 }
276
277 if let Some(retval) = ret {
279 let type_str = datatype_to_phpdoc(&retval.ty, retval.nullable);
280 if let Some(desc) = &parsed.returns {
281 writeln!(buf, " * @return {type_str} {desc}")?;
282 } else {
283 writeln!(buf, " * @return {type_str}")?;
284 }
285 }
286
287 for error in &parsed.errors {
289 writeln!(buf, " * @throws \\Exception {error}")?;
290 }
291
292 writeln!(buf, " */")?;
293 Ok(parsed.param_types)
294}
295
296fn extract_php_type(type_str: &str) -> String {
299 type_str
302 .split_whitespace()
303 .next()
304 .unwrap_or("mixed")
305 .to_string()
306}
307
308fn datatype_to_phpdoc(ty: &DataType, nullable: bool) -> String {
310 let base = match ty {
311 DataType::Bool | DataType::True | DataType::False => "bool",
312 DataType::Long => "int",
313 DataType::Double => "float",
314 DataType::String => "string",
315 DataType::Array => "array",
316 DataType::Object(Some(name)) => return format_class_type(name, nullable),
317 DataType::Object(None) => "object",
318 DataType::Resource => "resource",
319 DataType::Callable => "callable",
320 DataType::Void => "void",
321 DataType::Null => "null",
322 DataType::Iterable => "iterable",
323 _ => "mixed",
325 };
326
327 if nullable && !matches!(ty, DataType::Mixed | DataType::Null | DataType::Void) {
328 format!("{base}|null")
329 } else {
330 base.to_string()
331 }
332}
333
334fn format_class_type(name: &str, nullable: bool) -> String {
336 let class_name = if name.starts_with('\\') {
337 name.to_string()
338 } else {
339 format!("\\{name}")
340 };
341
342 if nullable {
343 format!("{class_name}|null")
344 } else {
345 class_name
346 }
347}
348
349pub trait ToStub {
351 fn to_stub(&self) -> Result<String, FmtError> {
362 let mut buf = String::new();
363 self.fmt_stub(&mut buf)?;
364 Ok(buf)
365 }
366
367 fn fmt_stub(&self, buf: &mut String) -> FmtResult;
381}
382
383impl ToStub for Module {
384 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
385 writeln!(buf, "<?php")?;
386 writeln!(buf)?;
387 writeln!(buf, "// Stubs for {}", self.name.as_ref())?;
388 writeln!(buf)?;
389
390 let mut entries: HashMap<StdOption<&str>, StdVec<(String, String)>> = HashMap::new();
394
395 let mut insert = |ns, sort_key: String, entry| {
398 let bucket = entries.entry(ns).or_default();
399 bucket.push((sort_key, entry));
400 };
401
402 for c in &*self.constants {
403 let (ns, name) = split_namespace(c.name.as_ref());
404 insert(ns, name.to_string(), c.to_stub()?);
405 }
406
407 for func in &*self.functions {
408 let (ns, name) = split_namespace(func.name.as_ref());
409 insert(ns, name.to_string(), func.to_stub()?);
410 }
411
412 for class in &*self.classes {
413 let (ns, name) = split_namespace(class.name.as_ref());
414 insert(ns, name.to_string(), class.to_stub()?);
415 }
416
417 #[cfg(feature = "enum")]
418 for r#enum in &*self.enums {
419 let (ns, name) = split_namespace(r#enum.name.as_ref());
420 insert(ns, name.to_string(), r#enum.to_stub()?);
421 }
422
423 for bucket in entries.values_mut() {
425 bucket.sort_by(|(a, _), (b, _)| a.cmp(b));
426 }
427
428 let mut entries: StdVec<_> = entries.iter().collect();
429 entries.sort_by(|(l, _), (r, _)| match (l, r) {
430 (None, _) => Ordering::Greater,
431 (_, None) => Ordering::Less,
432 (Some(l), Some(r)) => l.cmp(r),
433 });
434
435 buf.push_str(
436 &entries
437 .into_iter()
438 .map(|(ns, entries)| {
439 let mut buf = String::new();
440 if let Some(ns) = ns {
441 writeln!(buf, "namespace {ns} {{")?;
442 } else {
443 writeln!(buf, "namespace {{")?;
444 }
445
446 buf.push_str(
447 &entries
448 .iter()
449 .map(|(_, stub)| indent(stub, 4))
450 .collect::<StdVec<_>>()
451 .join(NEW_LINE_SEPARATOR),
452 );
453
454 writeln!(buf, "}}")?;
455 Ok(buf)
456 })
457 .collect::<Result<StdVec<_>, FmtError>>()?
458 .join(NEW_LINE_SEPARATOR),
459 );
460
461 Ok(())
462 }
463}
464
465impl ToStub for Function {
466 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
467 let ret_ref = match &self.ret {
469 Option::Some(r) => Some(r),
470 Option::None => None,
471 };
472 let type_overrides = format_phpdoc(&self.docs, &self.params, ret_ref, buf)?;
473
474 let (_, name) = split_namespace(self.name.as_ref());
475
476 let params_str = self
478 .params
479 .iter()
480 .map(|p| param_to_stub(p, &type_overrides))
481 .collect::<Result<StdVec<_>, FmtError>>()?
482 .join(", ");
483
484 write!(buf, "function {name}({params_str})")?;
485
486 if let Option::Some(retval) = &self.ret {
487 write!(buf, ": ")?;
488 if retval.nullable
490 && !matches!(retval.ty, DataType::Mixed | DataType::Null | DataType::Void)
491 {
492 write!(buf, "?")?;
493 }
494 retval.ty.fmt_stub(buf)?;
495 }
496
497 writeln!(buf, " {{}}")
498 }
499}
500
501fn param_to_stub(
506 param: &Parameter,
507 type_overrides: &HashMap<String, String>,
508) -> Result<String, FmtError> {
509 let mut buf = String::new();
510
511 let type_override = type_overrides
514 .get(param.name.as_ref())
515 .filter(|_| matches!(¶m.ty, Option::Some(DataType::Mixed) | Option::None));
516
517 if let Some(override_str) = type_override {
518 let type_str = extract_php_type(override_str);
520 write!(buf, "{type_str} ")?;
521 } else if let Option::Some(ty) = ¶m.ty {
522 if param.nullable && !matches!(ty, DataType::Mixed | DataType::Null | DataType::Void) {
524 write!(buf, "?")?;
525 }
526 ty.fmt_stub(&mut buf)?;
527 write!(buf, " ")?;
528 }
529
530 if param.variadic {
531 write!(buf, "...")?;
532 }
533
534 write!(buf, "${}", param.name)?;
535
536 if let Option::Some(default) = ¶m.default {
538 write!(buf, " = {default}")?;
539 } else if param.nullable {
540 write!(buf, " = null")?;
543 }
544
545 Ok(buf)
546}
547
548impl ToStub for Parameter {
549 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
550 let empty_overrides = HashMap::new();
551 let result = param_to_stub(self, &empty_overrides)?;
552 buf.push_str(&result);
553 Ok(())
554 }
555}
556
557impl ToStub for DataType {
558 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
559 let mut fqdn = "\\".to_owned();
560 write!(
561 buf,
562 "{}",
563 match self {
564 DataType::Bool | DataType::True | DataType::False => "bool",
565 DataType::Long => "int",
566 DataType::Double => "float",
567 DataType::String => "string",
568 DataType::Array => "array",
569 DataType::Object(Some(ty)) => {
570 fqdn.push_str(ty);
571 fqdn.as_str()
572 }
573 DataType::Object(None) => "object",
574 DataType::Resource => "resource",
575 DataType::Reference => "reference",
576 DataType::Callable => "callable",
577 DataType::Iterable => "iterable",
578 DataType::Void => "void",
579 DataType::Null => "null",
580 DataType::Mixed
581 | DataType::Undef
582 | DataType::Ptr
583 | DataType::Indirect
584 | DataType::ConstantExpression => "mixed",
585 }
586 )
587 }
588}
589
590impl ToStub for DocBlock {
591 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
592 if !self.0.is_empty() {
593 writeln!(buf, "/**")?;
594 for comment in self.0.iter() {
595 writeln!(buf, " *{comment}")?;
596 }
597 writeln!(buf, " */")?;
598 }
599 Ok(())
600 }
601}
602
603impl ToStub for Class {
604 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
605 self.docs.fmt_stub(buf)?;
606
607 let (_, name) = split_namespace(self.name.as_ref());
608 let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty());
609 let is_interface = flags.contains(ClassFlags::Interface);
610
611 if is_interface {
612 write!(buf, "interface {name} ")?;
613 } else {
614 write!(buf, "class {name} ")?;
615 }
616
617 if let Option::Some(extends) = &self.extends {
618 write!(buf, "extends {extends} ")?;
619 }
620
621 if !self.implements.is_empty() && !is_interface {
622 write!(
623 buf,
624 "implements {} ",
625 self.implements
626 .iter()
627 .map(RString::as_str)
628 .collect::<StdVec<_>>()
629 .join(", ")
630 )?;
631 }
632
633 if !self.implements.is_empty() && is_interface {
634 write!(
635 buf,
636 "extends {} ",
637 self.implements
638 .iter()
639 .map(RString::as_str)
640 .collect::<StdVec<_>>()
641 .join(", ")
642 )?;
643 }
644
645 writeln!(buf, "{{")?;
646
647 let mut constants: StdVec<_> = self
649 .constants
650 .iter()
651 .map(|c| {
652 c.to_stub()
653 .map(|s| (c.name.as_ref().to_string(), indent(&s, 4)))
654 })
655 .collect::<Result<_, FmtError>>()?;
656 let mut properties: StdVec<_> = self
657 .properties
658 .iter()
659 .map(|p| {
660 p.to_stub()
661 .map(|s| (p.name.as_ref().to_string(), indent(&s, 4)))
662 })
663 .collect::<Result<_, FmtError>>()?;
664 let mut methods: StdVec<_> = self
665 .methods
666 .iter()
667 .map(|m| {
668 m.to_stub()
669 .map(|s| (m.name.as_ref().to_string(), indent(&s, 4)))
670 })
671 .collect::<Result<_, FmtError>>()?;
672
673 constants.sort_by(|(a, _), (b, _)| a.cmp(b));
675 properties.sort_by(|(a, _), (b, _)| a.cmp(b));
676 methods.sort_by(|(a, _), (b, _)| a.cmp(b));
677
678 buf.push_str(
679 &constants
680 .into_iter()
681 .chain(properties)
682 .chain(methods)
683 .map(|(_, stub)| stub)
684 .collect::<StdVec<_>>()
685 .join(NEW_LINE_SEPARATOR),
686 );
687
688 writeln!(buf, "}}")
689 }
690}
691
692#[cfg(feature = "enum")]
693impl ToStub for Enum {
694 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
695 self.docs.fmt_stub(buf)?;
696
697 let (_, name) = split_namespace(self.name.as_ref());
698 write!(buf, "enum {name}")?;
699
700 if let Option::Some(backing_type) = &self.backing_type {
701 write!(buf, ": {backing_type}")?;
702 }
703
704 writeln!(buf, " {{")?;
705
706 for case in self.cases.iter() {
707 case.fmt_stub(buf)?;
708 }
709
710 writeln!(buf, "}}")
711 }
712}
713
714#[cfg(feature = "enum")]
715impl ToStub for EnumCase {
716 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
717 self.docs.fmt_stub(buf)?;
718
719 write!(buf, " case {}", self.name)?;
720 if let Option::Some(value) = &self.value {
721 write!(buf, " = {value}")?;
722 }
723 writeln!(buf, ";")
724 }
725}
726
727impl ToStub for Property {
728 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
729 self.docs.fmt_stub(buf)?;
730 self.vis.fmt_stub(buf)?;
731
732 write!(buf, " ")?;
733
734 if self.static_ {
735 write!(buf, "static ")?;
736 }
737 if let Option::Some(ty) = &self.ty {
738 ty.fmt_stub(buf)?;
739 }
740 write!(buf, "${}", self.name)?;
741 if let Option::Some(default) = &self.default {
742 write!(buf, " = {default}")?;
743 }
744 writeln!(buf, ";")
745 }
746}
747
748impl ToStub for Visibility {
749 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
750 write!(
751 buf,
752 "{}",
753 match self {
754 Visibility::Private => "private",
755 Visibility::Protected => "protected",
756 Visibility::Public => "public",
757 }
758 )
759 }
760}
761
762impl ToStub for Method {
763 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
764 let ret_ref = if matches!(self.ty, MethodType::Constructor) {
767 None
768 } else {
769 match &self.retval {
770 Option::Some(r) => Some(r),
771 Option::None => None,
772 }
773 };
774 let type_overrides = format_phpdoc(&self.docs, &self.params, ret_ref, buf)?;
775
776 self.visibility.fmt_stub(buf)?;
777
778 write!(buf, " ")?;
779
780 if matches!(self.ty, MethodType::Static) {
781 write!(buf, "static ")?;
782 }
783
784 let params_str = self
786 .params
787 .iter()
788 .map(|p| param_to_stub(p, &type_overrides))
789 .collect::<Result<StdVec<_>, FmtError>>()?
790 .join(", ");
791
792 write!(buf, "function {}({params_str})", self.name)?;
793
794 if !matches!(self.ty, MethodType::Constructor)
795 && let Option::Some(retval) = &self.retval
796 {
797 write!(buf, ": ")?;
798 if retval.nullable
800 && !matches!(retval.ty, DataType::Mixed | DataType::Null | DataType::Void)
801 {
802 write!(buf, "?")?;
803 }
804 retval.ty.fmt_stub(buf)?;
805 }
806
807 if self.r#abstract {
808 writeln!(buf, ";")
809 } else {
810 writeln!(buf, " {{}}")
811 }
812 }
813}
814
815impl ToStub for Constant {
816 fn fmt_stub(&self, buf: &mut String) -> FmtResult {
817 self.docs.fmt_stub(buf)?;
818
819 write!(buf, "const {} = ", self.name)?;
820 if let Option::Some(value) = &self.value {
821 write!(buf, "{value}")?;
822 } else {
823 write!(buf, "null")?;
824 }
825 writeln!(buf, ";")
826 }
827}
828
829#[cfg(windows)]
830const NEW_LINE_SEPARATOR: &str = "\r\n";
831#[cfg(not(windows))]
832const NEW_LINE_SEPARATOR: &str = "\n";
833
834fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
841 let idx = class.rfind('\\');
842
843 if let Some(idx) = idx {
844 (Some(&class[0..idx]), &class[idx + 1..])
845 } else {
846 (None, class)
847 }
848}
849
850fn indent(s: &str, depth: usize) -> String {
863 let indent = format!("{:depth$}", "", depth = depth);
864
865 s.split('\n')
866 .map(|line| {
867 let mut result = String::new();
868 if line.chars().any(|c| !c.is_whitespace()) {
869 result.push_str(&indent);
870 result.push_str(line);
871 }
872 result
873 })
874 .collect::<StdVec<_>>()
875 .join(NEW_LINE_SEPARATOR)
876}
877
878#[cfg(test)]
879mod test {
880 use super::{ToStub, split_namespace};
881 use crate::flags::DataType;
882
883 #[test]
884 pub fn test_split_ns() {
885 assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
886 assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
887 assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
888 }
889
890 #[test]
891 #[cfg(not(windows))]
892 #[allow(clippy::uninlined_format_args)]
893 pub fn test_indent() {
894 use super::indent;
895 use crate::describe::stub::NEW_LINE_SEPARATOR;
896
897 assert_eq!(indent("hello", 4), " hello");
898 assert_eq!(
899 indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
900 format!(" hello{nl} world{nl}", nl = NEW_LINE_SEPARATOR)
901 );
902 }
903
904 #[test]
905 #[allow(clippy::unwrap_used)]
906 pub fn test_datatype_to_stub() {
907 assert_eq!(DataType::Void.to_stub().unwrap(), "void");
909 assert_eq!(DataType::Null.to_stub().unwrap(), "null");
910 assert_eq!(DataType::Bool.to_stub().unwrap(), "bool");
911 assert_eq!(DataType::True.to_stub().unwrap(), "bool");
912 assert_eq!(DataType::False.to_stub().unwrap(), "bool");
913 assert_eq!(DataType::Long.to_stub().unwrap(), "int");
914 assert_eq!(DataType::Double.to_stub().unwrap(), "float");
915 assert_eq!(DataType::String.to_stub().unwrap(), "string");
916 assert_eq!(DataType::Array.to_stub().unwrap(), "array");
917 assert_eq!(DataType::Object(None).to_stub().unwrap(), "object");
918 assert_eq!(
919 DataType::Object(Some("Foo\\Bar")).to_stub().unwrap(),
920 "\\Foo\\Bar"
921 );
922 assert_eq!(DataType::Resource.to_stub().unwrap(), "resource");
923 assert_eq!(DataType::Callable.to_stub().unwrap(), "callable");
924 assert_eq!(DataType::Iterable.to_stub().unwrap(), "iterable");
925 assert_eq!(DataType::Mixed.to_stub().unwrap(), "mixed");
926 assert_eq!(DataType::Undef.to_stub().unwrap(), "mixed");
927 assert_eq!(DataType::Ptr.to_stub().unwrap(), "mixed");
928 assert_eq!(DataType::Indirect.to_stub().unwrap(), "mixed");
929 assert_eq!(DataType::ConstantExpression.to_stub().unwrap(), "mixed");
930 assert_eq!(DataType::Reference.to_stub().unwrap(), "reference");
931 }
932
933 #[test]
934 fn test_parse_rustdoc() {
935 use super::{Str, parse_rustdoc};
936
937 let docs: Vec<Str> = vec![
939 " Gives you a nice greeting!".into(),
940 "".into(),
941 " # Arguments".into(),
942 "".into(),
943 " * `name` - Your name".into(),
944 " * `age` - Your age".into(),
945 "".into(),
946 " # Returns".into(),
947 "".into(),
948 " Nice greeting!".into(),
949 ];
950
951 let parsed = parse_rustdoc(&docs);
952
953 assert_eq!(parsed.summary.len(), 2);
955 assert!(parsed.summary[0].contains("Gives you a nice greeting"));
956
957 assert_eq!(parsed.params.len(), 2);
959 assert_eq!(parsed.params.get("name"), Some(&"Your name".to_string()));
960 assert_eq!(parsed.params.get("age"), Some(&"Your age".to_string()));
961
962 assert!(parsed.returns.is_some());
964 assert!(
965 parsed
966 .returns
967 .as_ref()
968 .is_some_and(|r| r.contains("Nice greeting"))
969 );
970 }
971
972 #[test]
973 fn test_parse_param_line() {
974 use super::parse_param_line;
975
976 assert_eq!(
978 parse_param_line("`name` - Your name"),
979 Some(("name".to_string(), "Your name".to_string()))
980 );
981
982 assert_eq!(
984 parse_param_line("`$name` - Your name"),
985 Some(("name".to_string(), "Your name".to_string()))
986 );
987
988 assert_eq!(
990 parse_param_line("name - Your name"),
991 Some(("name".to_string(), "Your name".to_string()))
992 );
993
994 assert_eq!(parse_param_line("no separator here"), None);
996 }
997
998 #[test]
999 fn test_format_phpdoc() {
1000 use super::{DocBlock, Parameter, Retval, Str, format_phpdoc};
1001 use crate::describe::abi::Option;
1002 use crate::flags::DataType;
1003
1004 let docs = DocBlock(
1006 vec![
1007 Str::from(" Greets the user."),
1008 Str::from(""),
1009 Str::from(" # Arguments"),
1010 Str::from(""),
1011 Str::from(" * `name` - The name to greet"),
1012 Str::from(""),
1013 Str::from(" # Returns"),
1014 Str::from(""),
1015 Str::from(" A greeting string."),
1016 ]
1017 .into(),
1018 );
1019
1020 let params = vec![Parameter {
1021 name: "name".into(),
1022 ty: Option::Some(DataType::String),
1023 nullable: false,
1024 variadic: false,
1025 default: Option::None,
1026 }];
1027
1028 let retval = Retval {
1029 ty: DataType::String,
1030 nullable: false,
1031 };
1032
1033 let mut buf = String::new();
1034 format_phpdoc(&docs, ¶ms, Some(&retval), &mut buf).expect("format_phpdoc failed");
1035
1036 assert!(buf.contains("/**"));
1038 assert!(buf.contains("*/"));
1039 assert!(buf.contains("@param string $name The name to greet"));
1040 assert!(buf.contains("@return string A greeting string."));
1041 assert!(!buf.contains("# Arguments"));
1043 assert!(!buf.contains("# Returns"));
1044 }
1045}