Skip to main content

mago_docblock/
tag.rs

1use crate::error::ParseError;
2use mago_span::Span;
3use serde::Deserialize;
4use serde::Serialize;
5use std::fmt;
6
7#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
8pub struct Variable {
9    pub name: Vec<u8>,
10    pub is_variadic: bool,
11    pub is_by_reference: bool,
12}
13
14#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
15pub enum Visibility {
16    Public,
17    Protected,
18    Private,
19}
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
22pub struct Method {
23    pub visibility: Visibility,
24    pub is_static: bool,
25
26    pub name: Vec<u8>,
27    pub argument_list: Vec<Argument>,
28}
29
30#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
31pub struct Argument {
32    pub type_hint: Option<TypeString>,
33    pub variable: Variable,
34    pub has_default: bool,
35    pub argument_span: Span,
36    pub variable_span: Span,
37}
38
39#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
40pub struct PropertyTag {
41    pub span: Span,
42    pub type_string: Option<TypeString>,
43    pub variable: Variable,
44    pub is_read: bool,
45    pub is_write: bool,
46}
47
48impl fmt::Display for Variable {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        if self.is_by_reference {
51            f.write_str("&")?;
52        }
53        if self.is_variadic {
54            f.write_str("...")?;
55        }
56        f.write_str(&String::from_utf8_lossy(&self.name))
57    }
58}
59
60impl fmt::Display for Method {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.write_str(&String::from_utf8_lossy(&self.name))
63    }
64}
65
66#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
67pub struct TypeString {
68    pub value: Vec<u8>,
69    pub span: Span,
70}
71
72#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
73pub struct ReturnTypeTag {
74    pub span: Span,
75    pub type_string: TypeString,
76
77    pub description: Vec<u8>,
78}
79
80#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
81pub struct TypeTag {
82    pub span: Span,
83
84    pub name: Vec<u8>,
85    pub type_string: TypeString,
86}
87
88#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
89pub struct ImportTypeTag {
90    pub span: Span,
91
92    pub name: Vec<u8>,
93
94    pub from: Vec<u8>,
95    pub alias: Option<ByteAlias>,
96}
97
98#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
99#[serde(transparent)]
100pub struct ByteAlias(pub Vec<u8>);
101
102#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
103pub struct ParameterTag {
104    pub span: Span,
105    pub variable: Variable,
106    pub type_string: Option<TypeString>,
107
108    pub description: Vec<u8>,
109}
110
111#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
112pub struct ParameterOutTag {
113    pub span: Span,
114    pub variable: Variable,
115    pub type_string: TypeString,
116}
117
118#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
119pub struct ThrowsTag {
120    pub span: Span,
121    pub type_string: TypeString,
122
123    pub description: Vec<u8>,
124}
125
126#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
127#[repr(u8)]
128pub enum TemplateModifier {
129    Of,
130    As,
131    Super,
132}
133
134#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
135pub struct TemplateTag {
136    pub span: Span,
137
138    pub name: Vec<u8>,
139    pub modifier: Option<TemplateModifier>,
140    pub type_string: Option<TypeString>,
141    pub default: Option<TypeString>,
142    pub covariant: bool,
143    pub contravariant: bool,
144}
145
146#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
147#[repr(u8)]
148pub enum WhereModifier {
149    Is,
150    Colon,
151}
152
153#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
154pub struct WhereTag {
155    pub span: Span,
156
157    pub name: Vec<u8>,
158    pub modifier: WhereModifier,
159    pub type_string: TypeString,
160}
161
162#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
163pub struct AssertionTag {
164    pub span: Span,
165    pub type_string: TypeString,
166    pub variable: Variable,
167}
168
169#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
170pub struct VarTag {
171    pub span: Span,
172    pub type_string: TypeString,
173    pub variable: Option<Variable>,
174}
175
176#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
177pub struct MethodTag {
178    pub span: Span,
179    pub method: Method,
180    pub type_string: TypeString,
181
182    pub description: Vec<u8>,
183}
184
185#[inline]
186fn position_of(haystack: &[u8], needle: u8) -> Option<usize> {
187    memchr::memchr(needle, haystack)
188}
189
190#[inline]
191fn split_once_byte(haystack: &[u8], needle: u8) -> Option<(&[u8], &[u8])> {
192    memchr::memchr(needle, haystack).map(|i| (&haystack[..i], &haystack[i + 1..]))
193}
194
195#[inline]
196fn rsplit_once_byte(haystack: &[u8], needle: u8) -> Option<(&[u8], &[u8])> {
197    memchr::memrchr(needle, haystack).map(|i| (&haystack[..i], &haystack[i + 1..]))
198}
199
200#[inline]
201fn find_ascii_whitespace(haystack: &[u8]) -> Option<usize> {
202    haystack.iter().position(|b| b.is_ascii_whitespace())
203}
204
205#[inline]
206fn split_once_ascii_whitespace(haystack: &[u8]) -> Option<(&[u8], &[u8])> {
207    find_ascii_whitespace(haystack).map(|i| (&haystack[..i], &haystack[i + 1..]))
208}
209
210/// First-byte predicate for a PHP variable name, matching PHP's
211/// `[a-zA-Z_\x80-\xff]` rule. The `\x80-\xff` range is how PHP admits
212/// multi-byte UTF-8 codepoints (e.g. `$café`, `$module🤔`).
213#[inline]
214const fn is_var_name_start(byte: u8) -> bool {
215    matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'_' | 0x80..=0xFF)
216}
217
218/// Continuation-byte predicate for a PHP variable name, matching PHP's
219/// `[a-zA-Z0-9_\x80-\xff]` rule.
220#[inline]
221const fn is_var_name_part(byte: u8) -> bool {
222    matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | 0x80..=0xFF)
223}
224
225/// Parses a `PHPDoc` variable token and returns a structured `Variable`.
226#[inline]
227fn parse_var_ident(raw: &[u8], allow_property_access: bool) -> Option<Variable> {
228    if allow_property_access {
229        if raw.starts_with(b"&") || raw.starts_with(b"...") {
230            return None;
231        }
232
233        if !raw.starts_with(b"$") {
234            return None;
235        }
236
237        let rest = &raw[1..];
238
239        if rest.is_empty() {
240            return None;
241        }
242
243        if !is_var_name_start(rest[0]) {
244            return None;
245        }
246
247        let mut pos = 1;
248        while pos < rest.len() && is_var_name_part(rest[pos]) {
249            pos += 1;
250        }
251
252        while pos < rest.len() {
253            if pos + 1 < rest.len() && &rest[pos..pos + 2] == b"->" {
254                pos += 2;
255
256                if pos >= rest.len() || !is_var_name_start(rest[pos]) {
257                    return None;
258                }
259
260                pos += 1;
261                while pos < rest.len() && is_var_name_part(rest[pos]) {
262                    pos += 1;
263                }
264            } else if rest[pos] == b'[' {
265                pos += 1;
266                let mut bracket_depth = 1;
267
268                while pos < rest.len() && bracket_depth > 0 {
269                    if rest[pos] == b'[' {
270                        bracket_depth += 1;
271                    } else if rest[pos] == b']' {
272                        bracket_depth -= 1;
273                    }
274                    pos += 1;
275                }
276
277                if bracket_depth != 0 {
278                    return None;
279                }
280            } else if rest[pos] == b'(' && rest.get(pos + 1).is_some_and(|b| *b == b')') {
281                pos += 2;
282                break;
283            } else {
284                break;
285            }
286        }
287
288        let token = &raw[..=pos];
289
290        Some(Variable { name: token.to_vec(), is_variadic: false, is_by_reference: false })
291    } else {
292        let is_by_reference = raw.starts_with(b"&");
293        let raw = raw.strip_prefix(b"&").unwrap_or(raw);
294        let (prefix_len, rest, is_variadic) = if let Some(r) = raw.strip_prefix(b"...$") {
295            (4usize, r, true)
296        } else {
297            let r = raw.strip_prefix(b"$")?;
298            (1usize, r, false)
299        };
300
301        if rest.is_empty() {
302            return None;
303        }
304
305        if !is_var_name_start(rest[0]) {
306            return None;
307        }
308
309        let mut len = 1usize;
310        while len < rest.len() && is_var_name_part(rest[len]) {
311            len += 1;
312        }
313
314        let token = &raw[..prefix_len + len];
315        let normalized = if is_variadic { &token[3..] } else { token };
316        Some(Variable { name: normalized.to_vec(), is_variadic, is_by_reference })
317    }
318}
319
320/// Parses the content of a `@template` (and variant) tag.
321///
322/// # Errors
323///
324/// Returns a [`ParseError`] when the tag content is malformed.
325#[inline]
326pub fn parse_template_tag(
327    content: &[u8],
328    span: Span,
329    mut covariant: bool,
330    mut contravariant: bool,
331) -> Result<TemplateTag, ParseError> {
332    let trim_start_offset_rel = content.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0);
333    let trimmed_content = content.trim_ascii();
334
335    if trimmed_content.is_empty() {
336        return Err(ParseError::InvalidTemplateTag(span, "Expected template parameter name".to_string()));
337    }
338
339    let mut parts = trimmed_content.split(|b: &u8| b.is_ascii_whitespace()).filter(|s| !s.is_empty());
340
341    let mut name_part = parts
342        .next()
343        .ok_or_else(|| ParseError::InvalidTemplateTag(span, "Expected template parameter name".to_string()))?;
344    if name_part.starts_with(b"+") && !contravariant && !covariant {
345        covariant = true;
346        name_part = &name_part[1..];
347    } else if name_part.starts_with(b"-") && !contravariant && !covariant {
348        contravariant = true;
349        name_part = &name_part[1..];
350    }
351
352    let name = name_part.to_vec();
353
354    let mut modifier: Option<TemplateModifier> = None;
355    let mut type_string_opt: Option<TypeString> = None;
356    let mut default_opt: Option<TypeString> = None;
357
358    let mut current_offset_rel = trim_start_offset_rel + name_part.len();
359
360    let remaining_after_name = content.get(current_offset_rel..).unwrap_or(b"");
361    let whitespace_len1 = remaining_after_name.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0);
362    let after_whitespace1_offset_rel = current_offset_rel + whitespace_len1;
363    let potential_modifier_slice = remaining_after_name.trim_ascii_start();
364
365    if let Some(rest) = potential_modifier_slice.strip_prefix(b"=") {
366        let after_eq_offset_rel = after_whitespace1_offset_rel + 1;
367        if let Some((default_type, _)) = split_tag_content(rest, span.subspan(after_eq_offset_rel as u32, 0)) {
368            default_opt = Some(default_type);
369        }
370    } else if !potential_modifier_slice.is_empty() {
371        let mut modifier_parts =
372            potential_modifier_slice.split(|b: &u8| b.is_ascii_whitespace()).filter(|s| !s.is_empty()).peekable();
373        if let Some(potential_modifier_str) = modifier_parts.peek().copied() {
374            let lowered = potential_modifier_str.to_ascii_lowercase();
375            let modifier_val = match lowered.as_slice() {
376                b"as" => Some(TemplateModifier::As),
377                b"of" => Some(TemplateModifier::Of),
378                b"super" => Some(TemplateModifier::Super),
379                _ => None,
380            };
381
382            if modifier_val.is_some() {
383                modifier = modifier_val;
384                modifier_parts.next();
385                current_offset_rel = after_whitespace1_offset_rel + potential_modifier_str.len();
386
387                let remaining_after_modifier = content.get(current_offset_rel..).unwrap_or(b"");
388                if let Some((type_string, _)) =
389                    split_tag_content(remaining_after_modifier, span.subspan(current_offset_rel as u32, 0))
390                {
391                    let type_end_rel = (type_string.span.end.offset - span.start.offset) as usize;
392                    type_string_opt = Some(type_string);
393
394                    let after_constraint = content.get(type_end_rel..).unwrap_or(b"");
395                    let trimmed = after_constraint.trim_ascii_start();
396                    if let Some(rest) = trimmed.strip_prefix(b"=") {
397                        let leading_ws = after_constraint.len() - trimmed.len();
398                        let after_eq_offset_rel = type_end_rel + leading_ws + 1;
399                        if let Some((default_type, _)) =
400                            split_tag_content(rest, span.subspan(after_eq_offset_rel as u32, 0))
401                        {
402                            default_opt = Some(default_type);
403                        }
404                    }
405                }
406            }
407        }
408    }
409
410    Ok(TemplateTag {
411        span,
412        name,
413        modifier,
414        type_string: type_string_opt,
415        default: default_opt,
416        covariant,
417        contravariant,
418    })
419}
420
421/// Parses the content of a `@where` tag.
422///
423/// # Errors
424///
425/// Returns a [`ParseError`] when the tag content is malformed.
426pub fn parse_where_tag(content: &[u8], span: Span) -> Result<WhereTag, ParseError> {
427    let name_end_pos = find_ascii_whitespace(content).ok_or_else(|| {
428        ParseError::InvalidWhereTag(span, "Expected template parameter name and constraint".to_string())
429    })?;
430    let (name_part, rest_raw) = content.split_at(name_end_pos);
431    let mut rest = rest_raw;
432
433    if !is_valid_identifier_start(name_part, false) {
434        return Err(ParseError::InvalidWhereTag(
435            span,
436            format!("Invalid template parameter name: '{}'", String::from_utf8_lossy(name_part)),
437        ));
438    }
439
440    rest = rest.trim_ascii_start();
441    let modifier = if rest.starts_with(b"is") && rest.get(2).is_some_and(|b| b.is_ascii_whitespace()) {
442        rest = &rest[2..];
443        WhereModifier::Is
444    } else if rest.starts_with(b":") {
445        rest = &rest[1..];
446        WhereModifier::Colon
447    } else {
448        return Err(ParseError::InvalidWhereTag(
449            span,
450            "Expected 'is' or ':' after template parameter name".to_string(),
451        ));
452    };
453
454    let consumed_len = content.len() - rest.len();
455    let type_part_start_pos = span.start.forward(consumed_len as u32);
456    let type_part_span = Span::new(span.file_id, type_part_start_pos, span.end);
457
458    let (type_string, _rest) = split_tag_content(rest, type_part_span)
459        .ok_or_else(|| ParseError::InvalidWhereTag(span, "Failed to parse type constraint".to_string()))?;
460
461    Ok(WhereTag { span, name: name_part.to_vec(), modifier, type_string })
462}
463
464/// Parses the content of a `@param` tag.
465///
466/// # Errors
467///
468/// Returns a [`ParseError`] when the tag content is malformed.
469pub fn parse_param_tag(content: &[u8], span: Span) -> Result<ParameterTag, ParseError> {
470    let trimmed = content.trim_ascii_start();
471
472    if trimmed.starts_with(b"$") {
473        let raw_name = trimmed
474            .split(|b: &u8| b.is_ascii_whitespace())
475            .find(|s| !s.is_empty())
476            .ok_or_else(|| ParseError::InvalidParameterTag(span, "Expected parameter name".to_string()))?;
477
478        let variable = parse_var_ident(raw_name, false).ok_or_else(|| {
479            ParseError::InvalidParameterTag(
480                span,
481                format!("Invalid parameter name: '{}'", String::from_utf8_lossy(raw_name)),
482            )
483        })?;
484
485        let desc_start = find_subslice(trimmed, &variable.name).map_or(0, |i| i + variable.name.len());
486        let description = trimmed[desc_start..].trim_ascii().to_vec();
487
488        return Ok(ParameterTag { span, variable, type_string: None, description });
489    }
490
491    let (type_string, rest_slice) = split_tag_content(content, span)
492        .ok_or_else(|| ParseError::InvalidParameterTag(span, "Failed to parse parameter type".to_string()))?;
493
494    if type_string.value.is_empty()
495        || type_string.value.starts_with(b"{")
496        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
497    {
498        return Err(ParseError::InvalidParameterTag(
499            span,
500            format!("Invalid parameter type: '{}'", String::from_utf8_lossy(&type_string.value)),
501        ));
502    }
503
504    if rest_slice.is_empty() {
505        return Err(ParseError::InvalidParameterTag(span, "Missing parameter name".to_string()));
506    }
507
508    let raw_name = rest_slice
509        .split(|b: &u8| b.is_ascii_whitespace())
510        .find(|s| !s.is_empty())
511        .ok_or_else(|| ParseError::InvalidParameterTag(span, "Expected parameter name".to_string()))?;
512    let variable = parse_var_ident(raw_name, false).ok_or_else(|| {
513        ParseError::InvalidParameterTag(
514            span,
515            format!("Invalid parameter name: '{}'", String::from_utf8_lossy(raw_name)),
516        )
517    })?;
518
519    let desc_start = find_subslice(rest_slice, &variable.name).map_or(0, |i| i + variable.name.len());
520    let description = rest_slice[desc_start..].trim_ascii_start().to_vec();
521
522    Ok(ParameterTag { span, variable, type_string: Some(type_string), description })
523}
524
525#[inline]
526fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
527    memchr::memmem::find(haystack, needle)
528}
529
530/// Parses the content of a `@param-out` tag.
531///
532/// # Errors
533///
534/// Returns a [`ParseError`] when the tag content is malformed.
535pub fn parse_param_out_tag(content: &[u8], span: Span) -> Result<ParameterOutTag, ParseError> {
536    let (type_string, rest_slice) = split_tag_content(content, span)
537        .ok_or_else(|| ParseError::InvalidParameterOutTag(span, "Failed to parse parameter type".to_string()))?;
538
539    if type_string.value.is_empty()
540        || type_string.value.starts_with(b"{")
541        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
542    {
543        return Err(ParseError::InvalidParameterOutTag(
544            span,
545            format!("Invalid parameter type: '{}'", String::from_utf8_lossy(&type_string.value)),
546        ));
547    }
548
549    if rest_slice.is_empty() {
550        return Err(ParseError::InvalidParameterOutTag(span, "Missing parameter name".to_string()));
551    }
552
553    let raw_name = rest_slice
554        .split(|b: &u8| b.is_ascii_whitespace())
555        .find(|s| !s.is_empty())
556        .ok_or_else(|| ParseError::InvalidParameterOutTag(span, "Expected parameter name".to_string()))?;
557    let variable = parse_var_ident(raw_name, false).ok_or_else(|| {
558        ParseError::InvalidParameterOutTag(
559            span,
560            format!("Invalid parameter name: '{}'", String::from_utf8_lossy(raw_name)),
561        )
562    })?;
563
564    Ok(ParameterOutTag { span, variable, type_string })
565}
566
567/// Parses the content of a `@return` tag.
568///
569/// # Errors
570///
571/// Returns a [`ParseError`] when the tag content is malformed.
572pub fn parse_return_tag(content: &[u8], span: Span) -> Result<ReturnTypeTag, ParseError> {
573    let (type_string, rest_slice) = split_tag_content(content, span)
574        .ok_or_else(|| ParseError::InvalidReturnTag(span, "Failed to parse return type".to_string()))?;
575
576    if type_string.value.starts_with(b"{") {
577        return Err(ParseError::InvalidReturnTag(
578            span,
579            format!("Invalid return type: '{}'", String::from_utf8_lossy(&type_string.value)),
580        ));
581    }
582
583    let description = rest_slice.to_vec();
584
585    Ok(ReturnTypeTag { span, type_string, description })
586}
587
588/// Parses the content of a `@throws` tag.
589///
590/// # Errors
591///
592/// Returns a [`ParseError`] when the tag content is malformed.
593pub fn parse_throws_tag(content: &[u8], span: Span) -> Result<ThrowsTag, ParseError> {
594    let (type_string, rest_slice) = split_tag_content(content, span)
595        .ok_or_else(|| ParseError::InvalidThrowsTag(span, "Failed to parse exception type".to_string()))?;
596
597    if type_string.value.starts_with(b"{") {
598        return Err(ParseError::InvalidThrowsTag(
599            span,
600            format!("Invalid exception type: '{}'", String::from_utf8_lossy(&type_string.value)),
601        ));
602    }
603
604    if type_string.value.starts_with(b"$") && type_string.value != b"$this" {
605        return Err(ParseError::InvalidThrowsTag(
606            span,
607            format!("Invalid exception type: '{}'", String::from_utf8_lossy(&type_string.value)),
608        ));
609    }
610
611    let description = rest_slice.to_vec();
612
613    Ok(ThrowsTag { span, type_string, description })
614}
615
616/// Parses the content of an `@assert`/`@assert-if-true`/`@assert-if-false` tag.
617///
618/// # Errors
619///
620/// Returns a [`ParseError`] when the tag content is malformed.
621pub fn parse_assertion_tag(content: &[u8], span: Span) -> Result<AssertionTag, ParseError> {
622    let (type_string, rest_slice) = split_tag_content(content, span)
623        .ok_or_else(|| ParseError::InvalidAssertionTag(span, "Failed to parse assertion type".to_string()))?;
624
625    if type_string.value.is_empty()
626        || type_string.value.starts_with(b"{")
627        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
628    {
629        return Err(ParseError::InvalidAssertionTag(
630            span,
631            format!("Invalid assertion type: '{}'", String::from_utf8_lossy(&type_string.value)),
632        ));
633    }
634
635    if rest_slice.is_empty() {
636        return Err(ParseError::InvalidAssertionTag(span, "Missing variable name".to_string()));
637    }
638
639    let raw_name = rest_slice
640        .split(|b: &u8| b.is_ascii_whitespace())
641        .find(|s| !s.is_empty())
642        .ok_or_else(|| ParseError::InvalidAssertionTag(span, "Expected variable name".to_string()))?;
643    let variable = parse_var_ident(raw_name, true).ok_or_else(|| {
644        ParseError::InvalidAssertionTag(span, format!("Invalid variable name: '{}'", String::from_utf8_lossy(raw_name)))
645    })?;
646
647    Ok(AssertionTag { span, type_string, variable })
648}
649
650/// Parses the content of a `@var` tag.
651///
652/// # Errors
653///
654/// Returns a [`ParseError`] when the tag content is malformed.
655pub fn parse_var_tag(content: &[u8], span: Span) -> Result<VarTag, ParseError> {
656    let (type_string, rest_slice) = split_tag_content(content, span)
657        .ok_or_else(|| ParseError::InvalidVarTag(span, "Failed to parse variable type".to_string()))?;
658
659    if type_string.value.is_empty()
660        || type_string.value.starts_with(b"{")
661        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
662    {
663        return Err(ParseError::InvalidVarTag(
664            span,
665            format!("Invalid variable type: '{}'", String::from_utf8_lossy(&type_string.value)),
666        ));
667    }
668
669    let variable = if rest_slice.is_empty() {
670        None
671    } else {
672        let var_part = rest_slice
673            .split(|b: &u8| b.is_ascii_whitespace())
674            .find(|s| !s.is_empty())
675            .ok_or_else(|| ParseError::InvalidVarTag(span, "Expected variable name".to_string()))?;
676        parse_var_ident(var_part, true)
677    };
678
679    Ok(VarTag { span, type_string, variable })
680}
681
682/// Parses the content of a `@type` tag.
683///
684/// # Errors
685///
686/// Returns a [`ParseError`] when the tag content is malformed.
687pub fn parse_type_tag(content: &[u8], span: Span) -> Result<TypeTag, ParseError> {
688    let leading_ws = (content.len() - content.trim_ascii_start().len()) as u32;
689    let content = content.trim_ascii_start();
690
691    if content.is_empty() {
692        return Err(ParseError::InvalidTypeTag(span, "Type alias declaration is empty".to_string()));
693    }
694
695    let (potential_name, _) = split_once_ascii_whitespace(content).ok_or_else(|| {
696        let trimmed = content.trim_ascii();
697        ParseError::InvalidTypeTag(
698            span,
699            format!("Type alias name '{}' must be followed by a type definition", String::from_utf8_lossy(trimmed)),
700        )
701    })?;
702
703    let name_len = potential_name.len();
704    let after_name = &content[name_len..];
705    let trimmed_after_name = after_name.trim_ascii_start();
706
707    let (name, type_part, type_offset) = if let Some(after_equals) = trimmed_after_name.strip_prefix(b"=") {
708        let name = potential_name.trim_ascii();
709
710        if !is_valid_identifier_start(name, false) {
711            return Err(ParseError::InvalidTypeTag(
712                span,
713                format!("Invalid type alias name: '{}'", String::from_utf8_lossy(name)),
714            ));
715        }
716
717        let type_start_offset = name_len + (after_name.len() - trimmed_after_name.len()) + 1;
718
719        (name, after_equals, leading_ws + type_start_offset as u32)
720    } else {
721        let name = potential_name.trim_ascii();
722
723        if !is_valid_identifier_start(name, false) {
724            return Err(ParseError::InvalidTypeTag(
725                span,
726                format!("Invalid type alias name: '{}'", String::from_utf8_lossy(name)),
727            ));
728        }
729
730        let rest = after_name.trim_ascii_start();
731        let type_start_offset = name_len + (after_name.len() - rest.len());
732
733        (name, rest, leading_ws + type_start_offset as u32)
734    };
735
736    let (type_string, _) = split_tag_content(type_part, span.subspan(type_offset, 0))
737        .ok_or_else(|| ParseError::InvalidTypeTag(span, "Failed to parse type definition".to_string()))?;
738
739    if type_string.value.is_empty()
740        || type_string.value.starts_with(b"{")
741        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
742    {
743        return Err(ParseError::InvalidTypeTag(
744            span,
745            format!("Invalid type definition: '{}'", String::from_utf8_lossy(&type_string.value)),
746        ));
747    }
748
749    Ok(TypeTag { span, name: name.to_vec(), type_string })
750}
751
752/// Parses the content of an `@import-type` tag.
753///
754/// # Errors
755///
756/// Returns a [`ParseError`] when the tag content is malformed.
757pub fn parse_import_type_tag(content: &[u8], span: Span) -> Result<ImportTypeTag, ParseError> {
758    let trimmed = content.trim_ascii_start();
759    let (name, rest) = split_once_byte(trimmed, b' ').ok_or_else(|| {
760        ParseError::InvalidImportTypeTag(span, "Expected type alias name and 'from' clause".to_string())
761    })?;
762    let name = name.trim_ascii();
763    let rest = rest.trim_ascii();
764
765    if !is_valid_identifier_start(name, false) {
766        return Err(ParseError::InvalidImportTypeTag(
767            span,
768            format!("Invalid type alias name: '{}'", String::from_utf8_lossy(name)),
769        ));
770    }
771
772    if rest.is_empty() {
773        return Err(ParseError::InvalidImportTypeTag(span, "Missing 'from' clause".to_string()));
774    }
775
776    let (from, rest) = split_once_byte(rest, b' ').ok_or_else(|| {
777        ParseError::InvalidImportTypeTag(span, "Expected 'from' keyword followed by class name".to_string())
778    })?;
779
780    if !from.eq_ignore_ascii_case(b"from") {
781        return Err(ParseError::InvalidImportTypeTag(
782            span,
783            format!("Expected 'from' keyword, found '{}'", String::from_utf8_lossy(from)),
784        ));
785    }
786
787    if rest.is_empty() {
788        return Err(ParseError::InvalidImportTypeTag(span, "Missing class name after 'from'".to_string()));
789    }
790
791    let (imported_from, rest) = if let Some((imp_from, rest)) = split_once_byte(rest, b' ') {
792        (imp_from.trim_ascii(), rest.trim_ascii())
793    } else {
794        (rest.trim_ascii(), b"" as &[u8])
795    };
796
797    if !is_valid_identifier_start(imported_from, true) {
798        return Err(ParseError::InvalidImportTypeTag(
799            span,
800            format!("Invalid class name: '{}'", String::from_utf8_lossy(imported_from)),
801        ));
802    }
803
804    let mut alias = None;
805
806    if let Some((r#as, rest)) = split_once_byte(rest, b' ')
807        && r#as.trim_ascii().eq_ignore_ascii_case(b"as")
808        && !rest.is_empty()
809    {
810        let alias_name = rest
811            .split(|b: &u8| b.is_ascii_whitespace())
812            .find(|s| !s.is_empty())
813            .ok_or_else(|| ParseError::InvalidImportTypeTag(span, "Expected alias name after 'as'".to_string()))?
814            .trim_ascii()
815            .to_vec();
816        alias = Some(ByteAlias(alias_name));
817    }
818
819    Ok(ImportTypeTag { span, name: name.to_vec(), from: imported_from.to_vec(), alias })
820}
821
822/// Parses the content of a `@property` tag.
823///
824/// # Errors
825///
826/// Returns a [`ParseError`] when the tag content is malformed.
827pub fn parse_property_tag(
828    content: &[u8],
829    span: Span,
830    is_read: bool,
831    is_write: bool,
832) -> Result<PropertyTag, ParseError> {
833    let trimmed_start = content.trim_ascii_start();
834    let (type_string, variable) = if trimmed_start.starts_with(b"$") && !trimmed_start.starts_with(b"$this") {
835        let var_part = content
836            .split(|b: &u8| b.is_ascii_whitespace())
837            .find(|s| !s.is_empty())
838            .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Expected variable name".to_string()))?;
839        let variable = parse_var_ident(var_part, false).ok_or_else(|| {
840            ParseError::InvalidPropertyTag(
841                span,
842                format!("Invalid variable name: '{}'", String::from_utf8_lossy(var_part)),
843            )
844        })?;
845
846        (None, variable)
847    } else {
848        let (type_string, rest_slice) = split_tag_content(content, span)
849            .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Failed to parse type definition".to_string()))?;
850
851        if type_string.value.is_empty()
852            || type_string.value.starts_with(b"{")
853            || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
854        {
855            return Err(ParseError::InvalidPropertyTag(
856                span,
857                format!("Invalid type definition: '{}'", String::from_utf8_lossy(&type_string.value)),
858            ));
859        }
860
861        if rest_slice.is_empty() {
862            return Err(ParseError::InvalidPropertyTag(span, "Missing variable name after type".to_string()));
863        }
864
865        let var_part = rest_slice
866            .split(|b: &u8| b.is_ascii_whitespace())
867            .find(|s| !s.is_empty())
868            .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Expected variable name".to_string()))?;
869        let variable = parse_var_ident(var_part, false).ok_or_else(|| {
870            ParseError::InvalidPropertyTag(
871                span,
872                format!("Invalid variable name: '{}'", String::from_utf8_lossy(var_part)),
873            )
874        })?;
875
876        (Some(type_string), variable)
877    };
878
879    Ok(PropertyTag { span, type_string, variable, is_read, is_write })
880}
881
882/// Splits tag content into the type-string part and the rest, respecting brackets/quotes.
883#[inline]
884#[must_use]
885pub fn split_tag_content(content: &[u8], input_span: Span) -> Option<(TypeString, &[u8])> {
886    let trim_start_offset = content.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0);
887    let trimmed_start_pos = input_span.start.forward(trim_start_offset as u32);
888
889    let trimmed_content = content.trim_ascii();
890    if trimmed_content.is_empty() {
891        return None;
892    }
893
894    let mut bracket_stack: Vec<u8> = Vec::with_capacity(8);
895    let mut quote_char: Option<u8> = None;
896    let mut escaped = false;
897    let mut last_char_was_significant = false;
898    let mut split_point_rel: Option<usize> = None;
899
900    let bytes = trimmed_content;
901    let len = bytes.len();
902    let mut i = 0usize;
903
904    while i < len {
905        let ch = bytes[i];
906
907        if let Some(q) = quote_char {
908            if ch == q && !escaped {
909                quote_char = None;
910            } else {
911                escaped = ch == b'\\' && !escaped;
912            }
913            last_char_was_significant = true;
914            i += 1;
915            continue;
916        }
917        if ch == b'\'' || ch == b'"' {
918            quote_char = Some(ch);
919            last_char_was_significant = true;
920            i += 1;
921            continue;
922        }
923        match ch {
924            b'<' | b'(' | b'[' | b'{' => bracket_stack.push(ch),
925            b'>' | b')' | b']' | b'}' => match bracket_stack.pop() {
926                Some(opening) if brackets_match(opening, ch) => {}
927                _ => return None,
928            },
929            _ => {}
930        }
931
932        if ch == b':' || ch == b'|' || ch == b'&' {
933            last_char_was_significant = true;
934
935            let mut peek_i = i + 1;
936            let mut has_whitespace_after = false;
937            while peek_i < len && bytes[peek_i].is_ascii_whitespace() {
938                peek_i += 1;
939                has_whitespace_after = true;
940            }
941
942            let next_non_ws = bytes.get(peek_i).copied();
943            if bracket_stack.is_empty() && matches!(next_non_ws, None | Some(b'$')) {
944                split_point_rel = Some(i + 1);
945                break;
946            }
947
948            if has_whitespace_after {
949                while i + 1 < len && bytes[i + 1].is_ascii_whitespace() {
950                    i += 1;
951                }
952            }
953
954            i += 1;
955            continue;
956        }
957
958        if ch == b'/' && bytes.get(i + 1).is_some_and(|&b| b == b'/') {
959            if !bracket_stack.is_empty() {
960                while i + 1 < len {
961                    if bytes[i + 1] == b'\n' {
962                        break;
963                    }
964                    i += 1;
965                }
966                last_char_was_significant = true;
967                i += 1;
968                continue;
969            }
970
971            split_point_rel = Some(i);
972
973            break;
974        }
975
976        if ch.is_ascii_whitespace() {
977            if bracket_stack.is_empty() && last_char_was_significant {
978                let mut peek_i = i + 1;
979                let mut found_continuation = false;
980
981                while peek_i < len && bytes[peek_i].is_ascii_whitespace() {
982                    peek_i += 1;
983                }
984
985                if peek_i < len {
986                    let next_char = bytes[peek_i];
987                    found_continuation = next_char == b':'
988                        || next_char == b'|'
989                        || (next_char == b'&' && {
990                            let after = bytes.get(peek_i + 1).copied();
991                            !matches!(after, Some(b'$') | Some(b'.'))
992                        });
993                }
994
995                if found_continuation {
996                    while i + 1 < len && bytes[i + 1].is_ascii_whitespace() {
997                        i += 1;
998                    }
999
1000                    last_char_was_significant = true;
1001                } else {
1002                    split_point_rel = Some(i);
1003                    break;
1004                }
1005            } else {
1006                last_char_was_significant = false;
1007            }
1008        } else if ch == b'.' {
1009            let prev_is_digit = i > 0 && bytes[i - 1].is_ascii_digit();
1010            let next_is_digit = bytes.get(i + 1).is_some_and(|b| b.is_ascii_digit());
1011
1012            if prev_is_digit && next_is_digit {
1013                last_char_was_significant = true;
1014            } else if bracket_stack.is_empty() && last_char_was_significant {
1015                split_point_rel = Some(i);
1016                break;
1017            } else {
1018                last_char_was_significant = false;
1019            }
1020        } else {
1021            last_char_was_significant = true;
1022        }
1023
1024        i += 1;
1025    }
1026
1027    if !bracket_stack.is_empty() || quote_char.is_some() {
1028        return None;
1029    }
1030
1031    if let Some(split_idx_rel) = split_point_rel {
1032        let type_part_slice = trimmed_content[..split_idx_rel].trim_ascii_end();
1033        let rest_part_slice = trimmed_content[split_idx_rel..].trim_ascii_start();
1034
1035        let type_span =
1036            Span::new(input_span.file_id, trimmed_start_pos, trimmed_start_pos.forward(type_part_slice.len() as u32));
1037
1038        Some((TypeString { value: type_part_slice.to_vec(), span: type_span }, rest_part_slice))
1039    } else {
1040        let type_part_slice = trimmed_content;
1041        let type_span =
1042            Span::new(input_span.file_id, trimmed_start_pos, trimmed_start_pos.forward(type_part_slice.len() as u32));
1043
1044        Some((TypeString { value: type_part_slice.to_vec(), span: type_span }, b"" as &[u8]))
1045    }
1046}
1047
1048/// Parses the content of a `@method` tag.
1049///
1050/// # Errors
1051///
1052/// Returns a [`ParseError`] when the tag content is malformed.
1053pub fn parse_method_tag(mut content: &[u8], mut span: Span) -> Result<MethodTag, ParseError> {
1054    let (trimmed_content, leading_ws) = consume_whitespace(content);
1055    content = trimmed_content;
1056    span = span.subspan(leading_ws as u32, span.length());
1057
1058    let mut is_static = false;
1059    let mut visibility = None;
1060
1061    let mut acc_len = 0;
1062
1063    let mut static_modifier_start = 0u32;
1064    let mut static_modifier_len = 0u32;
1065
1066    loop {
1067        if let Some((new_content, char_count)) = try_consume(content, b"static ") {
1068            if is_static {
1069                return Err(ParseError::InvalidMethodTag(span, "Duplicate 'static' modifier".to_string()));
1070            }
1071
1072            is_static = true;
1073            static_modifier_start = acc_len as u32;
1074            static_modifier_len = 6;
1075            acc_len += char_count;
1076            content = new_content;
1077        } else if let Some((new_content, char_count)) = try_consume(content, b"public ") {
1078            if visibility.is_some() {
1079                return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1080            }
1081
1082            visibility = Some(Visibility::Public);
1083            acc_len += char_count;
1084            content = new_content;
1085        } else if let Some((new_content, char_count)) = try_consume(content, b"protected ") {
1086            if visibility.is_some() {
1087                return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1088            }
1089
1090            visibility = Some(Visibility::Protected);
1091            acc_len += char_count;
1092            content = new_content;
1093        } else if let Some((new_content, char_count)) = try_consume(content, b"private ") {
1094            if visibility.is_some() {
1095                return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1096            }
1097
1098            visibility = Some(Visibility::Private);
1099            acc_len += char_count;
1100            content = new_content;
1101        } else {
1102            break;
1103        }
1104    }
1105
1106    let rest_span = span.subspan(acc_len as u32, span.length());
1107
1108    let (type_string, rest_slice, rest_slice_span) = if is_static && looks_like_method_signature_only(content) {
1109        is_static = false;
1110        let static_span = span.subspan(static_modifier_start, static_modifier_start + static_modifier_len);
1111        let type_string = TypeString { value: b"static".to_vec(), span: static_span };
1112        let (rest_slice, whitespace_count) = consume_whitespace(content);
1113        let rest_slice_span = rest_span.subspan(whitespace_count as u32, rest_span.length());
1114        (type_string, rest_slice, rest_slice_span)
1115    } else {
1116        let type_string = split_tag_content(content, rest_span)
1117            .ok_or_else(|| ParseError::InvalidMethodTag(span, "Failed to parse return type".to_string()))?
1118            .0;
1119        let (rest_slice, whitespace_count) = consume_whitespace(&content[type_string.span.length() as usize..]);
1120        let rest_slice_span =
1121            rest_span.subspan(type_string.span.length() + whitespace_count as u32, rest_span.length());
1122        (type_string, rest_slice, rest_slice_span)
1123    };
1124
1125    if type_string.value.is_empty()
1126        || type_string.value.starts_with(b"{")
1127        || (type_string.value.starts_with(b"$") && type_string.value != b"$this")
1128    {
1129        return Err(ParseError::InvalidMethodTag(
1130            span,
1131            format!("Invalid return type: '{}'", String::from_utf8_lossy(&type_string.value)),
1132        ));
1133    }
1134
1135    if rest_slice.is_empty() {
1136        return Err(ParseError::InvalidMethodTag(span, "Missing method signature".to_string()));
1137    }
1138
1139    let name_end = position_of(rest_slice, b'(').ok_or_else(|| {
1140        ParseError::InvalidMethodTag(span, "Missing opening parenthesis '(' for method arguments".to_string())
1141    })?;
1142
1143    let name = rest_slice[..name_end].trim_ascii();
1144
1145    if name.is_empty() {
1146        return Err(ParseError::InvalidMethodTag(span, "Missing method name".to_string()));
1147    }
1148
1149    let mut depth = 1;
1150    let mut args_end = None;
1151
1152    let mut idx = name_end + 1;
1153    while idx < rest_slice.len() {
1154        match rest_slice[idx] {
1155            b'(' => depth += 1,
1156            b')' => {
1157                depth -= 1;
1158                if depth == 0 {
1159                    args_end = Some(idx);
1160                    break;
1161                }
1162            }
1163            _ => {}
1164        }
1165        idx += 1;
1166    }
1167
1168    let args_end = args_end.ok_or_else(|| {
1169        ParseError::InvalidMethodTag(span, "Missing closing parenthesis ')' for method arguments".to_string())
1170    })?;
1171    let (args_str, whitespace_count) = consume_whitespace(&rest_slice[name_end + 1..args_end]);
1172    let args_span = rest_slice_span.subspan((whitespace_count + name_end) as u32 + 1, args_end as u32);
1173
1174    let description = rest_slice[args_end..].trim_ascii();
1175    let arguments_split = split_args(args_str, args_span);
1176    let arguments = arguments_split.iter().filter_map(|(arg, span)| parse_argument(arg, span)).collect::<Vec<_>>();
1177
1178    let method = Method {
1179        name: name.to_vec(),
1180        argument_list: arguments,
1181        visibility: visibility.unwrap_or(Visibility::Public),
1182        is_static,
1183    };
1184
1185    Ok(MethodTag { span, type_string, method, description: description.to_vec() })
1186}
1187
1188fn consume_whitespace(input: &[u8]) -> (&[u8], usize) {
1189    let mut byte_count = 0;
1190    while byte_count < input.len() && input[byte_count].is_ascii_whitespace() {
1191        byte_count += 1;
1192    }
1193    (&input[byte_count..], byte_count)
1194}
1195
1196fn try_consume<'input>(input: &'input [u8], token: &[u8]) -> Option<(&'input [u8], usize)> {
1197    let (input, whitespace_count) = consume_whitespace(input);
1198
1199    if !input.starts_with(token) {
1200        return None;
1201    }
1202
1203    let len = token.len() + whitespace_count;
1204    let input = &input[token.len()..];
1205
1206    let (input, trailing_whitespace) = consume_whitespace(input);
1207
1208    Some((input, len + trailing_whitespace))
1209}
1210
1211fn looks_like_method_signature_only(content: &[u8]) -> bool {
1212    let trimmed = content.trim_ascii();
1213    if let Some(paren_pos) = position_of(trimmed, b'(') {
1214        let before_paren = trimmed[..paren_pos].trim_ascii();
1215        !before_paren.is_empty() && !before_paren.contains(&b' ')
1216    } else {
1217        false
1218    }
1219}
1220
1221fn split_args(args_str: &[u8], span: Span) -> Vec<(&[u8], Span)> {
1222    let mut args = Vec::new();
1223
1224    let mut start = 0;
1225    let mut depth = 0;
1226    for (i, &ch) in args_str.iter().enumerate() {
1227        match ch {
1228            b'(' | b'[' => depth += 1,
1229            b')' | b']' => depth -= 1,
1230            b',' if depth == 0 => {
1231                let (arg, whitespace_count) = consume_whitespace(&args_str[start..i]);
1232                if !arg.is_empty() {
1233                    args.push((arg, span.subspan((whitespace_count + start) as u32, i as u32)));
1234                }
1235                start = i + 1;
1236            }
1237            _ => {}
1238        }
1239    }
1240
1241    if start < args_str.len() {
1242        let (arg, whitespace_count) = consume_whitespace(&args_str[start..]);
1243        let arg_trimmed = arg.trim_ascii_end();
1244        if !arg.is_empty() {
1245            args.push((
1246                arg_trimmed,
1247                span.subspan(
1248                    (whitespace_count + start) as u32,
1249                    (args_str.len() - arg.len() + arg_trimmed.len()) as u32,
1250                ),
1251            ));
1252        }
1253    }
1254
1255    args
1256}
1257
1258fn parse_argument(arg_str: &[u8], span: &Span) -> Option<Argument> {
1259    let default_value_split = rsplit_once_byte(arg_str, b'=');
1260
1261    let ((arg_type, raw_name), default_value): ((_, _), Option<&[u8]>) =
1262        if let Some((variable_definition, default_value)) = default_value_split {
1263            let arg = variable_definition.trim_ascii();
1264            if let Some((arg_type, raw_name)) = rsplit_once_byte(arg, b' ') {
1265                ((Some(arg_type), raw_name), Some(default_value.trim_ascii()))
1266            } else {
1267                ((None, arg), Some(default_value))
1268            }
1269        } else {
1270            let arg = arg_str.trim_ascii();
1271            if let Some((arg_type, raw_name)) = rsplit_once_byte(arg, b' ') {
1272                ((Some(arg_type), raw_name), None)
1273            } else {
1274                ((None, arg), None)
1275            }
1276        };
1277
1278    let type_string =
1279        arg_type.map(|arg_type| TypeString { value: arg_type.to_vec(), span: span.subspan(0, arg_type.len() as u32) });
1280
1281    let variable_span = span.subspan(arg_type.map_or(0, |t| 1 + t.len() as u32), span.length());
1282
1283    let variable = parse_var_ident(raw_name, false)?;
1284
1285    Some(Argument {
1286        type_hint: type_string,
1287        variable,
1288        has_default: default_value.is_some(),
1289        argument_span: *span,
1290        variable_span,
1291    })
1292}
1293
1294#[inline]
1295const fn brackets_match(open: u8, close: u8) -> bool {
1296    matches!((open, close), (b'<', b'>') | (b'(', b')') | (b'[', b']') | (b'{', b'}'))
1297}
1298
1299#[inline]
1300fn is_valid_identifier_start(mut identifier: &[u8], allow_qualified: bool) -> bool {
1301    if allow_qualified && identifier.starts_with(b"\\") {
1302        identifier = &identifier[1..];
1303    }
1304
1305    if identifier.is_empty() {
1306        return false;
1307    }
1308
1309    let first = identifier[0];
1310    if !(first.is_ascii_alphabetic() || first == b'_') {
1311        return false;
1312    }
1313
1314    identifier.iter().all(|&b| b.is_ascii_alphanumeric() || b == b'_' || (allow_qualified && b == b'\\'))
1315}
1316
1317#[cfg(test)]
1318#[allow(clippy::unwrap_used, clippy::expect_used, clippy::single_char_lifetime_names)]
1319mod tests {
1320    use mago_database::file::FileId;
1321    use mago_span::Position;
1322    use mago_span::Span;
1323
1324    use super::*;
1325
1326    fn test_span(input: &[u8], start_offset: u32) -> Span {
1327        let base_start = Position::new(start_offset);
1328        Span::new(FileId::zero(), base_start, base_start.forward(input.len() as u32))
1329    }
1330
1331    fn test_span_for(s: &[u8]) -> Span {
1332        test_span(s, 0)
1333    }
1334
1335    fn make_span(start: u32, end: u32) -> Span {
1336        Span::new(FileId::zero(), Position::new(start), Position::new(end))
1337    }
1338
1339    #[test]
1340    fn test_parse_var_ident() {
1341        struct Expect<'a> {
1342            s: &'a [u8],
1343            variadic: bool,
1344            by_ref: bool,
1345        }
1346        let cases: &[(&[u8], Option<Expect>)] = &[
1347            (b"$x", Some(Expect { s: b"$x", variadic: false, by_ref: false })),
1348            (b"&$refVar", Some(Expect { s: b"$refVar", variadic: false, by_ref: true })),
1349            (b"$foo,", Some(Expect { s: b"$foo", variadic: false, by_ref: false })),
1350            (b"...$ids)", Some(Expect { s: b"$ids", variadic: true, by_ref: false })),
1351            (b"...$items,", Some(Expect { s: b"$items", variadic: true, by_ref: false })),
1352            (b"$", None),
1353            (b"...$", None),
1354            (b"$1x", None),
1355            (b"foo", None),
1356        ];
1357
1358        for (input, expected) in cases {
1359            let got = parse_var_ident(input, false);
1360            match (got, expected) {
1361                (None, None) => {}
1362                (Some(v), Some(e)) => {
1363                    assert_eq!(v.name, e.s, "input={input:?}");
1364                    assert_eq!(v.is_variadic, e.variadic, "input={input:?}");
1365                    assert_eq!(v.is_by_reference, e.by_ref, "input={input:?}");
1366                }
1367                _ => panic!("mismatch for input={input:?}"),
1368            }
1369        }
1370    }
1371
1372    #[test]
1373    fn test_parse_var_ident_accepts_non_ascii_bytes() {
1374        let input = "$module🤔_lorem_ipsum_dolor_sit_amet_consete".as_bytes();
1375        let parsed = parse_var_ident(input, false).expect("emoji-containing variable should parse");
1376        assert_eq!(parsed.name.as_slice(), input);
1377        assert!(!parsed.is_variadic);
1378        assert!(!parsed.is_by_reference);
1379
1380        let parsed = parse_var_ident(input, true).expect("emoji-containing variable should parse (path mode)");
1381        assert_eq!(parsed.name.as_slice(), input);
1382
1383        let input = "$café".as_bytes();
1384        let parsed = parse_var_ident(input, false).expect("$café should parse");
1385        assert_eq!(parsed.name.as_slice(), input);
1386
1387        let input = "$module🤔->name".as_bytes();
1388        let parsed = parse_var_ident(input, true).expect("property access on emoji name should parse");
1389        assert_eq!(parsed.name.as_slice(), input);
1390    }
1391
1392    #[test]
1393    fn test_variable_display_and_raw() {
1394        let cases: &[(&[u8], &str)] =
1395            &[(b"$x", "$x"), (b"&$x", "&$x"), (b"...$x", "...$x"), (b"...$x)", "...$x"), (b"...$x,", "...$x")];
1396
1397        for (input, expected_raw) in cases {
1398            let v = parse_var_ident(input, false).expect("should parse variable");
1399            assert_eq!(v.to_string(), *expected_raw);
1400        }
1401    }
1402
1403    #[test]
1404    fn test_splitter_brackets() {
1405        let input = b"array<int, (string|bool)> desc";
1406        let span = test_span_for(input);
1407        let (ts, rest) = split_tag_content(input, span).unwrap();
1408        assert_eq!(ts.value, b"array<int, (string|bool)>");
1409        assert_eq!(ts.span, make_span(0, b"array<int, (string|bool)>".len() as u32));
1410        assert_eq!(rest, b"desc");
1411
1412        let input = b"array<int, string> desc";
1413        let span = test_span_for(input);
1414        let (ts, rest) = split_tag_content(input, span).unwrap();
1415        assert_eq!(ts.value, b"array<int, string>");
1416        assert_eq!(ts.span, make_span(0, b"array<int, string>".len() as u32));
1417        assert_eq!(rest, b"desc");
1418
1419        assert!(split_tag_content(b"array<int", test_span_for(b"array<int")).is_none());
1420        assert!(split_tag_content(b"array<int)", test_span_for(b"array<int)")).is_none());
1421        assert!(split_tag_content(b"array(int>", test_span_for(b"array(int>")).is_none());
1422        assert!(split_tag_content(b"string>", test_span_for(b"string>")).is_none());
1423    }
1424
1425    #[test]
1426    fn test_splitter_quotes() {
1427        let input = b" 'inside quote' outside ";
1428        let span = test_span_for(input);
1429        let (ts, rest) = split_tag_content(input, span).unwrap();
1430        assert_eq!(ts.value, b"'inside quote'");
1431        assert_eq!(ts.span, make_span(1, "'inside quote'".len() as u32 + 1));
1432        assert_eq!(rest, b"outside");
1433
1434        let input = br#""string \" with escape" $var"#;
1435        let span = test_span_for(input);
1436        let (ts, rest) = split_tag_content(input, span).unwrap();
1437        assert_eq!(ts.value, br#""string \" with escape""#);
1438        assert_eq!(ts.span, make_span(0, r#""string \" with escape""#.len() as u32));
1439        assert_eq!(rest, b"$var");
1440
1441        assert!(split_tag_content(b"\"unterminated", test_span_for(b"\"unterminated")).is_none());
1442    }
1443
1444    #[test]
1445    fn test_splitter_comments() {
1446        let input = b"(string // comment \n | int) $var";
1447        let span = test_span_for(input);
1448        let (ts, rest) = split_tag_content(input, span).unwrap();
1449        assert_eq!(ts.value, b"(string // comment \n | int)");
1450        assert_eq!(ts.span, make_span(0, "(string // comment \n | int)".len() as u32));
1451        assert_eq!(rest, b"$var");
1452
1453        let input = b"string // comment goes to end";
1454        let span = test_span_for(input);
1455        let (ts, rest) = split_tag_content(input, span).unwrap();
1456        assert_eq!(ts.value, b"string");
1457        assert_eq!(ts.span, make_span(0, "string".len() as u32));
1458        assert_eq!(rest, b"// comment goes to end");
1459
1460        let input = b"array<string // comment\n> $var";
1461        let span = test_span_for(input);
1462        let (ts, rest) = split_tag_content(input, span).unwrap();
1463        assert_eq!(ts.value, b"array<string // comment\n>");
1464        assert_eq!(ts.span, make_span(0, "array<string // comment\n>".len() as u32));
1465        assert_eq!(rest, b"$var");
1466    }
1467
1468    #[test]
1469    fn test_splitter_whole_string_is_type() {
1470        let input = b" array<int, string> ";
1471        let span = test_span_for(input);
1472        let (ts, rest) = split_tag_content(input, span).unwrap();
1473        assert_eq!(ts.value, b"array<int, string>");
1474        assert_eq!(ts.span, make_span(1, "array<int, string>".len() as u32 + 1));
1475        assert_eq!(rest, b"");
1476    }
1477
1478    #[test]
1479    fn test_splitter_with_dot() {
1480        let input = b"string[]. something";
1481        let span = test_span_for(input);
1482        let (ts, rest) = split_tag_content(input, span).unwrap();
1483        assert_eq!(ts.value, b"string[]");
1484        assert_eq!(ts.span, make_span(0, "string[]".len() as u32));
1485        assert_eq!(rest, b". something");
1486    }
1487
1488    #[test]
1489    fn test_param_basic() {
1490        let offset = 10;
1491        let content = b" string|int $myVar Description here ";
1492        let span = test_span(content, offset);
1493        let result = parse_param_tag(content, span).unwrap();
1494
1495        assert_eq!(result.type_string.as_ref().unwrap().value, b"string|int");
1496        assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1);
1497        assert_eq!(result.type_string.as_ref().unwrap().span.end.offset, offset + 1 + "string|int".len() as u32);
1498        assert_eq!(result.variable.name, b"$myVar");
1499        assert_eq!(result.description, b"Description here");
1500        assert_eq!(result.span, span);
1501    }
1502
1503    #[test]
1504    fn test_param_complex_type_no_desc() {
1505        let offset = 5;
1506        let content = b" array<int, string> $param ";
1507        let span = test_span(content, offset);
1508        let result = parse_param_tag(content, span).unwrap();
1509        assert_eq!(result.type_string.as_ref().unwrap().value, b"array<int, string>");
1510        assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1);
1511        assert_eq!(
1512            result.type_string.as_ref().unwrap().span.end.offset,
1513            offset + 1 + "array<int, string>".len() as u32
1514        );
1515        assert_eq!(result.variable.name, b"$param");
1516        assert_eq!(result.description, b"");
1517    }
1518
1519    #[test]
1520    fn test_param_type_with_comment() {
1521        let offset = 20;
1522        let content = b" (string // comment \n | int) $var desc";
1523        let span = test_span(content, offset);
1524        let result = parse_param_tag(content, span).unwrap();
1525        assert_eq!(result.type_string.as_ref().unwrap().value, b"(string // comment \n | int)");
1526        assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1);
1527        assert_eq!(
1528            result.type_string.as_ref().unwrap().span.end.offset,
1529            offset + 1 + "(string // comment \n | int)".len() as u32
1530        );
1531        assert_eq!(result.variable.name, b"$var");
1532        assert_eq!(result.description, b"desc");
1533    }
1534
1535    #[test]
1536    fn test_param_no_type() {
1537        let content = b" $param Description here ";
1538        let span = test_span(content, 0);
1539        let result = parse_param_tag(content, span).unwrap();
1540        assert!(result.type_string.is_none());
1541        assert_eq!(result.variable.name, b"$param");
1542        assert_eq!(result.description, b"Description here");
1543    }
1544
1545    #[test]
1546    fn test_return_basic() {
1547        let offset = 10u32;
1548        let content = b" string Description here ";
1549        let span = test_span(content, offset);
1550        let result = parse_return_tag(content, span).unwrap();
1551        assert_eq!(result.type_string.value, b"string");
1552        assert_eq!(result.type_string.span.start.offset, offset + 1);
1553        assert_eq!(result.type_string.span.end.offset, offset + 1 + "string".len() as u32);
1554        assert_eq!(result.description, b"Description here");
1555        assert_eq!(result.span, span);
1556    }
1557
1558    #[test]
1559    fn test_return_complex_type_with_desc() {
1560        let offset = 0;
1561        let content = b" array<int, (string|null)> Description ";
1562        let span = test_span(content, offset);
1563        let result = parse_return_tag(content, span).unwrap();
1564        assert_eq!(result.type_string.value, b"array<int, (string|null)>");
1565        assert_eq!(result.type_string.span.start.offset, offset + 1);
1566        assert_eq!(result.type_string.span.end.offset, offset + 1 + "array<int, (string|null)>".len() as u32);
1567        assert_eq!(result.description, b"Description");
1568    }
1569
1570    #[test]
1571    fn test_return_complex_type_no_desc() {
1572        let offset = 0;
1573        let content = b" array<int, (string|null)> ";
1574        let span = test_span(content, offset);
1575        let result = parse_return_tag(content, span).unwrap();
1576        assert_eq!(result.type_string.value, b"array<int, (string|null)>");
1577        assert_eq!(result.type_string.span.start.offset, offset + 1);
1578        assert_eq!(result.type_string.span.end.offset, offset + 1 + "array<int, (string|null)>".len() as u32);
1579        assert_eq!(result.description, b"");
1580    }
1581
1582    #[test]
1583    fn test_param_out_no_type() {
1584        let content = b" $myVar ";
1585        let span = test_span(content, 0);
1586        parse_param_out_tag(content, span).unwrap_err();
1587    }
1588
1589    #[test]
1590    fn test_param_out_no_var() {
1591        let content = b" string ";
1592        let span = test_span(content, 0);
1593        parse_param_out_tag(content, span).unwrap_err();
1594    }
1595
1596    #[test]
1597    fn test_type() {
1598        let content = b"MyType = string";
1599        let span = test_span_for(content);
1600        let result = parse_type_tag(content, span).unwrap();
1601        assert_eq!(result.name, b"MyType");
1602        assert_eq!(result.type_string.value, b"string");
1603        assert_eq!(result.type_string.span.start.offset, 9);
1604        assert_eq!(result.type_string.span.end.offset, 9 + "string".len() as u32);
1605        assert_eq!(result.span, span);
1606    }
1607
1608    #[test]
1609    fn test_import_type() {
1610        let content = b"MyType from \\My\\Namespace\\Class as Alias";
1611        let span = test_span_for(content);
1612        let result = parse_import_type_tag(content, span).unwrap();
1613        assert_eq!(result.name, b"MyType");
1614        assert_eq!(result.from, b"\\My\\Namespace\\Class");
1615        assert_eq!(result.alias.as_ref().map(|a| a.0.as_slice()), Some(b"Alias" as &[u8]));
1616        assert_eq!(result.span, span);
1617    }
1618
1619    #[test]
1620    fn test_param_trailing_comma_is_ignored_in_name() {
1621        let content = b" string $foo, desc";
1622        let span = test_span_for(content);
1623        let result = parse_param_tag(content, span).unwrap();
1624        assert_eq!(result.variable.name, b"$foo");
1625        assert_eq!(result.description, b", desc");
1626    }
1627
1628    #[test]
1629    fn test_param_variadic_trailing_paren_is_ignored_in_name() {
1630        let content = b" list<int> ...$items) rest";
1631        let span = test_span_for(content);
1632        let result = parse_param_tag(content, span).unwrap();
1633        assert_eq!(result.variable.name, b"$items");
1634        assert_eq!(result.description, b") rest");
1635    }
1636
1637    #[test]
1638    fn test_param_out_trailing_comma() {
1639        let content = b" int $out,";
1640        let span = test_span_for(content);
1641        let result = parse_param_out_tag(content, span).unwrap();
1642        assert_eq!(result.variable.name, b"$out");
1643    }
1644
1645    #[test]
1646    fn test_assertion_trailing_comma() {
1647        let content = b" int $x,";
1648        let span = test_span_for(content);
1649        let result = parse_assertion_tag(content, span).unwrap();
1650        assert_eq!(result.type_string.value, b"int");
1651        assert_eq!(result.variable.name, b"$x");
1652    }
1653
1654    #[test]
1655    fn test_assertion_method_call() {
1656        let content = b" Statement $this->first()";
1657        let span = test_span_for(content);
1658        let result = parse_assertion_tag(content, span).unwrap();
1659        assert_eq!(result.type_string.value, b"Statement");
1660        assert_eq!(result.variable.name, b"$this->first()");
1661    }
1662
1663    #[test]
1664    fn test_assertion_property_access() {
1665        let content = b" Statement $this->property";
1666        let span = test_span_for(content);
1667        let result = parse_assertion_tag(content, span).unwrap();
1668        assert_eq!(result.type_string.value, b"Statement");
1669        assert_eq!(result.variable.name, b"$this->property");
1670    }
1671
1672    #[test]
1673    fn test_param_trailing_without_space() {
1674        let content = b" string $foo,desc";
1675        let span = test_span_for(content);
1676        let result = parse_param_tag(content, span).unwrap();
1677        assert_eq!(result.variable.name, b"$foo");
1678        assert_eq!(result.description, b",desc");
1679    }
1680
1681    #[test]
1682    fn test_param_variadic_trailing_paren_without_space() {
1683        let content = b" list<int> ...$items)more";
1684        let span = test_span_for(content);
1685        let result = parse_param_tag(content, span).unwrap();
1686        assert_eq!(result.variable.name, b"$items");
1687        assert_eq!(result.description, b")more");
1688    }
1689
1690    #[test]
1691    fn test_param_with_numeric_literals_in_union() {
1692        let content = b"-1|-24.0|string $a";
1693        let span = test_span_for(content);
1694        let result = parse_param_tag(content, span).unwrap();
1695        assert_eq!(result.type_string.as_ref().unwrap().value, b"-1|-24.0|string");
1696        assert_eq!(result.variable.name, b"$a");
1697        assert_eq!(result.description, b"");
1698    }
1699
1700    #[test]
1701    fn test_param_with_float_literals() {
1702        let content = b"1.5|2.0|3.14 $value";
1703        let span = test_span_for(content);
1704        let result = parse_param_tag(content, span).unwrap();
1705        assert_eq!(result.type_string.as_ref().unwrap().value, b"1.5|2.0|3.14");
1706        assert_eq!(result.variable.name, b"$value");
1707    }
1708
1709    #[test]
1710    fn test_splitter_with_dot_still_works_as_separator() {
1711        let input = b"string[]. something else";
1712        let span = test_span_for(input);
1713        let (ts, rest) = split_tag_content(input, span).unwrap();
1714        assert_eq!(ts.value, b"string[]");
1715        assert_eq!(rest, b". something else");
1716    }
1717
1718    #[test]
1719    fn test_splitter_with_colon_after_whitespace() {
1720        let input = b"callable(string)    :         string     $callback";
1721        let span = test_span_for(input);
1722        let (ts, rest) = split_tag_content(input, span).unwrap();
1723        assert_eq!(ts.value, b"callable(string)    :         string");
1724        assert_eq!(rest, b"$callback");
1725
1726        let input2 = b"callable(string) : string $callback";
1727        let span2 = test_span_for(input2);
1728        let (ts2, rest2) = split_tag_content(input2, span2).unwrap();
1729        assert_eq!(ts2.value, b"callable(string) : string");
1730        assert_eq!(rest2, b"$callback");
1731
1732        let input3 = b"callable(string): string $callback";
1733        let span3 = test_span_for(input3);
1734        let (ts3, rest3) = split_tag_content(input3, span3).unwrap();
1735        assert_eq!(ts3.value, b"callable(string): string");
1736        assert_eq!(rest3, b"$callback");
1737    }
1738
1739    #[test]
1740    fn test_consume_whitespace_ascii_only() {
1741        let input = b"   rest";
1742        let (rest, count) = consume_whitespace(input);
1743        assert_eq!(rest, b"rest");
1744        assert_eq!(count, 3);
1745
1746        let input2 = b"\t \trest";
1747        let (rest2, count2) = consume_whitespace(input2);
1748        assert_eq!(rest2, b"rest");
1749        assert_eq!(count2, 3);
1750    }
1751}