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