1use bumpalo::Bump;
2
3use mago_span::Span;
4use mago_syntax::ast::Trivia;
5use mago_syntax::ast::TriviaKind;
6
7use crate::document::Document;
8use crate::error::ParseError;
9
10mod internal;
11
12pub mod document;
13pub mod error;
14pub mod tag;
15
16#[inline]
22pub fn parse_trivia<'arena>(arena: &'arena Bump, trivia: &Trivia<'arena>) -> Result<Document<'arena>, ParseError> {
23 if TriviaKind::DocBlockComment != trivia.kind {
24 return Err(ParseError::InvalidTrivia(trivia.span));
25 }
26
27 parse_phpdoc_with_span(arena, trivia.value, trivia.span)
28}
29
30#[inline]
36pub fn parse_phpdoc_with_span<'arena>(
37 arena: &'arena Bump,
38 content: &'arena str,
39 span: Span,
40) -> Result<Document<'arena>, ParseError> {
41 let tokens = internal::lexer::tokenize(content, span)?;
42
43 internal::parser::parse_document(span, tokens.as_slice(), arena)
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 use mago_database::file::FileId;
51 use mago_span::Position;
52 use mago_span::Span;
53
54 use crate::document::*;
55
56 #[test]
57 fn test_parse_all_elements() {
58 let arena = Bump::new();
59 let phpdoc = r#"/**
60 * This is a simple description.
61 *
62 * This text contains an inline code `echo "Hello, World!";`.
63 *
64 * This text contains an inline tag {@see \Some\Class}.
65 *
66 * ```php
67 * echo "Hello, World!";
68 * ```
69 *
70 * $foo = "bar";
71 * echo "Hello, World!";
72 *
73 * @param string $foo
74 * @param array{
75 * bar: string,
76 * baz: int
77 * } $bar
78 * @return void
79 */"#;
80
81 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
82 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
83 assert_eq!(document.elements.len(), 12);
84
85 let Element::Text(text) = &document.elements[0] else {
86 panic!("Expected Element::Text, got {:?}", document.elements[0]);
87 };
88
89 assert_eq!(text.segments.len(), 1);
90
91 let TextSegment::Paragraph { span, content } = text.segments[0] else {
92 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
93 };
94
95 assert_eq!(content, "This is a simple description.");
96 assert_eq!(&phpdoc[span.start.offset as usize..span.end.offset as usize], "This is a simple description.");
97
98 let Element::Line(_) = &document.elements[1] else {
99 panic!("Expected Element::Line, got {:?}", document.elements[1]);
100 };
101
102 let Element::Text(text) = &document.elements[2] else {
103 panic!("Expected Element::Text, got {:?}", document.elements[2]);
104 };
105
106 assert_eq!(text.segments.len(), 3);
107
108 let TextSegment::Paragraph { content, .. } = text.segments[0] else {
109 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
110 };
111
112 assert_eq!(content, "This text contains an inline code ");
113
114 let TextSegment::InlineCode(code) = &text.segments[1] else {
115 panic!("Expected TextSegment::InlineCode, got {:?}", text.segments[1]);
116 };
117
118 let content = code.content;
119 assert_eq!(content, "echo \"Hello, World!\";");
120 assert_eq!(
121 &phpdoc[code.span.start.offset as usize..code.span.end.offset as usize],
122 "`echo \"Hello, World!\";`"
123 );
124
125 let TextSegment::Paragraph { content, .. } = text.segments[2] else {
126 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[2]);
127 };
128
129 assert_eq!(content, ".");
130
131 let Element::Line(_) = &document.elements[3] else {
132 panic!("Expected Element::Line, got {:?}", document.elements[3]);
133 };
134
135 let Element::Text(text) = &document.elements[4] else {
136 panic!("Expected Element::Text, got {:?}", document.elements[4]);
137 };
138
139 assert_eq!(text.segments.len(), 3);
140
141 let TextSegment::Paragraph { content, .. } = text.segments[0] else {
142 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
143 };
144
145 assert_eq!(content, "This text contains an inline tag ");
146
147 let TextSegment::InlineTag(tag) = &text.segments[1] else {
148 panic!("Expected TextSegment::InlineTag, got {:?}", text.segments[1]);
149 };
150
151 let name = tag.name;
152 let description = tag.description;
153 assert_eq!(name, "see");
154 assert_eq!(description, "\\Some\\Class");
155 assert_eq!(tag.kind, TagKind::See);
156 assert_eq!(&phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize], "{@see \\Some\\Class}");
157
158 let TextSegment::Paragraph { content, .. } = text.segments[2] else {
159 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[2]);
160 };
161
162 assert_eq!(content, ".");
163
164 let Element::Line(_) = &document.elements[5] else {
165 panic!("Expected Element::Line, got {:?}", document.elements[5]);
166 };
167
168 let Element::Code(code) = &document.elements[6] else {
169 panic!("Expected Element::CodeBlock, got {:?}", document.elements[6]);
170 };
171
172 let content = code.content;
173 assert_eq!(code.directives, &["php"]);
174 assert_eq!(content, "echo \"Hello, World!\";");
175 assert_eq!(
176 &phpdoc[code.span.start.offset as usize..code.span.end.offset as usize],
177 "```php\n * echo \"Hello, World!\";\n * ```"
178 );
179
180 let Element::Line(_) = &document.elements[7] else {
181 panic!("Expected Element::Line, got {:?}", document.elements[7]);
182 };
183
184 let Element::Code(code) = &document.elements[8] else {
185 panic!("Expected Element::CodeBlock, got {:?}", document.elements[8]);
186 };
187
188 let content = code.content;
189 assert!(code.directives.is_empty());
190 assert_eq!(content, "$foo = \"bar\";\necho \"Hello, World!\";\n");
191 assert_eq!(
192 &phpdoc[code.span.start.offset as usize..code.span.end.offset as usize],
193 " $foo = \"bar\";\n * echo \"Hello, World!\";\n"
194 );
195
196 let Element::Tag(tag) = &document.elements[9] else {
197 panic!("Expected Element::Tag, got {:?}", document.elements[9]);
198 };
199
200 let name = tag.name;
201 let description = tag.description;
202 assert_eq!(name, "param");
203 assert_eq!(tag.kind, TagKind::Param);
204 assert_eq!(description, "string $foo");
205 assert_eq!(&phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize], "@param string $foo");
206
207 let Element::Tag(tag) = &document.elements[10] else {
208 panic!("Expected Element::Tag, got {:?}", document.elements[10]);
209 };
210
211 let name = tag.name;
212 let description = tag.description;
213 assert_eq!(name, "param");
214 assert_eq!(tag.kind, TagKind::Param);
215 assert_eq!(description, "array{\n bar: string,\n baz: int\n} $bar");
216 assert_eq!(
217 &phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize],
218 "@param array{\n * bar: string,\n * baz: int\n * } $bar"
219 );
220
221 let Element::Tag(tag) = &document.elements[11] else {
222 panic!("Expected Element::Tag, got {:?}", document.elements[11]);
223 };
224
225 let name = tag.name;
226 let description = tag.description;
227 assert_eq!(name, "return");
228 assert_eq!(tag.kind, TagKind::Return);
229 assert_eq!(description, "void");
230 assert_eq!(&phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize], "@return void");
231 }
232
233 #[test]
234 fn test_unclosed_inline_tag() {
235 let arena = Bump::new();
237 let phpdoc = "/** This is a doc block with an unclosed inline tag {@see Class */";
238 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
239
240 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
241
242 match result {
243 Err(ParseError::UnclosedInlineTag(error_span)) => {
244 let expected_start = phpdoc.find("{@see").unwrap();
245 let expected_span = span.subspan(expected_start as u32, phpdoc.len() as u32 - 3);
246 assert_eq!(error_span, expected_span);
247 }
248 _ => {
249 panic!("Expected ParseError::UnclosedInlineTag");
250 }
251 }
252 }
253
254 #[test]
255 fn test_unclosed_inline_code() {
256 let arena = Bump::new();
258 let phpdoc = "/** This is a doc block with unclosed inline code `code sample */";
259 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
260
261 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
262
263 match result {
264 Err(ParseError::UnclosedInlineCode(error_span)) => {
265 let expected_start = phpdoc.find('`').unwrap();
266 let expected_span = span.subspan(expected_start as u32, phpdoc.len() as u32 - 3);
267 assert_eq!(error_span, expected_span);
268 }
269 _ => {
270 panic!("Expected ParseError::UnclosedInlineCode");
271 }
272 }
273 }
274
275 #[test]
276 fn test_unclosed_code_block() {
277 let arena = Bump::new();
278 let phpdoc = r"/**
279 * This is a doc block with unclosed code block
280 * ```
281 * Some code here
282 */";
283 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
284
285 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
286
287 match result {
288 Err(ParseError::UnclosedCodeBlock(error_span)) => {
289 let code_block_start = phpdoc.find("```").unwrap();
290 let expected_span = span.subspan(code_block_start as u32, 109);
291 assert_eq!(error_span, expected_span);
292 }
293 _ => {
294 panic!("Expected ParseError::UnclosedCodeBlock");
295 }
296 }
297 }
298
299 #[test]
300 fn test_invalid_tag_name() {
301 let arena = Bump::new();
303 let phpdoc = "/** @invalid_tag_name Description */";
304 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
305
306 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
307
308 match result {
309 Err(ParseError::InvalidTagName(error_span)) => {
310 let tag_start = phpdoc.find("@invalid_tag_name").unwrap();
311 let tag_end = tag_start + "@invalid_tag_name".len();
312 let expected_span = span.subspan(tag_start as u32, tag_end as u32);
313 assert_eq!(error_span, expected_span);
314 }
315 _ => {
316 panic!("Expected ParseError::InvalidTagName");
317 }
318 }
319 }
320
321 #[test]
322 fn test_malformed_code_block() {
323 let arena = Bump::new();
324 let phpdoc = r"/**
325 * ```
326 * Some code here
327 * Incorrect closing
328 */";
329 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
330
331 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
332
333 match result {
334 Ok(document) => {
335 panic!("Expected the parser to return an error, got {document:#?}");
336 }
337 Err(ParseError::UnclosedCodeBlock(error_span)) => {
338 let code_block_start = phpdoc.find("```").unwrap();
339 let expected_span = span.subspan(code_block_start as u32, 82);
340 assert_eq!(error_span, expected_span);
341 }
342 _ => {
343 panic!("Expected ParseError::UnclosedCodeBlock");
344 }
345 }
346 }
347
348 #[test]
349 fn test_invalid_comment() {
350 let arena = Bump::new();
352 let phpdoc = "/* Not a valid doc block */";
353 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
354
355 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
356
357 match result {
358 Err(ParseError::InvalidComment(error_span)) => {
359 assert_eq!(error_span, span);
360 }
361 _ => {
362 panic!("Expected ParseError::InvalidComment");
363 }
364 }
365 }
366
367 #[test]
368 fn test_inconsistent_indentation() {
369 let arena = Bump::new();
371 let phpdoc = r"/**
372 * This is a doc block
373 * With inconsistent indentation
374 */";
375 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
376
377 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
378
379 match result {
380 Ok(document) => {
381 assert_eq!(document.elements.len(), 1);
382 let Element::Text(text) = &document.elements[0] else {
383 panic!("Expected Element::Text, got {:?}", document.elements[0]);
384 };
385
386 assert_eq!(text.segments.len(), 1);
387 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
388 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
389 };
390
391 assert_eq!(*content, "This is a doc block\nWith inconsistent indentation");
392 assert_eq!(
393 &phpdoc[span.start.offset as usize..span.end.offset as usize],
394 "This is a doc block\n * With inconsistent indentation"
395 );
396 }
397 _ => {
398 panic!("Expected ParseError::InconsistentIndentation");
399 }
400 }
401 }
402
403 #[test]
404 fn test_missing_asterisk() {
405 let arena = Bump::new();
406 let phpdoc = r"/**
407 This line is missing an asterisk
408 * This line is fine
409 */";
410 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
411
412 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
413
414 match result {
415 Ok(document) => {
416 assert_eq!(document.elements.len(), 1);
417 let Element::Text(text) = &document.elements[0] else {
418 panic!("Expected Element::Text, got {:?}", document.elements[0]);
419 };
420
421 assert_eq!(text.segments.len(), 1);
422
423 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
424 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
425 };
426
427 assert_eq!(*content, "This line is missing an asterisk\nThis line is fine");
428 assert_eq!(
429 &phpdoc[span.start.offset as usize..span.end.offset as usize],
430 "This line is missing an asterisk\n * This line is fine"
431 );
432 }
433 _ => {
434 panic!("Expected ParseError::MissingAsterisk");
435 }
436 }
437 }
438
439 #[test]
440 fn test_missing_whitespace_after_asterisk() {
441 let arena = Bump::new();
442 let phpdoc = r"/**
443 *This line is missing a space after asterisk
444 */";
445 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
446
447 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
448
449 match result {
450 Ok(document) => {
451 assert_eq!(document.elements.len(), 1);
452 let Element::Text(text) = &document.elements[0] else {
453 panic!("Expected Element::Text, got {:?}", document.elements[0]);
454 };
455
456 assert_eq!(text.segments.len(), 1);
457 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
458 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
459 };
460
461 assert_eq!(*content, "This line is missing a space after asterisk");
462 assert_eq!(
463 &phpdoc[span.start.offset as usize..span.end.offset as usize],
464 "This line is missing a space after asterisk"
465 );
466 }
467 _ => {
468 panic!("Expected ParseError::MissingWhitespaceAfterAsterisk");
469 }
470 }
471 }
472
473 #[test]
474 fn test_missing_whitespace_after_opening_asterisk() {
475 let arena = Bump::new();
476 let phpdoc = "/**This is a doc block without space after /** */";
477 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
478
479 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
480
481 match result {
482 Ok(document) => {
483 assert_eq!(document.elements.len(), 1);
484 let Element::Text(text) = &document.elements[0] else {
485 panic!("Expected Element::Text, got {:?}", document.elements[0]);
486 };
487
488 assert_eq!(text.segments.len(), 1);
489 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
490 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
491 };
492
493 assert_eq!(*content, "This is a doc block without space after /**");
494 assert_eq!(
495 &phpdoc[span.start.offset as usize..span.end.offset as usize],
496 "This is a doc block without space after /**"
497 );
498 }
499 _ => {
500 panic!("Expected ParseError::MissingWhitespaceAfterOpeningAsterisk");
501 }
502 }
503 }
504
505 #[test]
506 fn test_missing_whitespace_before_closing_asterisk() {
507 let arena = Bump::new();
508 let phpdoc = "/** This is a doc block without space before */*/";
509 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
510
511 let result = parse_phpdoc_with_span(&arena, phpdoc, span);
512
513 match result {
514 Ok(document) => {
515 assert_eq!(document.elements.len(), 1);
516 let Element::Text(text) = &document.elements[0] else {
517 panic!("Expected Element::Text, got {:?}", document.elements[0]);
518 };
519
520 assert_eq!(text.segments.len(), 1);
521 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
522 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
523 };
524
525 assert_eq!(*content, "This is a doc block without space before */");
526 assert_eq!(
527 &phpdoc[span.start.offset as usize..span.end.offset as usize],
528 "This is a doc block without space before */"
529 );
530 }
531 _ => {
532 panic!("Expected ParseError::MissingWhitespaceBeforeClosingAsterisk");
533 }
534 }
535 }
536
537 #[test]
538 fn test_utf8_characters() {
539 let arena = Bump::new();
540 let phpdoc = r#"/**
541 * هذا نص باللغة العربية.
542 * 这是一段中文。
543 * Here are some mathematical symbols: ∑, ∆, π, θ.
544 *
545 * ```php
546 * // Arabic comment
547 * echo "مرحبا بالعالم";
548 * // Chinese comment
549 * echo "你好,世界";
550 * // Math symbols in code
551 * $sum = $a + $b; // ∑
552 * ```
553 *
554 * @param string $مثال A parameter with an Arabic variable name.
555 * @return int 返回值是整数类型。
556 */"#;
557
558 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
559 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
560
561 assert_eq!(document.elements.len(), 6);
563
564 let Element::Text(text) = &document.elements[0] else {
566 panic!("Expected Element::Text, got {:?}", document.elements[0]);
567 };
568
569 assert_eq!(text.segments.len(), 1);
570
571 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
572 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
573 };
574
575 assert_eq!(*content, "هذا نص باللغة العربية.\n这是一段中文。\nHere are some mathematical symbols: ∑, ∆, π, θ.");
576
577 assert_eq!(
578 &phpdoc[span.start.offset as usize..span.end.offset as usize],
579 "هذا نص باللغة العربية.\n * 这是一段中文。\n * Here are some mathematical symbols: ∑, ∆, π, θ."
580 );
581
582 let Element::Line(_) = &document.elements[1] else {
584 panic!("Expected Element::Line, got {:?}", document.elements[3]);
585 };
586
587 let Element::Code(code) = &document.elements[2] else {
589 panic!("Expected Element::Code, got {:?}", document.elements[2]);
590 };
591
592 let content_str = code.content;
593 let expected_code = "// Arabic comment\necho \"مرحبا بالعالم\";\n// Chinese comment\necho \"你好,世界\";\n// Math symbols in code\n$sum = $a + $b; // ∑";
594 assert_eq!(content_str, expected_code);
595 assert_eq!(
596 &phpdoc[code.span.start.offset as usize..code.span.end.offset as usize],
597 "```php\n * // Arabic comment\n * echo \"مرحبا بالعالم\";\n * // Chinese comment\n * echo \"你好,世界\";\n * // Math symbols in code\n * $sum = $a + $b; // ∑\n * ```"
598 );
599
600 let Element::Line(_) = &document.elements[3] else {
602 panic!("Expected Element::Line, got {:?}", document.elements[3]);
603 };
604
605 let Element::Tag(tag) = &document.elements[4] else {
607 panic!("Expected Element::Tag, got {:?}", document.elements[4]);
608 };
609
610 let name = tag.name;
611 let description = tag.description;
612 assert_eq!(name, "param");
613 assert_eq!(tag.kind, TagKind::Param);
614 assert_eq!(description, "string $مثال A parameter with an Arabic variable name.");
615 assert_eq!(
616 &phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize],
617 "@param string $مثال A parameter with an Arabic variable name."
618 );
619
620 let Element::Tag(tag) = &document.elements[5] else {
622 panic!("Expected Element::Tag, got {:?}", document.elements[5]);
623 };
624
625 let name = tag.name;
626 let description = tag.description;
627 assert_eq!(name, "return");
628 assert_eq!(tag.kind, TagKind::Return);
629 assert_eq!(description, "int 返回值是整数类型。");
630 assert_eq!(
631 &phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize],
632 "@return int 返回值是整数类型。"
633 );
634 }
635
636 #[test]
637 fn test_annotation_parsing() {
638 let arena = Bump::new();
639 let phpdoc = r#"/**
640 * @Event("Symfony\Component\Workflow\Event\CompletedEvent")
641 * @AnotherAnnotation({
642 * "key": "value",
643 * "list": [1, 2, 3]
644 * })
645 * @SimpleAnnotation
646 */"#;
647 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
648 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
649
650 assert_eq!(document.elements.len(), 3);
652
653 let Element::Annotation(annotation) = &document.elements[0] else {
655 panic!("Expected Element::Annotation, got {:?}", document.elements[0]);
656 };
657
658 let name = annotation.name;
659 assert_eq!(name, "Event");
660 let arguments = annotation.arguments.unwrap();
661 assert_eq!(arguments, "(\"Symfony\\Component\\Workflow\\Event\\CompletedEvent\")");
662
663 let Element::Annotation(annotation) = &document.elements[1] else {
665 panic!("Expected Element::Annotation, got {:?}", document.elements[1]);
666 };
667
668 let name = annotation.name;
669 assert_eq!(name, "AnotherAnnotation");
670 let arguments = annotation.arguments.unwrap();
671 let expected_arguments = "({\n \"key\": \"value\",\n \"list\": [1, 2, 3]\n})";
672 assert_eq!(arguments, expected_arguments);
673
674 let Element::Annotation(annotation) = &document.elements[2] else {
676 panic!("Expected Element::Annotation, got {:?}", document.elements[2]);
677 };
678
679 let name = annotation.name;
680 assert_eq!(name, "SimpleAnnotation");
681 assert!(annotation.arguments.is_none());
682 }
683
684 #[test]
685 fn test_long_description_with_missing_asterisk() {
686 let arena = Bump::new();
687 let phpdoc = r"/** @var string[] this is a really long description
688 that spans multiple lines, and demonstrates how the parser handles
689 docblocks with multiple descriptions, and missing astricks*/";
690 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
691 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
692
693 assert_eq!(document.elements.len(), 1);
694 let Element::Tag(tag) = &document.elements[0] else {
695 panic!("Expected Element::Tag, got {:?}", document.elements[0]);
696 };
697
698 let name = tag.name;
699 let description = tag.description;
700 assert_eq!(name, "var");
701 assert_eq!(tag.kind, TagKind::Var);
702 assert_eq!(
703 description,
704 "string[] this is a really long description\nthat spans multiple lines, and demonstrates how the parser handles\ndocblocks with multiple descriptions, and missing astricks"
705 );
706 assert_eq!(
707 &phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize],
708 "@var string[] this is a really long description\n that spans multiple lines, and demonstrates how the parser handles\n docblocks with multiple descriptions, and missing astricks"
709 );
710 }
711
712 #[test]
713 fn test_code_indent_using_non_ascii_chars() {
714 let arena = Bump::new();
715 let phpdoc = r"/**
716 * └─ comment 2
717 * └─ comment 4
718 * └─ comment 3
719 */";
720
721 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
722 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
723
724 assert_eq!(document.elements.len(), 1);
725
726 let Element::Code(code) = &document.elements[0] else {
727 panic!("Expected Element::Code, got {:?}", document.elements[0]);
728 };
729
730 let content_str = code.content;
731 assert_eq!(content_str, " └─ comment 2\n\u{a0}\u{a0} └─ comment 4\n └─ comment 3");
732 assert_eq!(
733 &phpdoc[code.span.start.offset as usize..code.span.end.offset as usize],
734 " \u{a0} └─ comment 2\n * \u{a0}\u{a0} └─ comment 4\n * \u{a0} └─ comment 3"
735 );
736 }
737
738 #[test]
739 fn test_issue_456() {
740 let arena = Bump::new();
741 let phpdoc = "/**
742 * \u{3000}(イベント日数をもとに計算)\u{3000}
743 * @return\u{3000}int
744 * @throws\u{3000}Exception
745 */";
746
747 let span = Span::new(FileId::zero(), Position::new(0), Position::new(phpdoc.len() as u32));
748 let document = parse_phpdoc_with_span(&arena, phpdoc, span).expect("Failed to parse PHPDoc");
749
750 assert_eq!(document.elements.len(), 3);
751
752 let Element::Text(text) = &document.elements[0] else {
753 panic!("Expected Element::Text, got {:?}", document.elements[0]);
754 };
755
756 assert_eq!(text.segments.len(), 1);
757 let TextSegment::Paragraph { span, content } = &text.segments[0] else {
758 panic!("Expected TextSegment::Paragraph, got {:?}", text.segments[0]);
759 };
760
761 assert_eq!(*content, " (イベント日数をもとに計算)");
762 assert_eq!(&phpdoc[span.start.offset as usize..span.end.offset as usize], "\u{3000}(イベント日数をもとに計算)");
763
764 let Element::Tag(tag) = &document.elements[1] else {
765 panic!("Expected Element::Tag, got {:?}", document.elements[1]);
766 };
767
768 let name = tag.name;
769 let description = tag.description;
770 assert_eq!(name, "return");
771 assert_eq!(tag.kind, TagKind::Return);
772 assert_eq!(description, "int");
773 assert_eq!(&phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize], "@return\u{3000}int");
774
775 let Element::Tag(tag) = &document.elements[2] else {
776 panic!("Expected Element::Tag, got {:?}", document.elements[2]);
777 };
778
779 let name = tag.name;
780 let description = tag.description;
781 assert_eq!(name, "throws");
782 assert_eq!(tag.kind, TagKind::Throws);
783 assert_eq!(description, "Exception");
784 assert_eq!(&phpdoc[tag.span.start.offset as usize..tag.span.end.offset as usize], "@throws\u{3000}Exception");
785 }
786}