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#[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        // Test case for ParseError::UnclosedInlineTag
226        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        // Test case for ParseError::UnclosedInlineCode
247        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        // Test case for ParseError::InvalidTagName
292        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        // Test case for ParseError::InvalidComment
341        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        // Test case for ParseError::InconsistentIndentation
360        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        // Verify the number of elements parsed
552        assert_eq!(document.elements.len(), 6);
553
554        // First text element (Arabic text)
555        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        // Empty line
573        let Element::Line(_) = &document.elements[1] else {
574            panic!("Expected Element::Line, got {:?}", document.elements[3]);
575        };
576
577        // Code block
578        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        // Empty line
591        let Element::Line(_) = &document.elements[3] else {
592            panic!("Expected Element::Line, got {:?}", document.elements[3]);
593        };
594
595        // @param tag with Arabic variable name
596        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        // @return tag with Chinese description
611        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        // Verify that the document has the expected number of elements
641        assert_eq!(document.elements.len(), 3);
642
643        // First annotation
644        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        // Second annotation
654        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        // Third annotation
665        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}