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: String, pub is_variadic: bool, pub is_by_reference: bool, }
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 pub name: String,
26 pub argument_list: Vec<Argument>,
27}
28
29#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
30pub struct Argument {
31 pub type_hint: Option<TypeString>,
32 pub variable: Variable,
33 pub has_default: bool,
34 pub argument_span: Span,
35 pub variable_span: Span,
36}
37
38#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
39pub struct PropertyTag {
40 pub span: Span,
41 pub type_string: Option<TypeString>,
42 pub variable: Variable,
43 pub is_read: bool,
44 pub is_write: bool,
45}
46
47impl fmt::Display for Variable {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 if self.is_by_reference {
50 f.write_str("&")?;
51 }
52 if self.is_variadic {
53 f.write_str("...")?;
54 }
55 f.write_str(&self.name)
56 }
57}
58
59impl fmt::Display for Method {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(&self.name)
62 }
63}
64
65#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
66pub struct TypeString {
67 pub value: String,
68 pub span: Span,
69}
70
71#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
72pub struct ReturnTypeTag {
73 pub span: Span,
74 pub type_string: TypeString,
75 pub description: String,
76}
77
78#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
79pub struct TypeTag {
80 pub span: Span,
81 pub name: String,
82 pub type_string: TypeString,
83}
84
85#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
86pub struct ImportTypeTag {
87 pub span: Span,
88 pub name: String,
89 pub from: String,
90 pub alias: Option<String>,
91}
92
93#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
94pub struct ParameterTag {
95 pub span: Span,
96 pub variable: Variable,
97 pub type_string: Option<TypeString>,
98 pub description: String,
99}
100
101#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
102pub struct ParameterOutTag {
103 pub span: Span,
104 pub variable: Variable,
105 pub type_string: TypeString,
106}
107
108#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
109pub struct ThrowsTag {
110 pub span: Span,
111 pub type_string: TypeString,
112 pub description: String,
113}
114
115#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
116#[repr(u8)]
117pub enum TemplateModifier {
118 Of,
119 As,
120 Super,
121}
122
123#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
124pub struct TemplateTag {
125 pub span: Span,
127 pub name: String,
129 pub modifier: Option<TemplateModifier>,
131 pub type_string: Option<TypeString>,
133 pub covariant: bool,
135 pub contravariant: bool,
137}
138
139#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
140#[repr(u8)]
141pub enum WhereModifier {
142 Is,
143 Colon,
144}
145
146#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
147pub struct WhereTag {
148 pub span: Span,
150 pub name: String,
152 pub modifier: WhereModifier,
154 pub type_string: TypeString,
156}
157
158#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
159pub struct AssertionTag {
160 pub span: Span,
161 pub type_string: TypeString,
162 pub variable: Variable,
163}
164
165#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
166pub struct VarTag {
167 pub span: Span,
168 pub type_string: TypeString,
169 pub variable: Option<Variable>,
170}
171
172#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
173pub struct MethodTag {
174 pub span: Span,
175 pub method: Method,
176 pub type_string: TypeString,
177 pub description: String,
178}
179#[inline]
207fn parse_var_ident(raw: &str, allow_property_access: bool) -> Option<Variable> {
208 if allow_property_access {
209 if raw.starts_with('&') || raw.starts_with("...") {
211 return None;
212 }
213
214 if !raw.starts_with('$') {
216 return None;
217 }
218
219 let rest = &raw[1..]; let bytes = rest.as_bytes();
221
222 if bytes.is_empty() {
223 return None;
224 }
225
226 let is_start = |b: u8| b == b'_' || b.is_ascii_alphabetic();
228 let is_cont = |b: u8| is_start(b) || b.is_ascii_digit();
229
230 if !is_start(bytes[0]) {
231 return None;
232 }
233
234 let mut pos = 1;
235 while pos < bytes.len() && is_cont(bytes[pos]) {
236 pos += 1;
237 }
238
239 while pos < bytes.len() {
241 if pos + 1 < bytes.len() && &bytes[pos..pos + 2] == b"->" {
242 pos += 2; if pos >= bytes.len() || !is_start(bytes[pos]) {
246 return None; }
248
249 pos += 1;
250 while pos < bytes.len() && is_cont(bytes[pos]) {
251 pos += 1;
252 }
253 } else if bytes[pos] == b'[' {
254 pos += 1; let mut bracket_depth = 1;
257
258 while pos < bytes.len() && bracket_depth > 0 {
259 if bytes[pos] == b'[' {
260 bracket_depth += 1;
261 } else if bytes[pos] == b']' {
262 bracket_depth -= 1;
263 }
264 pos += 1;
265 }
266
267 if bracket_depth != 0 {
268 return None; }
270 } else {
271 break;
273 }
274 }
275
276 let token = &raw[..=pos]; Some(Variable { name: token.to_owned(), is_variadic: false, is_by_reference: false })
280 } else {
281 let is_by_reference = raw.starts_with('&');
283 let raw = raw.strip_prefix('&').unwrap_or(raw);
285 let (prefix_len, rest, is_variadic) = if let Some(r) = raw.strip_prefix("...$") {
287 (4usize, r, true)
288 } else if let Some(r) = raw.strip_prefix('$') {
289 (1usize, r, false)
290 } else {
291 return None;
292 };
293 let bytes = rest.as_bytes();
295 if bytes.is_empty() {
296 return None;
297 }
298 let is_start = |b: u8| b == b'_' || b.is_ascii_alphabetic();
299 let is_cont = |b: u8| is_start(b) || b.is_ascii_digit();
300 if !is_start(bytes[0]) {
301 return None;
302 }
303 let mut len = 1usize;
304 while len < bytes.len() && is_cont(bytes[len]) {
305 len += 1;
306 }
307 let token = &raw[..prefix_len + len];
308 let normalized = if is_variadic { &token[3..] } else { token };
310 Some(Variable { name: normalized.to_owned(), is_variadic, is_by_reference })
311 }
312}
313
314#[inline]
339pub fn parse_template_tag(
340 content: &str,
341 span: Span,
342 mut covariant: bool,
343 mut contravariant: bool,
344) -> Result<TemplateTag, ParseError> {
345 let trim_start_offset_rel = content.find(|c: char| !c.is_whitespace()).unwrap_or(0);
347 let trimmed_content = content.trim();
348
349 if trimmed_content.is_empty() {
350 return Err(ParseError::InvalidTemplateTag(span, "Expected template parameter name".to_string()));
351 }
352
353 let mut parts = trimmed_content.split_whitespace();
354
355 let mut name_part = parts
356 .next()
357 .ok_or_else(|| ParseError::InvalidTemplateTag(span, "Expected template parameter name".to_string()))?;
358 if name_part.starts_with('+') && !contravariant && !covariant {
359 covariant = true;
360 name_part = &name_part[1..];
361 } else if name_part.starts_with('-') && !contravariant && !covariant {
362 contravariant = true;
363 name_part = &name_part[1..];
364 }
365
366 let name = name_part.to_string();
367
368 let mut modifier: Option<TemplateModifier> = None;
369 let mut type_string_opt: Option<TypeString> = None;
370
371 let mut current_offset_rel = trim_start_offset_rel + name_part.len();
374
375 let remaining_after_name = content.get(current_offset_rel..).unwrap_or("");
378 let whitespace_len1 = remaining_after_name.find(|c: char| !c.is_whitespace()).unwrap_or(0);
379 let after_whitespace1_offset_rel = current_offset_rel + whitespace_len1;
380 let potential_modifier_slice = remaining_after_name.trim_start();
381
382 if !potential_modifier_slice.is_empty() {
383 let mut modifier_parts = potential_modifier_slice.split_whitespace().peekable();
384 if let Some(potential_modifier_str) = modifier_parts.peek().copied() {
385 let modifier_val = match potential_modifier_str.to_ascii_lowercase().as_str() {
386 "as" => Some(TemplateModifier::As),
387 "of" => Some(TemplateModifier::Of),
388 "super" => Some(TemplateModifier::Super),
389 _ => None,
390 };
391
392 if modifier_val.is_some() {
393 modifier = modifier_val;
394 modifier_parts.next();
395 current_offset_rel = after_whitespace1_offset_rel + potential_modifier_str.len();
396
397 let remaining_after_modifier = content.get(current_offset_rel..).unwrap_or("");
399 if let Some((type_string, _)) =
400 split_tag_content(remaining_after_modifier, span.subspan(current_offset_rel as u32, 0))
401 {
402 type_string_opt = Some(type_string);
403 }
404 }
405 }
406 }
407
408 Ok(TemplateTag { span, name, modifier, type_string: type_string_opt, covariant, contravariant })
409}
410
411pub fn parse_where_tag(content: &str, span: Span) -> Result<WhereTag, ParseError> {
422 let name_end_pos = content.find(char::is_whitespace).ok_or_else(|| {
423 ParseError::InvalidWhereTag(span, "Expected template parameter name and constraint".to_string())
424 })?;
425 let (name_part, mut rest) = content.split_at(name_end_pos);
426
427 if !is_valid_identifier_start(name_part, false) {
428 return Err(ParseError::InvalidWhereTag(span, format!("Invalid template parameter name: '{name_part}'")));
429 }
430
431 rest = rest.trim_start();
432 let modifier = if rest.starts_with("is") && rest.chars().nth(2).is_some_and(char::is_whitespace) {
433 rest = &rest[2..];
434 WhereModifier::Is
435 } else if rest.starts_with(':') {
436 rest = &rest[1..];
437 WhereModifier::Colon
438 } else {
439 return Err(ParseError::InvalidWhereTag(
440 span,
441 "Expected 'is' or ':' after template parameter name".to_string(),
442 ));
443 };
444
445 let consumed_len = content.len() - rest.len();
446 let type_part_start_pos = span.start.forward(consumed_len as u32);
447 let type_part_span = Span::new(span.file_id, type_part_start_pos, span.end);
448
449 let (type_string, _rest) = split_tag_content(rest, type_part_span)
450 .ok_or_else(|| ParseError::InvalidWhereTag(span, "Failed to parse type constraint".to_string()))?;
451
452 Ok(WhereTag { span, name: name_part.to_owned(), modifier, type_string })
453}
454
455pub fn parse_param_tag(content: &str, span: Span) -> Result<ParameterTag, ParseError> {
466 let trimmed = content.trim_start();
467
468 if trimmed.starts_with('$') {
470 let mut parts = trimmed.split_whitespace();
472 let raw_name =
473 parts.next().ok_or_else(|| ParseError::InvalidParameterTag(span, "Expected parameter name".to_string()))?;
474
475 let variable = parse_var_ident(raw_name, false)
476 .ok_or_else(|| ParseError::InvalidParameterTag(span, format!("Invalid parameter name: '{raw_name}'")))?;
477
478 let desc_start = trimmed.find(&variable.name).map_or(0, |i| i + variable.name.len());
479 let description = trimmed[desc_start..].trim().to_owned();
480
481 return Ok(ParameterTag { span, variable, type_string: None, description });
482 }
483
484 let (type_string, rest_slice) = split_tag_content(content, span)
486 .ok_or_else(|| ParseError::InvalidParameterTag(span, "Failed to parse parameter type".to_string()))?;
487
488 if type_string.value.is_empty()
490 || type_string.value.starts_with('{')
491 || (type_string.value.starts_with('$') && type_string.value != "$this")
492 {
493 return Err(ParseError::InvalidParameterTag(span, format!("Invalid parameter type: '{}'", type_string.value)));
494 }
495
496 if rest_slice.is_empty() {
497 return Err(ParseError::InvalidParameterTag(span, "Missing parameter name".to_string()));
499 }
500
501 let mut rest_parts = rest_slice.split_whitespace();
502 let raw_name = rest_parts
503 .next()
504 .ok_or_else(|| ParseError::InvalidParameterTag(span, "Expected parameter name".to_string()))?;
505 let variable = parse_var_ident(raw_name, false)
506 .ok_or_else(|| ParseError::InvalidParameterTag(span, format!("Invalid parameter name: '{raw_name}'")))?;
507
508 let desc_start = rest_slice.find(&variable.name).map_or(0, |i| i + variable.name.len());
509 let description = rest_slice[desc_start..].trim_start().to_owned();
510
511 Ok(ParameterTag { span, variable, type_string: Some(type_string), description })
512}
513
514pub fn parse_param_out_tag(content: &str, span: Span) -> Result<ParameterOutTag, ParseError> {
525 let (type_string, rest_slice) = split_tag_content(content, span)
526 .ok_or_else(|| ParseError::InvalidParameterOutTag(span, "Failed to parse parameter type".to_string()))?;
527
528 if type_string.value.is_empty()
530 || type_string.value.starts_with('{')
531 || (type_string.value.starts_with('$') && type_string.value != "$this")
532 {
533 return Err(ParseError::InvalidParameterOutTag(
534 span,
535 format!("Invalid parameter type: '{}'", type_string.value),
536 ));
537 }
538
539 if rest_slice.is_empty() {
540 return Err(ParseError::InvalidParameterOutTag(span, "Missing parameter name".to_string()));
541 }
542
543 let raw_name = rest_slice
544 .split_whitespace()
545 .next()
546 .ok_or_else(|| ParseError::InvalidParameterOutTag(span, "Expected parameter name".to_string()))?;
547 let variable = parse_var_ident(raw_name, false)
548 .ok_or_else(|| ParseError::InvalidParameterOutTag(span, format!("Invalid parameter name: '{raw_name}'")))?;
549
550 Ok(ParameterOutTag { span, variable, type_string })
551}
552
553pub fn parse_return_tag(content: &str, span: Span) -> Result<ReturnTypeTag, ParseError> {
564 let (type_string, rest_slice) = split_tag_content(content, span)
565 .ok_or_else(|| ParseError::InvalidReturnTag(span, "Failed to parse return type".to_string()))?;
566
567 if type_string.value.starts_with('{') {
569 return Err(ParseError::InvalidReturnTag(span, format!("Invalid return type: '{}'", type_string.value)));
570 }
571
572 let description = rest_slice.to_owned();
573
574 Ok(ReturnTypeTag { span, type_string, description })
575}
576
577pub fn parse_throws_tag(content: &str, span: Span) -> Result<ThrowsTag, ParseError> {
588 let (type_string, rest_slice) = split_tag_content(content, span)
589 .ok_or_else(|| ParseError::InvalidThrowsTag(span, "Failed to parse exception type".to_string()))?;
590
591 if type_string.value.starts_with('{') {
593 return Err(ParseError::InvalidThrowsTag(span, format!("Invalid exception type: '{}'", type_string.value)));
594 }
595
596 if type_string.value.starts_with('$') && type_string.value != "$this" {
598 return Err(ParseError::InvalidThrowsTag(span, format!("Invalid exception type: '{}'", type_string.value)));
599 }
600
601 let description = rest_slice.to_owned();
602
603 Ok(ThrowsTag { span, type_string, description })
604}
605
606pub fn parse_assertion_tag(content: &str, span: Span) -> Result<AssertionTag, ParseError> {
617 let (type_string, rest_slice) = split_tag_content(content, span)
618 .ok_or_else(|| ParseError::InvalidAssertionTag(span, "Failed to parse assertion type".to_string()))?;
619
620 if type_string.value.is_empty()
622 || type_string.value.starts_with('{')
623 || (type_string.value.starts_with('$') && type_string.value != "$this")
624 {
625 return Err(ParseError::InvalidAssertionTag(span, format!("Invalid assertion type: '{}'", type_string.value)));
626 }
627
628 if rest_slice.is_empty() {
629 return Err(ParseError::InvalidAssertionTag(span, "Missing variable name".to_string()));
631 }
632
633 let mut rest_parts = rest_slice.split_whitespace();
634
635 let raw_name =
636 rest_parts.next().ok_or_else(|| ParseError::InvalidAssertionTag(span, "Expected variable name".to_string()))?;
637 let variable = parse_var_ident(raw_name, true)
638 .ok_or_else(|| ParseError::InvalidAssertionTag(span, format!("Invalid variable name: '{raw_name}'")))?;
639
640 Ok(AssertionTag { span, type_string, variable })
641}
642
643pub fn parse_var_tag(content: &str, span: Span) -> Result<VarTag, ParseError> {
654 let (type_string, rest_slice) = split_tag_content(content, span)
655 .ok_or_else(|| ParseError::InvalidVarTag(span, "Failed to parse variable type".to_string()))?;
656
657 if type_string.value.is_empty()
659 || type_string.value.starts_with('{')
660 || (type_string.value.starts_with('$') && type_string.value != "$this")
661 {
662 return Err(ParseError::InvalidVarTag(span, format!("Invalid variable type: '{}'", type_string.value)));
663 }
664
665 let variable = if rest_slice.is_empty() {
666 None
667 } else {
668 let var_part = rest_slice
669 .split_whitespace()
670 .next()
671 .ok_or_else(|| ParseError::InvalidVarTag(span, "Expected variable name".to_string()))?;
672 parse_var_ident(var_part, true)
673 };
674
675 Ok(VarTag { span, type_string, variable })
676}
677
678pub fn parse_type_tag(content: &str, span: Span) -> Result<TypeTag, ParseError> {
689 let leading_ws = (content.len() - content.trim_start().len()) as u32;
690 let content = content.trim_start();
691
692 if content.is_empty() {
693 return Err(ParseError::InvalidTypeTag(span, "Type alias declaration is empty".to_string()));
694 }
695
696 let (potential_name, _) = content.split_once(char::is_whitespace).ok_or_else(|| {
697 let trimmed = content.trim();
698 ParseError::InvalidTypeTag(span, format!("Type alias name '{trimmed}' must be followed by a type definition"))
699 })?;
700
701 let name_len = potential_name.len();
702 let after_name = &content[name_len..];
703 let trimmed_after_name = after_name.trim_start();
704
705 let (name, type_part, type_offset) = if let Some(after_equals) = trimmed_after_name.strip_prefix('=') {
706 let name = potential_name.trim();
708
709 if !is_valid_identifier_start(name, false) {
710 return Err(ParseError::InvalidTypeTag(span, format!("Invalid type alias name: '{name}'")));
711 }
712
713 let type_start_offset = name_len + (after_name.len() - trimmed_after_name.len()) + 1;
714
715 (name, after_equals, leading_ws + type_start_offset as u32)
716 } else {
717 let name = potential_name.trim();
718
719 if !is_valid_identifier_start(name, false) {
720 return Err(ParseError::InvalidTypeTag(span, format!("Invalid type alias name: '{name}'")));
721 }
722
723 let rest = after_name.trim_start();
724 let type_start_offset = name_len + (after_name.len() - rest.len());
725
726 (name, rest, leading_ws + type_start_offset as u32)
727 };
728
729 let (type_string, _) = split_tag_content(type_part, span.subspan(type_offset, 0))
730 .ok_or_else(|| ParseError::InvalidTypeTag(span, "Failed to parse type definition".to_string()))?;
731
732 if type_string.value.is_empty()
733 || type_string.value.starts_with('{')
734 || (type_string.value.starts_with('$') && type_string.value != "$this")
735 {
736 return Err(ParseError::InvalidTypeTag(span, format!("Invalid type definition: '{}'", type_string.value)));
737 }
738
739 Ok(TypeTag { span, name: name.to_owned(), type_string })
740}
741
742pub fn parse_import_type_tag(content: &str, span: Span) -> Result<ImportTypeTag, ParseError> {
753 let (name, rest) = content.trim_start().split_once(' ').ok_or_else(|| {
754 ParseError::InvalidImportTypeTag(span, "Expected type alias name and 'from' clause".to_string())
755 })?;
756 let name = name.trim();
757 let rest = rest.trim();
758
759 if !is_valid_identifier_start(name, false) {
760 return Err(ParseError::InvalidImportTypeTag(span, format!("Invalid type alias name: '{name}'")));
761 }
762
763 if rest.is_empty() {
764 return Err(ParseError::InvalidImportTypeTag(span, "Missing 'from' clause".to_string()));
765 }
766
767 let (from, rest) = rest.split_once(' ').ok_or_else(|| {
768 ParseError::InvalidImportTypeTag(span, "Expected 'from' keyword followed by class name".to_string())
769 })?;
770
771 if !from.eq_ignore_ascii_case("from") {
772 return Err(ParseError::InvalidImportTypeTag(span, format!("Expected 'from' keyword, found '{from}'")));
773 }
774
775 if rest.is_empty() {
776 return Err(ParseError::InvalidImportTypeTag(span, "Missing class name after 'from'".to_string()));
777 }
778
779 let (imported_from, rest) = if let Some((imp_from, rest)) = rest.split_once(' ') {
780 (imp_from.trim(), rest.trim())
781 } else {
782 (rest.trim(), "")
783 };
784
785 if !is_valid_identifier_start(imported_from, true) {
786 return Err(ParseError::InvalidImportTypeTag(span, format!("Invalid class name: '{imported_from}'")));
787 }
788
789 let mut alias = None;
790
791 if let Some((r#as, rest)) = rest.split_once(' ')
792 && r#as.trim().eq_ignore_ascii_case("as")
793 && !rest.is_empty()
794 {
795 let alias_name = rest
796 .split_whitespace()
797 .next()
798 .ok_or_else(|| ParseError::InvalidImportTypeTag(span, "Expected alias name after 'as'".to_string()))?
799 .trim()
800 .to_owned();
801 alias = Some(alias_name);
802 }
803
804 Ok(ImportTypeTag { span, name: name.to_owned(), from: imported_from.to_owned(), alias })
805}
806
807pub fn parse_property_tag(content: &str, span: Span, is_read: bool, is_write: bool) -> Result<PropertyTag, ParseError> {
813 let (type_string, variable) = if content.trim_start().starts_with('$') && !content.trim_start().starts_with("$this")
815 {
816 let var_part = content
817 .split_whitespace()
818 .next()
819 .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Expected variable name".to_string()))?;
820 let variable = parse_var_ident(var_part, false)
821 .ok_or_else(|| ParseError::InvalidPropertyTag(span, format!("Invalid variable name: '{var_part}'")))?;
822
823 (None, variable)
824 } else {
825 let (type_string, rest_slice) = split_tag_content(content, span)
826 .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Failed to parse type definition".to_string()))?;
827
828 if type_string.value.is_empty()
830 || type_string.value.starts_with('{')
831 || (type_string.value.starts_with('$') && type_string.value != "$this")
832 {
833 return Err(ParseError::InvalidPropertyTag(
834 span,
835 format!("Invalid type definition: '{}'", type_string.value),
836 ));
837 }
838
839 if rest_slice.is_empty() {
840 return Err(ParseError::InvalidPropertyTag(span, "Missing variable name after type".to_string()));
841 }
842
843 let var_part = rest_slice
844 .split_whitespace()
845 .next()
846 .ok_or_else(|| ParseError::InvalidPropertyTag(span, "Expected variable name".to_string()))?;
847 let variable = parse_var_ident(var_part, false)
848 .ok_or_else(|| ParseError::InvalidPropertyTag(span, format!("Invalid variable name: '{var_part}'")))?;
849
850 (Some(type_string), variable)
851 };
852
853 Ok(PropertyTag { span, type_string, variable, is_read, is_write })
854}
855
856#[inline]
863#[must_use]
864pub fn split_tag_content(content: &str, input_span: Span) -> Option<(TypeString, &str)> {
865 let trim_start_offset = content.find(|c: char| !c.is_whitespace()).unwrap_or(0);
867 let trimmed_start_pos = input_span.start.forward(trim_start_offset as u32);
869
870 let trimmed_content = content.trim();
872 if trimmed_content.is_empty() {
873 return None;
874 }
875
876 let mut bracket_stack: Vec<char> = Vec::with_capacity(8);
877 let mut quote_char: Option<char> = None;
878 let mut escaped = false;
879 let mut last_char_was_significant = false;
880 let mut split_point_rel: Option<usize> = None;
882
883 let mut iter = trimmed_content.char_indices().peekable();
884
885 while let Some((i, char)) = iter.next() {
886 if let Some(q) = quote_char {
887 if char == q && !escaped {
888 quote_char = None;
889 } else {
890 escaped = char == '\\' && !escaped;
891 }
892 last_char_was_significant = true;
893 continue;
894 }
895 if char == '\'' || char == '"' {
896 quote_char = Some(char);
897 last_char_was_significant = true;
898 continue;
899 }
900 match char {
901 '<' | '(' | '[' | '{' => bracket_stack.push(char),
902 '>' | ')' | ']' | '}' => {
903 match bracket_stack.pop() {
904 Some(opening) if brackets_match(opening, char) => {}
905 _ => return None, }
907 }
908 _ => {}
909 }
910
911 if char == ':' || char == '|' || char == '&' {
916 last_char_was_significant = true;
917 while let Some(&(_, next_char)) = iter.peek() {
918 if next_char.is_whitespace() {
919 iter.next();
920 } else {
921 break;
922 }
923 }
924
925 continue;
926 }
927
928 if char == '/' && iter.peek().is_some_and(|&(_, c)| c == '/') {
929 if !bracket_stack.is_empty() {
930 while let Some(&(_, next_char)) = iter.peek() {
931 if next_char == '\n' {
932 break;
933 }
934
935 iter.next();
936 }
937 last_char_was_significant = true;
938 continue;
939 }
940
941 split_point_rel = Some(i);
943
944 break;
946 }
947
948 if char.is_whitespace() {
949 if bracket_stack.is_empty() && last_char_was_significant {
950 let mut temp_iter = iter.clone();
951 let mut found_continuation = false;
952
953 while let Some(&(_, next_char)) = temp_iter.peek() {
954 if next_char.is_whitespace() {
955 temp_iter.next();
956 } else {
957 found_continuation = next_char == ':' || next_char == '|' || next_char == '&';
958 break;
959 }
960 }
961
962 if found_continuation {
963 while let Some(&(_, next_char)) = iter.peek() {
964 if next_char.is_whitespace() {
965 iter.next();
966 } else {
967 break;
968 }
969 }
970
971 last_char_was_significant = true;
972 } else {
973 split_point_rel = Some(i);
974 break;
975 }
976 } else {
977 last_char_was_significant = false;
978 }
979 } else if char == '.' {
980 let prev_is_digit = i > 0 && trimmed_content.as_bytes()[i - 1].is_ascii_digit();
983 let next_is_digit = iter.peek().is_some_and(|&(_, c)| c.is_ascii_digit());
984
985 if prev_is_digit && next_is_digit {
986 last_char_was_significant = true;
988 } else {
989 if bracket_stack.is_empty() && last_char_was_significant {
991 split_point_rel = Some(i);
992 break;
993 }
994 last_char_was_significant = false;
995 }
996 } else {
997 last_char_was_significant = true;
998 }
999 }
1000
1001 if !bracket_stack.is_empty() || quote_char.is_some() {
1003 return None;
1004 }
1005
1006 if let Some(split_idx_rel) = split_point_rel {
1007 let type_part_slice = trimmed_content[..split_idx_rel].trim_end();
1009 let rest_part_slice = trimmed_content[split_idx_rel..].trim_start();
1010
1011 let type_span =
1013 Span::new(input_span.file_id, trimmed_start_pos, trimmed_start_pos.forward(type_part_slice.len() as u32));
1014
1015 Some((TypeString { value: type_part_slice.to_owned(), span: type_span }, rest_part_slice))
1016 } else {
1017 let type_part_slice = trimmed_content;
1019 let type_span =
1020 Span::new(input_span.file_id, trimmed_start_pos, trimmed_start_pos.forward(type_part_slice.len() as u32));
1021
1022 Some((TypeString { value: type_part_slice.to_owned(), span: type_span }, ""))
1023 }
1024}
1025
1026pub fn parse_method_tag(mut content: &str, mut span: Span) -> Result<MethodTag, ParseError> {
1037 let (trimmed_content, leading_ws) = consume_whitespace(content);
1038 content = trimmed_content;
1039 span = span.subspan(leading_ws as u32, span.length());
1040
1041 let mut is_static = false;
1042 let mut visibility = None;
1043
1044 let mut acc_len = 0;
1045
1046 let mut static_modifier_start = 0u32;
1048 let mut static_modifier_len = 0u32;
1049
1050 loop {
1051 if let Some((new_content, char_count)) = try_consume(content, "static ") {
1052 if is_static {
1053 return Err(ParseError::InvalidMethodTag(span, "Duplicate 'static' modifier".to_string()));
1054 }
1055
1056 is_static = true;
1057 static_modifier_start = acc_len as u32;
1058 static_modifier_len = 6; acc_len += char_count;
1060 content = new_content;
1061 } else if let Some((new_content, char_count)) = try_consume(content, "public ") {
1062 if visibility.is_some() {
1063 return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1064 }
1065
1066 visibility = Some(Visibility::Public);
1067 acc_len += char_count;
1068 content = new_content;
1069 } else if let Some((new_content, char_count)) = try_consume(content, "protected ") {
1070 if visibility.is_some() {
1071 return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1072 }
1073
1074 visibility = Some(Visibility::Protected);
1075 acc_len += char_count;
1076 content = new_content;
1077 } else if let Some((new_content, char_count)) = try_consume(content, "private ") {
1078 if visibility.is_some() {
1079 return Err(ParseError::InvalidMethodTag(span, "Duplicate visibility modifier".to_string()));
1080 }
1081
1082 visibility = Some(Visibility::Private);
1083 acc_len += char_count;
1084 content = new_content;
1085 } else {
1086 break;
1087 }
1088 }
1089
1090 let rest_span = span.subspan(acc_len as u32, span.length());
1091
1092 let (type_string, rest_slice, rest_slice_span) = if is_static && looks_like_method_signature_only(content) {
1093 is_static = false;
1094 let static_span = span.subspan(static_modifier_start, static_modifier_start + static_modifier_len);
1095 let type_string = TypeString { value: "static".into(), span: static_span };
1096 let (rest_slice, whitespace_count) = consume_whitespace(content);
1097 let rest_slice_span = rest_span.subspan(whitespace_count as u32, rest_span.length());
1098 (type_string, rest_slice, rest_slice_span)
1099 } else {
1100 let type_string = split_tag_content(content, rest_span)
1101 .ok_or_else(|| ParseError::InvalidMethodTag(span, "Failed to parse return type".to_string()))?
1102 .0;
1103 let (rest_slice, whitespace_count) = consume_whitespace(&content[type_string.span.length() as usize..]);
1104 let rest_slice_span =
1105 rest_span.subspan(type_string.span.length() + whitespace_count as u32, rest_span.length());
1106 (type_string, rest_slice, rest_slice_span)
1107 };
1108
1109 if type_string.value.is_empty()
1111 || type_string.value.starts_with('{')
1112 || (type_string.value.starts_with('$') && type_string.value != "$this")
1113 {
1114 return Err(ParseError::InvalidMethodTag(span, format!("Invalid return type: '{}'", type_string.value)));
1115 }
1116
1117 if rest_slice.is_empty() {
1118 return Err(ParseError::InvalidMethodTag(span, "Missing method signature".to_string()));
1120 }
1121
1122 let mut chars = rest_slice.char_indices().peekable();
1123
1124 let mut name_end = None;
1125
1126 for (i, ch) in &mut chars {
1127 if ch == '(' {
1128 name_end = Some(i);
1129 break;
1130 }
1131 }
1132
1133 let name_end = name_end.ok_or_else(|| {
1134 ParseError::InvalidMethodTag(span, "Missing opening parenthesis '(' for method arguments".to_string())
1135 })?;
1136
1137 let name = rest_slice[..name_end].trim();
1138
1139 if name.is_empty() {
1140 return Err(ParseError::InvalidMethodTag(span, "Missing method name".to_string()));
1141 }
1142
1143 let mut depth = 1;
1144 let mut args_end = None;
1145
1146 for (i, ch) in &mut chars {
1147 match ch {
1148 '(' => depth += 1,
1149 ')' => {
1150 depth -= 1;
1151 if depth == 0 {
1152 args_end = Some(i);
1153 break;
1154 }
1155 }
1156 _ => {}
1157 }
1158 }
1159
1160 let args_end = args_end.ok_or_else(|| {
1161 ParseError::InvalidMethodTag(span, "Missing closing parenthesis ')' for method arguments".to_string())
1162 })?;
1163 let (args_str, whitespace_count) = consume_whitespace(&rest_slice[name_end + 1..args_end]);
1164 let args_span = rest_slice_span.subspan((whitespace_count + name_end) as u32 + 1, args_end as u32);
1165
1166 let description = rest_slice[args_end..].trim();
1167 let arguments_split = split_args(args_str, args_span);
1168 let arguments = arguments_split.iter().filter_map(|(arg, span)| parse_argument(arg, span)).collect::<Vec<_>>();
1169
1170 let method = Method {
1171 name: name.into(),
1172 argument_list: arguments,
1173 visibility: visibility.unwrap_or(Visibility::Public),
1174 is_static,
1175 };
1176
1177 Ok(MethodTag { span, type_string, method, description: description.into() })
1178}
1179
1180fn consume_whitespace(input: &str) -> (&str, usize) {
1181 let mut iter = input.chars().peekable();
1182 let mut count = 0;
1183
1184 while let Some(ch) = iter.peek() {
1185 if ch.is_whitespace() {
1186 iter.next();
1187 count += 1;
1188 } else {
1189 break;
1190 }
1191 }
1192
1193 (&input[count..], count)
1194}
1195
1196fn try_consume<'a>(input: &'a str, token: &str) -> Option<(&'a str, 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[len..];
1205
1206 let (input, whitespace_count) = consume_whitespace(input);
1207
1208 Some((input, len + whitespace_count))
1209}
1210
1211fn looks_like_method_signature_only(content: &str) -> bool {
1215 let trimmed = content.trim();
1216 if let Some(paren_pos) = trimmed.find('(') {
1217 let before_paren = trimmed[..paren_pos].trim();
1218 !before_paren.is_empty() && !before_paren.contains(' ')
1219 } else {
1220 false
1221 }
1222}
1223
1224fn split_args(args_str: &str, span: Span) -> Vec<(&str, Span)> {
1225 let mut args = Vec::new();
1226
1227 let mut start = 0;
1228 let mut depth = 0;
1229 for (i, ch) in args_str.char_indices() {
1230 match ch {
1231 '(' | '[' => depth += 1,
1232 ')' | ']' => depth -= 1,
1233 ',' if depth == 0 => {
1234 let (arg, whitespace_count) = consume_whitespace(&args_str[start..i]);
1235 if !arg.is_empty() {
1236 args.push((arg, span.subspan((whitespace_count + start) as u32, i as u32)));
1237 }
1238 start = i + 1;
1239 }
1240 _ => {}
1241 }
1242 }
1243
1244 if start < args_str.len() {
1245 let (arg, whitespace_count) = consume_whitespace(&args_str[start..]);
1246 let arg_trimmed = arg.trim_end();
1247 if !arg.is_empty() {
1248 args.push((
1249 arg_trimmed,
1250 span.subspan(
1251 (whitespace_count + start) as u32,
1252 (args_str.len() - arg.len() + arg_trimmed.len()) as u32,
1253 ),
1254 ));
1255 }
1256 }
1257
1258 args
1259}
1260
1261fn parse_argument(arg_str: &str, span: &Span) -> Option<Argument> {
1262 let default_value_split = arg_str.rsplit_once('=');
1263
1264 let ((arg_type, raw_name), default_value): ((_, _), Option<&str>) =
1265 if let Some((variable_definition, default_value)) = default_value_split {
1266 let arg = variable_definition.trim();
1267 if let Some((arg_type, raw_name)) = arg.rsplit_once(' ') {
1268 ((Some(arg_type), raw_name), Some(default_value.trim()))
1269 } else {
1270 ((None, arg), Some(default_value))
1271 }
1272 } else {
1273 let arg = arg_str.trim();
1274 if let Some((arg_type, raw_name)) = arg.rsplit_once(' ') {
1275 ((Some(arg_type), raw_name), None)
1276 } else {
1277 ((None, arg), None)
1278 }
1279 };
1280
1281 let type_string =
1282 arg_type.map(|arg_type| TypeString { value: arg_type.into(), span: span.subspan(0, arg_type.len() as u32) });
1283
1284 let variable_span = span.subspan(arg_type.map_or(0, |t| 1 + t.len() as u32), span.length());
1285
1286 let variable = parse_var_ident(raw_name, false)?;
1287
1288 Some(Argument {
1289 type_hint: type_string,
1290 variable,
1291 has_default: default_value.is_some(),
1292 argument_span: *span,
1293 variable_span,
1294 })
1295}
1296
1297#[inline]
1299const fn brackets_match(open: char, close: char) -> bool {
1300 matches!((open, close), ('<', '>') | ('(', ')') | ('[', ']') | ('{', '}'))
1301}
1302
1303#[inline]
1305fn is_valid_identifier_start(mut identifier: &str, allow_qualified: bool) -> bool {
1306 if allow_qualified && identifier.starts_with('\\') {
1307 identifier = &identifier[1..];
1308 }
1309
1310 !identifier.is_empty()
1311 && identifier.chars().all(|c| c.is_alphanumeric() || c == '_' || (allow_qualified && c == '\\'))
1312 && identifier.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_')
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317 use mago_database::file::FileId;
1318 use mago_span::Position;
1319 use mago_span::Span;
1320
1321 use super::*;
1322
1323 fn test_span(input: &str, start_offset: u32) -> Span {
1324 let base_start = Position::new(start_offset);
1325 Span::new(FileId::zero(), base_start, base_start.forward(input.len() as u32))
1326 }
1327
1328 fn test_span_for(s: &str) -> Span {
1329 test_span(s, 0)
1330 }
1331
1332 fn make_span(start: u32, end: u32) -> Span {
1333 Span::new(FileId::zero(), Position::new(start), Position::new(end))
1334 }
1335
1336 #[test]
1337 fn test_parse_var_ident() {
1338 struct Expect<'a> {
1339 s: &'a str,
1340 variadic: bool,
1341 by_ref: bool,
1342 }
1343 let cases: &[(&str, Option<Expect>)] = &[
1344 ("$x", Some(Expect { s: "$x", variadic: false, by_ref: false })),
1345 ("&$refVar", Some(Expect { s: "$refVar", variadic: false, by_ref: true })),
1346 ("$foo,", Some(Expect { s: "$foo", variadic: false, by_ref: false })),
1347 ("...$ids)", Some(Expect { s: "$ids", variadic: true, by_ref: false })),
1348 ("...$items,", Some(Expect { s: "$items", variadic: true, by_ref: false })),
1349 ("$", None),
1350 ("...$", None),
1351 ("$1x", None),
1352 ("foo", None),
1353 ];
1354
1355 for (input, expected) in cases {
1356 let got = parse_var_ident(input, false);
1357 match (got, expected) {
1358 (None, None) => {}
1359 (Some(v), Some(e)) => {
1360 assert_eq!(v.name, e.s, "input={input}");
1361 assert_eq!(v.is_variadic, e.variadic, "input={input}");
1362 assert_eq!(v.is_by_reference, e.by_ref, "input={input}");
1363 }
1364 _ => panic!("mismatch for input={input}"),
1365 }
1366 }
1367 }
1368
1369 #[test]
1370 fn test_variable_display_and_raw() {
1371 let cases = vec![("$x", "$x"), ("&$x", "&$x"), ("...$x", "...$x"), ("...$x)", "...$x"), ("...$x,", "...$x")];
1372
1373 for (input, expected_raw) in cases {
1374 let v = parse_var_ident(input, false).expect("should parse variable");
1375 assert_eq!(v.to_string(), expected_raw);
1376 }
1377 }
1378
1379 #[test]
1380 fn test_splitter_brackets() {
1381 let input = "array<int, (string|bool)> desc";
1382 let span = test_span_for(input);
1383 let (ts, rest) = split_tag_content(input, span).unwrap();
1384 assert_eq!(ts.value, "array<int, (string|bool)>");
1385 assert_eq!(ts.span, make_span(0, "array<int, (string|bool)>".len() as u32));
1386 assert_eq!(rest, "desc");
1387
1388 let input = "array<int, string> desc";
1389 let span = test_span_for(input);
1390 let (ts, rest) = split_tag_content(input, span).unwrap();
1391 assert_eq!(ts.value, "array<int, string>");
1392 assert_eq!(ts.span, make_span(0, "array<int, string>".len() as u32));
1393 assert_eq!(rest, "desc");
1394
1395 assert!(split_tag_content("array<int", test_span_for("array<int")).is_none()); assert!(split_tag_content("array<int)", test_span_for("array<int)")).is_none()); assert!(split_tag_content("array(int>", test_span_for("array(int>")).is_none()); assert!(split_tag_content("string>", test_span_for("string>")).is_none()); }
1400
1401 #[test]
1402 fn test_splitter_quotes() {
1403 let input = " 'inside quote' outside ";
1404 let span = test_span_for(input);
1405 let (ts, rest) = split_tag_content(input, span).unwrap();
1406 assert_eq!(ts.value, "'inside quote'");
1407 assert_eq!(ts.span, make_span(1, "'inside quote'".len() as u32 + 1));
1408 assert_eq!(rest, "outside");
1409
1410 let input = r#""string \" with escape" $var"#;
1411 let span = test_span_for(input);
1412 let (ts, rest) = split_tag_content(input, span).unwrap();
1413 assert_eq!(ts.value, r#""string \" with escape""#);
1414 assert_eq!(ts.span, make_span(0, r#""string \" with escape""#.len() as u32));
1415 assert_eq!(rest, "$var");
1416
1417 assert!(split_tag_content("\"unterminated", test_span_for("\"unterminated")).is_none());
1418 }
1419
1420 #[test]
1421 fn test_splitter_comments() {
1422 let input = "(string // comment \n | int) $var";
1423 let span = test_span_for(input);
1424 let (ts, rest) = split_tag_content(input, span).unwrap();
1425 assert_eq!(ts.value, "(string // comment \n | int)");
1426 assert_eq!(ts.span, make_span(0, "(string // comment \n | int)".len() as u32));
1427 assert_eq!(rest, "$var");
1428
1429 let input = "string // comment goes to end";
1430 let span = test_span_for(input);
1431 let (ts, rest) = split_tag_content(input, span).unwrap();
1432 assert_eq!(ts.value, "string");
1433 assert_eq!(ts.span, make_span(0, "string".len() as u32));
1434 assert_eq!(rest, "// comment goes to end");
1435
1436 let input = "array<string // comment\n> $var";
1437 let span = test_span_for(input);
1438 let (ts, rest) = split_tag_content(input, span).unwrap();
1439 assert_eq!(ts.value, "array<string // comment\n>");
1440 assert_eq!(ts.span, make_span(0, "array<string // comment\n>".len() as u32));
1441 assert_eq!(rest, "$var");
1442 }
1443
1444 #[test]
1445 fn test_splitter_whole_string_is_type() {
1446 let input = " array<int, string> ";
1447 let span = test_span_for(input);
1448 let (ts, rest) = split_tag_content(input, span).unwrap();
1449 assert_eq!(ts.value, "array<int, string>");
1450 assert_eq!(ts.span, make_span(1, "array<int, string>".len() as u32 + 1));
1451 assert_eq!(rest, ""); }
1453
1454 #[test]
1455 fn test_splitter_with_dot() {
1456 let input = "string[]. something";
1457 let span = test_span_for(input);
1458 let (ts, rest) = split_tag_content(input, span).unwrap();
1459 assert_eq!(ts.value, "string[]");
1460 assert_eq!(ts.span, make_span(0, "string[]".len() as u32));
1461 assert_eq!(rest, ". something");
1462 }
1463
1464 #[test]
1465 fn test_param_basic() {
1466 let offset = 10;
1467 let content = " string|int $myVar Description here ";
1468 let span = test_span(content, offset);
1469 let result = parse_param_tag(content, span).unwrap();
1470
1471 assert_eq!(result.type_string.as_ref().unwrap().value, "string|int"); assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1); assert_eq!(result.type_string.as_ref().unwrap().span.end.offset, offset + 1 + "string|int".len() as u32);
1474 assert_eq!(result.variable.name, "$myVar");
1475 assert_eq!(result.description, "Description here");
1476 assert_eq!(result.span, span); }
1478
1479 #[test]
1480 fn test_param_complex_type_no_desc() {
1481 let offset = 5;
1482 let content = " array<int, string> $param ";
1483 let span = test_span(content, offset);
1484 let result = parse_param_tag(content, span).unwrap();
1485 assert_eq!(result.type_string.as_ref().unwrap().value, "array<int, string>"); assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1);
1487 assert_eq!(
1488 result.type_string.as_ref().unwrap().span.end.offset,
1489 offset + 1 + "array<int, string>".len() as u32
1490 );
1491 assert_eq!(result.variable.name, "$param");
1492 assert_eq!(result.description, "");
1493 }
1494
1495 #[test]
1496 fn test_param_type_with_comment() {
1497 let offset = 20;
1498 let content = " (string // comment \n | int) $var desc";
1499 let span = test_span(content, offset);
1500 let result = parse_param_tag(content, span).unwrap();
1501 assert_eq!(result.type_string.as_ref().unwrap().value, "(string // comment \n | int)");
1502 assert_eq!(result.type_string.as_ref().unwrap().span.start.offset, offset + 1);
1503 assert_eq!(
1504 result.type_string.as_ref().unwrap().span.end.offset,
1505 offset + 1 + "(string // comment \n | int)".len() as u32
1506 );
1507 assert_eq!(result.variable.name, "$var");
1508 assert_eq!(result.description, "desc");
1509 }
1510
1511 #[test]
1512 fn test_param_no_type() {
1513 let content = " $param Description here ";
1514 let span = test_span(content, 0);
1515 let result = parse_param_tag(content, span).unwrap();
1516 assert!(result.type_string.is_none()); assert_eq!(result.variable.name, "$param");
1518 assert_eq!(result.description, "Description here");
1519 }
1520
1521 #[test]
1522 fn test_return_basic() {
1523 let offset = 10u32;
1524 let content = " string Description here ";
1525 let span = test_span(content, offset);
1526 let result = parse_return_tag(content, span).unwrap();
1527 assert_eq!(result.type_string.value, "string");
1528 assert_eq!(result.type_string.span.start.offset, offset + 1);
1529 assert_eq!(result.type_string.span.end.offset, offset + 1 + "string".len() as u32);
1530 assert_eq!(result.description, "Description here");
1531 assert_eq!(result.span, span);
1532 }
1533
1534 #[test]
1535 fn test_return_complex_type_with_desc() {
1536 let offset = 0;
1537 let content = " array<int, (string|null)> Description ";
1538 let span = test_span(content, offset);
1539 let result = parse_return_tag(content, span).unwrap();
1540 assert_eq!(result.type_string.value, "array<int, (string|null)>");
1541 assert_eq!(result.type_string.span.start.offset, offset + 1);
1542 assert_eq!(result.type_string.span.end.offset, offset + 1 + "array<int, (string|null)>".len() as u32);
1543 assert_eq!(result.description, "Description");
1544 }
1545
1546 #[test]
1547 fn test_return_complex_type_no_desc() {
1548 let offset = 0;
1549 let content = " array<int, (string|null)> ";
1550 let span = test_span(content, offset);
1551 let result = parse_return_tag(content, span).unwrap();
1552 assert_eq!(result.type_string.value, "array<int, (string|null)>");
1553 assert_eq!(result.type_string.span.start.offset, offset + 1);
1554 assert_eq!(result.type_string.span.end.offset, offset + 1 + "array<int, (string|null)>".len() as u32);
1555 assert_eq!(result.description, "");
1556 }
1557
1558 #[test]
1559 fn test_param_out_no_type() {
1560 let content = " $myVar ";
1561 let span = test_span(content, 0);
1562 assert!(parse_param_out_tag(content, span).is_err());
1563 }
1564
1565 #[test]
1566 fn test_param_out_no_var() {
1567 let content = " string ";
1568 let span = test_span(content, 0);
1569 assert!(parse_param_out_tag(content, span).is_err());
1570 }
1571
1572 #[test]
1573 fn test_type() {
1574 let content = "MyType = string";
1575 let span = test_span_for(content);
1576 let result = parse_type_tag(content, span).unwrap();
1577 assert_eq!(result.name, "MyType");
1578 assert_eq!(result.type_string.value, "string");
1579 assert_eq!(result.type_string.span.start.offset, 9);
1580 assert_eq!(result.type_string.span.end.offset, 9 + "string".len() as u32);
1581 assert_eq!(result.span, span);
1582 }
1583
1584 #[test]
1585 fn test_import_type() {
1586 let content = "MyType from \\My\\Namespace\\Class as Alias";
1587 let span = test_span_for(content);
1588 let result = parse_import_type_tag(content, span).unwrap();
1589 assert_eq!(result.name, "MyType");
1590 assert_eq!(result.from, "\\My\\Namespace\\Class");
1591 assert_eq!(result.alias, Some("Alias".to_owned()));
1592 assert_eq!(result.span, span);
1593 }
1594
1595 #[test]
1596 fn test_param_trailing_comma_is_ignored_in_name() {
1597 let content = " string $foo, desc";
1598 let span = test_span_for(content);
1599 let result = parse_param_tag(content, span).unwrap();
1600 assert_eq!(result.variable.name, "$foo");
1601 assert_eq!(result.description, ", desc");
1602 }
1603
1604 #[test]
1605 fn test_param_variadic_trailing_paren_is_ignored_in_name() {
1606 let content = " list<int> ...$items) rest";
1607 let span = test_span_for(content);
1608 let result = parse_param_tag(content, span).unwrap();
1609 assert_eq!(result.variable.name, "$items");
1610 assert_eq!(result.description, ") rest");
1611 }
1612
1613 #[test]
1614 fn test_param_out_trailing_comma() {
1615 let content = " int $out,";
1616 let span = test_span_for(content);
1617 let result = parse_param_out_tag(content, span).unwrap();
1618 assert_eq!(result.variable.name, "$out");
1619 }
1620
1621 #[test]
1622 fn test_assertion_trailing_comma() {
1623 let content = " int $x,";
1624 let span = test_span_for(content);
1625 let result = parse_assertion_tag(content, span).unwrap();
1626 assert_eq!(result.variable.name, "$x");
1627 }
1628
1629 #[test]
1630 fn test_param_trailing_without_space() {
1631 let content = " string $foo,desc";
1632 let span = test_span_for(content);
1633 let result = parse_param_tag(content, span).unwrap();
1634 assert_eq!(result.variable.name, "$foo");
1635 assert_eq!(result.description, ",desc");
1636 }
1637
1638 #[test]
1639 fn test_param_variadic_trailing_paren_without_space() {
1640 let content = " list<int> ...$items)more";
1641 let span = test_span_for(content);
1642 let result = parse_param_tag(content, span).unwrap();
1643 assert_eq!(result.variable.name, "$items");
1644 assert_eq!(result.description, ")more");
1645 }
1646
1647 #[test]
1648 fn test_param_with_numeric_literals_in_union() {
1649 let content = "-1|-24.0|string $a";
1650 let span = test_span_for(content);
1651 let result = parse_param_tag(content, span).unwrap();
1652 assert_eq!(result.type_string.as_ref().unwrap().value, "-1|-24.0|string");
1653 assert_eq!(result.variable.name, "$a");
1654 assert_eq!(result.description, "");
1655 }
1656
1657 #[test]
1658 fn test_param_with_float_literals() {
1659 let content = "1.5|2.0|3.14 $value";
1660 let span = test_span_for(content);
1661 let result = parse_param_tag(content, span).unwrap();
1662 assert_eq!(result.type_string.as_ref().unwrap().value, "1.5|2.0|3.14");
1663 assert_eq!(result.variable.name, "$value");
1664 }
1665
1666 #[test]
1667 fn test_splitter_with_dot_still_works_as_separator() {
1668 let input = "string[]. something else";
1670 let span = test_span_for(input);
1671 let (ts, rest) = split_tag_content(input, span).unwrap();
1672 assert_eq!(ts.value, "string[]");
1673 assert_eq!(rest, ". something else");
1674 }
1675
1676 #[test]
1677 fn test_splitter_with_colon_after_whitespace() {
1678 let input = "callable(string) : string $callback";
1679 let span = test_span_for(input);
1680 let (ts, rest) = split_tag_content(input, span).unwrap();
1681 assert_eq!(ts.value, "callable(string) : string");
1682 assert_eq!(rest, "$callback");
1683
1684 let input2 = "callable(string) : string $callback";
1685 let span2 = test_span_for(input2);
1686 let (ts2, rest2) = split_tag_content(input2, span2).unwrap();
1687 assert_eq!(ts2.value, "callable(string) : string");
1688 assert_eq!(rest2, "$callback");
1689
1690 let input3 = "callable(string): string $callback";
1691 let span3 = test_span_for(input3);
1692 let (ts3, rest3) = split_tag_content(input3, span3).unwrap();
1693 assert_eq!(ts3.value, "callable(string): string");
1694 assert_eq!(rest3, "$callback");
1695 }
1696}