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#[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#[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#[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#[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
421pub 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
464pub 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
530pub 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
567pub 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
588pub 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
616pub 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
650pub 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
682pub 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
752pub 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
822pub 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#[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
1048pub 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}