mago_docblock/
lib.rs

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/// Parses a docblock from a trivia token.
17///
18/// # Errors
19///
20/// Returns a [`ParseError`] if the trivia is not a docblock comment or parsing fails.
21#[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/// Parses a `PHPDoc` comment string with an associated span.
31///
32/// # Errors
33///
34/// Returns a [`ParseError`] if tokenization or parsing fails.
35#[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        // Test case for ParseError::UnclosedInlineTag
236        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        // Test case for ParseError::UnclosedInlineCode
257        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        // Test case for ParseError::InvalidTagName
302        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        // Test case for ParseError::InvalidComment
351        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        // Test case for ParseError::InconsistentIndentation
370        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        // Verify the number of elements parsed
562        assert_eq!(document.elements.len(), 6);
563
564        // First text element (Arabic text)
565        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        // Empty line
583        let Element::Line(_) = &document.elements[1] else {
584            panic!("Expected Element::Line, got {:?}", document.elements[3]);
585        };
586
587        // Code block
588        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        // Empty line
601        let Element::Line(_) = &document.elements[3] else {
602            panic!("Expected Element::Line, got {:?}", document.elements[3]);
603        };
604
605        // @param tag with Arabic variable name
606        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        // @return tag with Chinese description
621        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        // Verify that the document has the expected number of elements
651        assert_eq!(document.elements.len(), 3);
652
653        // First annotation
654        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        // Second annotation
664        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        // Third annotation
675        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}