1use std::sync::LazyLock;
33
34use regex::Regex;
35
36use super::{DocStandardParser, ParsedDocumentation};
37use crate::annotate::{AnnotationType, Suggestion, SuggestionSource};
38
39static JSDOC_TAG: LazyLock<Regex> = LazyLock::new(|| {
41 Regex::new(r"^@(\w+)(?:\s+\{([^}]+)\})?\s*(.*)").expect("Invalid JSDoc tag regex")
42});
43
44static JSDOC_LINK: LazyLock<Regex> =
46 LazyLock::new(|| Regex::new(r"\{@link\s+([^}]+)\}").expect("Invalid JSDoc link regex"));
47
48static INHERIT_DOC: LazyLock<Regex> = LazyLock::new(|| {
50 Regex::new(r"\{@inheritDoc(?:\s+([^}]+))?\}").expect("Invalid inheritDoc regex")
51});
52
53#[derive(Debug, Clone, Default)]
55pub struct TsDocExtensions {
56 pub is_alpha: bool,
58
59 pub is_beta: bool,
61
62 pub is_package_doc: bool,
64
65 pub remarks: Option<String>,
67
68 pub private_remarks: Option<String>,
70
71 pub default_values: Vec<(String, String)>,
73
74 pub type_params: Vec<(String, Option<String>)>,
76
77 pub is_override: bool,
79
80 pub is_virtual: bool,
82
83 pub is_sealed: bool,
85
86 pub inherit_doc: Option<String>,
88
89 pub is_event_property: bool,
91}
92
93pub struct JsDocParser {
96 parse_tsdoc: bool,
98}
99
100impl JsDocParser {
101 pub fn new() -> Self {
103 Self { parse_tsdoc: false }
104 }
105
106 pub fn with_tsdoc() -> Self {
108 Self { parse_tsdoc: true }
109 }
110
111 fn extract_inline_links(&self, text: &str) -> Vec<String> {
113 JSDOC_LINK
114 .captures_iter(text)
115 .filter_map(|caps| caps.get(1).map(|m| m.as_str().to_string()))
116 .collect()
117 }
118
119 fn extract_inherit_doc(&self, text: &str) -> Option<String> {
121 INHERIT_DOC
122 .captures(text)
123 .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
124 }
125
126 fn is_continuation_line(line: &str) -> bool {
128 !line.is_empty() && !line.starts_with('@')
129 }
130}
131
132impl Default for JsDocParser {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138pub struct TsDocParser {
141 pub extensions: TsDocExtensions,
143}
144
145impl TsDocParser {
146 pub fn new() -> Self {
148 Self {
149 extensions: TsDocExtensions::default(),
150 }
151 }
152
153 pub fn extensions(&self) -> &TsDocExtensions {
155 &self.extensions
156 }
157}
158
159impl Default for TsDocParser {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl DocStandardParser for JsDocParser {
166 fn parse(&self, raw_comment: &str) -> ParsedDocumentation {
167 let mut doc = ParsedDocumentation::new();
168 let mut description_lines = Vec::new();
169 let mut in_description = true;
170 let mut current_example = String::new();
171 let mut in_example = false;
172
173 let mut current_tag: Option<String> = None;
175 let mut current_tag_content = String::new();
176
177 let mut remarks_content = String::new();
179 let mut in_remarks = false;
180
181 for line in raw_comment.lines() {
182 let trimmed = line.trim();
184
185 if trimmed == "/**" || trimmed == "*/" || trimmed == "/*" {
187 continue;
188 }
189
190 let line = if trimmed.starts_with("/**") && trimmed.ends_with("*/") {
192 trimmed
193 .trim_start_matches("/**")
194 .trim_end_matches("*/")
195 .trim()
196 } else {
197 trimmed.trim_start_matches('*').trim()
199 };
200
201 if line.is_empty() && description_lines.is_empty() && !in_example && !in_remarks {
203 continue;
204 }
205
206 if let Some(caps) = JSDOC_TAG.captures(line) {
208 in_description = false;
209
210 if in_example && !current_example.is_empty() {
212 doc.examples.push(current_example.trim().to_string());
213 current_example = String::new();
214 in_example = false;
215 }
216
217 if in_remarks && !remarks_content.is_empty() {
219 doc.notes.push(remarks_content.trim().to_string());
220 remarks_content = String::new();
221 in_remarks = false;
222 }
223
224 if let Some(tag) = current_tag.take() {
226 self.save_multiline_tag(&mut doc, &tag, ¤t_tag_content);
227 current_tag_content.clear();
228 }
229
230 let tag = caps.get(1).map(|m| m.as_str()).unwrap_or("");
231 let type_info = caps.get(2).map(|m| m.as_str().to_string());
232 let content = caps.get(3).map(|m| m.as_str().trim().to_string());
233
234 match tag {
235 "description" | "desc" => {
236 if let Some(desc) = content {
237 if !desc.is_empty() {
238 doc.description = Some(desc);
239 }
240 }
241 }
242 "summary" => {
243 doc.summary = content;
244 }
245 "deprecated" => {
246 doc.deprecated = content.or(Some("Deprecated".to_string()));
247 }
248 "see" | "link" => {
249 if let Some(ref_target) = content {
250 doc.see_refs.push(ref_target);
251 }
252 }
253 "todo" | "fixme" => {
254 if let Some(msg) = content {
255 doc.todos.push(msg);
256 }
257 }
258 "param" | "arg" | "argument" => {
259 if let Some(rest) = content {
260 let parts: Vec<&str> =
262 rest.splitn(2, |c: char| c.is_whitespace()).collect();
263 let name = parts.first().unwrap_or(&"").to_string();
264 let desc = parts.get(1).map(|s| s.trim().to_string());
265 if !name.is_empty() {
266 doc.params.push((name, type_info, desc));
267 }
268 }
269 }
270 "returns" | "return" => {
271 doc.returns = Some((type_info, content));
272 }
273 "throws" | "exception" | "raise" => {
274 let exc_type =
275 type_info.unwrap_or_else(|| content.clone().unwrap_or_default());
276 if !exc_type.is_empty() {
277 doc.throws.push((exc_type, content));
278 }
279 }
280 "example" => {
281 in_example = true;
282 if let Some(ex) = content {
283 if !ex.is_empty() {
284 current_example.push_str(&ex);
285 current_example.push('\n');
286 }
287 }
288 }
289 "module" | "fileoverview" | "packageDocumentation" => {
290 if let Some(name) = content.clone() {
291 if !name.is_empty() {
292 doc.custom_tags.push(("module".to_string(), name));
293 }
294 }
295 if self.parse_tsdoc && tag == "packageDocumentation" {
297 doc.custom_tags
298 .push(("packageDocumentation".to_string(), "true".to_string()));
299 }
300 }
301 "category" | "group" => {
302 if let Some(cat) = content {
303 doc.custom_tags.push(("category".to_string(), cat));
304 }
305 }
306 "private" => {
307 doc.custom_tags
308 .push(("visibility".to_string(), "private".to_string()));
309 }
310 "internal" => {
311 doc.custom_tags
312 .push(("visibility".to_string(), "internal".to_string()));
313 }
314 "protected" => {
315 doc.custom_tags
316 .push(("visibility".to_string(), "protected".to_string()));
317 }
318 "public" => {
319 doc.custom_tags
320 .push(("visibility".to_string(), "public".to_string()));
321 }
322 "readonly" => {
323 doc.custom_tags
324 .push(("readonly".to_string(), "true".to_string()));
325 }
326 "since" => {
327 doc.since = content;
328 }
329 "author" => {
330 doc.author = content;
331 }
332 "note" | "remark" => {
333 if let Some(note) = content {
334 doc.notes.push(note);
335 }
336 }
337 "warning" | "warn" => {
338 if let Some(warning) = content {
339 doc.notes.push(format!("Warning: {}", warning));
340 }
341 }
342 "alpha" => {
344 doc.custom_tags
345 .push(("stability".to_string(), "alpha".to_string()));
346 }
347 "beta" => {
348 doc.custom_tags
349 .push(("stability".to_string(), "beta".to_string()));
350 }
351 "remarks" => {
352 in_remarks = true;
353 if let Some(r) = content {
354 if !r.is_empty() {
355 remarks_content.push_str(&r);
356 remarks_content.push('\n');
357 }
358 }
359 }
360 "privateRemarks" => {
361 if let Some(r) = content {
363 doc.custom_tags.push(("privateRemarks".to_string(), r));
364 }
365 }
366 "defaultValue" => {
367 if let Some(val) = content {
368 doc.custom_tags.push(("defaultValue".to_string(), val));
369 }
370 }
371 "typeParam" | "typeparam" => {
372 if let Some(rest) = content {
373 let parts: Vec<&str> =
375 rest.splitn(2, |c: char| c.is_whitespace()).collect();
376 let name = parts.first().unwrap_or(&"").to_string();
377 let desc = parts.get(1).map(|s| s.trim().to_string());
378 if !name.is_empty() {
379 doc.custom_tags.push((
380 "typeParam".to_string(),
381 format!("{}: {}", name, desc.unwrap_or_default()),
382 ));
383 }
384 }
385 }
386 "override" => {
387 doc.custom_tags
388 .push(("override".to_string(), "true".to_string()));
389 }
390 "virtual" => {
391 doc.custom_tags
392 .push(("virtual".to_string(), "true".to_string()));
393 }
394 "sealed" => {
395 doc.custom_tags
396 .push(("sealed".to_string(), "true".to_string()));
397 }
398 "eventProperty" => {
399 doc.custom_tags
400 .push(("eventProperty".to_string(), "true".to_string()));
401 }
402 _ => {
403 if let Some(val) = content.clone() {
405 if !val.is_empty() {
406 doc.custom_tags.push((tag.to_string(), val));
407 } else {
408 current_tag = Some(tag.to_string());
410 }
411 } else {
412 doc.custom_tags.push((tag.to_string(), String::new()));
414 }
415 }
416 }
417 } else if in_example {
418 current_example.push_str(line);
420 current_example.push('\n');
421 } else if in_remarks {
422 remarks_content.push_str(line);
424 remarks_content.push('\n');
425 } else if current_tag.is_some() && Self::is_continuation_line(line) {
426 current_tag_content.push_str(line);
428 current_tag_content.push('\n');
429 } else if in_description && !line.is_empty() {
430 description_lines.push(line.to_string());
431 }
432 }
433
434 if in_example && !current_example.is_empty() {
436 doc.examples.push(current_example.trim().to_string());
437 }
438
439 if in_remarks && !remarks_content.is_empty() {
441 doc.notes.push(remarks_content.trim().to_string());
442 }
443
444 if let Some(tag) = current_tag.take() {
446 self.save_multiline_tag(&mut doc, &tag, ¤t_tag_content);
447 }
448
449 if doc.summary.is_none() && !description_lines.is_empty() {
451 doc.summary = Some(description_lines[0].clone());
452 }
453
454 if !description_lines.is_empty() && doc.description.is_none() {
456 doc.description = Some(description_lines.join(" "));
457 }
458
459 if let Some(desc) = &doc.description {
461 let links = self.extract_inline_links(desc);
462 doc.see_refs.extend(links);
463
464 if let Some(inherit) = self.extract_inherit_doc(desc) {
466 doc.custom_tags.push(("inheritDoc".to_string(), inherit));
467 }
468 }
469
470 doc
471 }
472
473 fn standard_name(&self) -> &'static str {
474 if self.parse_tsdoc {
475 "tsdoc"
476 } else {
477 "jsdoc"
478 }
479 }
480
481 fn to_suggestions(
482 &self,
483 parsed: &ParsedDocumentation,
484 target: &str,
485 line: usize,
486 ) -> Vec<Suggestion> {
487 let mut suggestions = Vec::new();
488
489 if let Some(summary) = &parsed.summary {
491 let truncated = super::truncate_summary(summary, 100);
492 suggestions.push(Suggestion::summary(
493 target,
494 line,
495 truncated,
496 SuggestionSource::Converted,
497 ));
498 }
499
500 if let Some(msg) = &parsed.deprecated {
502 suggestions.push(Suggestion::deprecated(
503 target,
504 line,
505 msg,
506 SuggestionSource::Converted,
507 ));
508 }
509
510 for see_ref in &parsed.see_refs {
512 suggestions.push(Suggestion::new(
513 target,
514 line,
515 AnnotationType::Ref,
516 see_ref,
517 SuggestionSource::Converted,
518 ));
519 }
520
521 for todo in &parsed.todos {
523 suggestions.push(Suggestion::new(
524 target,
525 line,
526 AnnotationType::Hack,
527 format!("reason=\"{}\"", todo),
528 SuggestionSource::Converted,
529 ));
530 }
531
532 if let Some(visibility) = parsed.get_visibility() {
534 let lock_level = match visibility {
535 "private" | "internal" => "restricted",
536 "protected" => "normal",
537 _ => "normal",
538 };
539 suggestions.push(Suggestion::lock(
540 target,
541 line,
542 lock_level,
543 SuggestionSource::Converted,
544 ));
545 }
546
547 if let Some(module_name) = parsed.get_module() {
549 suggestions.push(Suggestion::new(
550 target,
551 line,
552 AnnotationType::Module,
553 module_name,
554 SuggestionSource::Converted,
555 ));
556 }
557
558 if let Some(category) = parsed.get_category() {
560 suggestions.push(Suggestion::domain(
561 target,
562 line,
563 category.to_lowercase(),
564 SuggestionSource::Converted,
565 ));
566 }
567
568 if !parsed.throws.is_empty() {
570 let throws_list: Vec<String> = parsed.throws.iter().map(|(t, _)| t.clone()).collect();
571 suggestions.push(Suggestion::ai_hint(
572 target,
573 line,
574 format!("throws {}", throws_list.join(", ")),
575 SuggestionSource::Converted,
576 ));
577 }
578
579 if self.parse_tsdoc {
581 if let Some((_, stability)) = parsed.custom_tags.iter().find(|(k, _)| k == "stability")
583 {
584 suggestions.push(Suggestion::new(
585 target,
586 line,
587 AnnotationType::Stability,
588 stability,
589 SuggestionSource::Converted,
590 ));
591 }
592
593 for (key, value) in &parsed.custom_tags {
595 if key == "defaultValue" {
596 suggestions.push(Suggestion::ai_hint(
597 target,
598 line,
599 format!("default: {}", value),
600 SuggestionSource::Converted,
601 ));
602 }
603 if key == "typeParam" {
604 suggestions.push(Suggestion::ai_hint(
605 target,
606 line,
607 format!("type param {}", value),
608 SuggestionSource::Converted,
609 ));
610 }
611 if key == "override" && value == "true" {
612 suggestions.push(Suggestion::ai_hint(
613 target,
614 line,
615 "overrides parent",
616 SuggestionSource::Converted,
617 ));
618 }
619 if key == "sealed" && value == "true" {
620 suggestions.push(Suggestion::lock(
621 target,
622 line,
623 "strict",
624 SuggestionSource::Converted,
625 ));
626 }
627 }
628 }
629
630 if parsed.is_readonly() {
632 suggestions.push(Suggestion::ai_hint(
633 target,
634 line,
635 "readonly",
636 SuggestionSource::Converted,
637 ));
638 }
639
640 if !parsed.examples.is_empty() {
642 suggestions.push(Suggestion::ai_hint(
643 target,
644 line,
645 "has examples",
646 SuggestionSource::Converted,
647 ));
648 }
649
650 for note in &parsed.notes {
652 suggestions.push(Suggestion::ai_hint(
653 target,
654 line,
655 note,
656 SuggestionSource::Converted,
657 ));
658 }
659
660 suggestions
661 }
662}
663
664impl JsDocParser {
665 fn save_multiline_tag(&self, doc: &mut ParsedDocumentation, tag: &str, content: &str) {
667 let content = content.trim().to_string();
668 if content.is_empty() {
669 return;
670 }
671
672 match tag {
673 "description" | "desc" => {
674 doc.description = Some(content);
675 }
676 "example" => {
677 doc.examples.push(content);
678 }
679 _ => {
680 doc.custom_tags.push((tag.to_string(), content));
681 }
682 }
683 }
684}
685
686impl DocStandardParser for TsDocParser {
687 fn parse(&self, raw_comment: &str) -> ParsedDocumentation {
688 let parser = JsDocParser::with_tsdoc();
690 parser.parse(raw_comment)
691 }
692
693 fn standard_name(&self) -> &'static str {
694 "tsdoc"
695 }
696
697 fn to_suggestions(
698 &self,
699 parsed: &ParsedDocumentation,
700 target: &str,
701 line: usize,
702 ) -> Vec<Suggestion> {
703 let parser = JsDocParser::with_tsdoc();
705 parser.to_suggestions(parsed, target, line)
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
714 fn test_parse_basic_jsdoc() {
715 let parser = JsDocParser::new();
716 let doc = parser.parse(
717 r#"
718 /**
719 * Validates a user session.
720 * @param {string} token - The JWT token
721 * @returns {Promise<User>} The user object
722 * @deprecated Use validateSessionV2 instead
723 */
724 "#,
725 );
726
727 assert_eq!(doc.summary, Some("Validates a user session.".to_string()));
728 assert_eq!(
729 doc.deprecated,
730 Some("Use validateSessionV2 instead".to_string())
731 );
732 assert_eq!(doc.params.len(), 1);
733 assert_eq!(doc.params[0].0, "token");
734 assert!(doc.returns.is_some());
735 }
736
737 #[test]
738 fn test_parse_module_jsdoc() {
739 let parser = JsDocParser::new();
740 let doc = parser.parse(
741 r#"
742 /**
743 * @module Authentication
744 * @category Security
745 */
746 "#,
747 );
748
749 assert_eq!(doc.get_module(), Some("Authentication"));
750 assert_eq!(doc.get_category(), Some("Security"));
751 }
752
753 #[test]
754 fn test_parse_visibility_tags() {
755 let parser = JsDocParser::new();
756
757 let private_doc = parser.parse("/** @private */");
758 assert_eq!(private_doc.get_visibility(), Some("private"));
759
760 let internal_doc = parser.parse("/** @internal */");
761 assert_eq!(internal_doc.get_visibility(), Some("internal"));
762 }
763
764 #[test]
765 fn test_parse_see_and_link() {
766 let parser = JsDocParser::new();
767 let doc = parser.parse(
768 r#"
769 /**
770 * See {@link OtherClass} for more info.
771 * @see AnotherClass
772 */
773 "#,
774 );
775
776 assert!(doc.see_refs.contains(&"OtherClass".to_string()));
777 assert!(doc.see_refs.contains(&"AnotherClass".to_string()));
778 }
779
780 #[test]
781 fn test_parse_throws() {
782 let parser = JsDocParser::new();
783 let doc = parser.parse(
784 r#"
785 /**
786 * @throws {Error} When something goes wrong
787 * @throws {ValidationError} When validation fails
788 */
789 "#,
790 );
791
792 assert_eq!(doc.throws.len(), 2);
793 assert_eq!(doc.throws[0].0, "Error");
794 assert_eq!(doc.throws[1].0, "ValidationError");
795 }
796
797 #[test]
798 fn test_parse_todo() {
799 let parser = JsDocParser::new();
800 let doc = parser.parse(
801 r#"
802 /**
803 * @todo Add proper error handling
804 * @fixme This is broken
805 */
806 "#,
807 );
808
809 assert_eq!(doc.todos.len(), 2);
810 assert!(doc.todos[0].contains("error handling"));
811 }
812
813 #[test]
814 fn test_parse_example() {
815 let parser = JsDocParser::new();
816 let doc = parser.parse(
817 r#"
818 /**
819 * Example function
820 * @example
821 * const result = myFunc();
822 * console.log(result);
823 */
824 "#,
825 );
826
827 assert!(!doc.examples.is_empty());
828 assert!(doc.examples[0].contains("myFunc"));
829 }
830
831 #[test]
832 fn test_parse_readonly() {
833 let parser = JsDocParser::new();
834 let doc = parser.parse("/** @readonly */");
835 assert!(doc.is_readonly());
836 }
837
838 #[test]
841 fn test_parse_tsdoc_alpha() {
842 let parser = JsDocParser::with_tsdoc();
843 let doc = parser.parse(
844 r#"
845 /**
846 * Experimental API
847 * @alpha
848 */
849 "#,
850 );
851
852 let has_alpha = doc
853 .custom_tags
854 .iter()
855 .any(|(k, v)| k == "stability" && v == "alpha");
856 assert!(has_alpha);
857 }
858
859 #[test]
860 fn test_parse_tsdoc_beta() {
861 let parser = JsDocParser::with_tsdoc();
862 let doc = parser.parse(
863 r#"
864 /**
865 * Preview API
866 * @beta
867 */
868 "#,
869 );
870
871 let has_beta = doc
872 .custom_tags
873 .iter()
874 .any(|(k, v)| k == "stability" && v == "beta");
875 assert!(has_beta);
876 }
877
878 #[test]
879 fn test_parse_tsdoc_package_documentation() {
880 let parser = JsDocParser::with_tsdoc();
881 let doc = parser.parse(
882 r#"
883 /**
884 * @packageDocumentation
885 * This is the main module.
886 */
887 "#,
888 );
889
890 let has_pkg_doc = doc
891 .custom_tags
892 .iter()
893 .any(|(k, v)| k == "packageDocumentation" && v == "true");
894 assert!(has_pkg_doc);
895 }
896
897 #[test]
898 fn test_parse_tsdoc_remarks() {
899 let parser = JsDocParser::with_tsdoc();
900 let doc = parser.parse(
901 r#"
902 /**
903 * Brief summary.
904 * @remarks
905 * This is a longer explanation
906 * that spans multiple lines.
907 */
908 "#,
909 );
910
911 assert!(!doc.notes.is_empty());
912 assert!(doc.notes[0].contains("longer explanation"));
913 }
914
915 #[test]
916 fn test_parse_tsdoc_default_value() {
917 let parser = JsDocParser::with_tsdoc();
918 let doc = parser.parse(
919 r#"
920 /**
921 * The timeout in milliseconds.
922 * @defaultValue 5000
923 */
924 "#,
925 );
926
927 let has_default = doc
928 .custom_tags
929 .iter()
930 .any(|(k, v)| k == "defaultValue" && v == "5000");
931 assert!(has_default);
932 }
933
934 #[test]
935 fn test_parse_tsdoc_type_param() {
936 let parser = JsDocParser::with_tsdoc();
937 let doc = parser.parse(
938 r#"
939 /**
940 * A generic container.
941 * @typeParam T The type of contained value
942 */
943 "#,
944 );
945
946 let has_type_param = doc
947 .custom_tags
948 .iter()
949 .any(|(k, v)| k == "typeParam" && v.contains("T:"));
950 assert!(has_type_param);
951 }
952
953 #[test]
954 fn test_parse_tsdoc_override() {
955 let parser = JsDocParser::with_tsdoc();
956 let doc = parser.parse(
957 r#"
958 /**
959 * Overrides parent implementation.
960 * @override
961 */
962 "#,
963 );
964
965 let has_override = doc
966 .custom_tags
967 .iter()
968 .any(|(k, v)| k == "override" && v == "true");
969 assert!(has_override);
970 }
971
972 #[test]
973 fn test_parse_tsdoc_sealed() {
974 let parser = JsDocParser::with_tsdoc();
975 let doc = parser.parse(
976 r#"
977 /**
978 * This class cannot be extended.
979 * @sealed
980 */
981 "#,
982 );
983
984 let has_sealed = doc
985 .custom_tags
986 .iter()
987 .any(|(k, v)| k == "sealed" && v == "true");
988 assert!(has_sealed);
989 }
990
991 #[test]
992 fn test_parse_tsdoc_virtual() {
993 let parser = JsDocParser::with_tsdoc();
994 let doc = parser.parse(
995 r#"
996 /**
997 * Can be overridden by subclasses.
998 * @virtual
999 */
1000 "#,
1001 );
1002
1003 let has_virtual = doc
1004 .custom_tags
1005 .iter()
1006 .any(|(k, v)| k == "virtual" && v == "true");
1007 assert!(has_virtual);
1008 }
1009
1010 #[test]
1011 fn test_parse_inherit_doc() {
1012 let parser = JsDocParser::with_tsdoc();
1013 let doc = parser.parse(
1014 r#"
1015 /**
1016 * {@inheritDoc ParentClass.method}
1017 */
1018 "#,
1019 );
1020
1021 let has_inherit = doc.custom_tags.iter().any(|(k, _)| k == "inheritDoc");
1022 assert!(has_inherit);
1023 }
1024
1025 #[test]
1026 fn test_tsdoc_to_suggestions_alpha() {
1027 let parser = JsDocParser::with_tsdoc();
1028 let doc = parser.parse(
1029 r#"
1030 /**
1031 * Experimental feature.
1032 * @alpha
1033 */
1034 "#,
1035 );
1036
1037 let suggestions = parser.to_suggestions(&doc, "myFunction", 10);
1038 let has_stability = suggestions
1039 .iter()
1040 .any(|s| s.annotation_type == AnnotationType::Stability && s.value == "alpha");
1041 assert!(has_stability);
1042 }
1043
1044 #[test]
1045 fn test_tsdoc_to_suggestions_sealed() {
1046 let parser = JsDocParser::with_tsdoc();
1047 let doc = parser.parse(
1048 r#"
1049 /**
1050 * Locked class.
1051 * @sealed
1052 */
1053 "#,
1054 );
1055
1056 let suggestions = parser.to_suggestions(&doc, "MyClass", 10);
1057 let has_strict_lock = suggestions
1058 .iter()
1059 .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "strict");
1060 assert!(has_strict_lock);
1061 }
1062
1063 #[test]
1064 fn test_tsdoc_to_suggestions_default_value() {
1065 let parser = JsDocParser::with_tsdoc();
1066 let doc = parser.parse(
1067 r#"
1068 /**
1069 * Timeout setting.
1070 * @defaultValue 3000
1071 */
1072 "#,
1073 );
1074
1075 let suggestions = parser.to_suggestions(&doc, "timeout", 10);
1076 let has_default_hint = suggestions
1077 .iter()
1078 .any(|s| s.annotation_type == AnnotationType::AiHint && s.value.contains("default:"));
1079 assert!(has_default_hint);
1080 }
1081
1082 #[test]
1083 fn test_tsdoc_parser_delegation() {
1084 let parser = TsDocParser::new();
1085 let doc = parser.parse(
1086 r#"
1087 /**
1088 * API function.
1089 * @beta
1090 * @param {string} name The name
1091 */
1092 "#,
1093 );
1094
1095 assert_eq!(doc.summary, Some("API function.".to_string()));
1096 assert_eq!(doc.params.len(), 1);
1097 let has_beta = doc
1098 .custom_tags
1099 .iter()
1100 .any(|(k, v)| k == "stability" && v == "beta");
1101 assert!(has_beta);
1102 }
1103
1104 #[test]
1105 fn test_multiline_remarks() {
1106 let parser = JsDocParser::with_tsdoc();
1107 let doc = parser.parse(
1108 r#"
1109 /**
1110 * Summary line.
1111 *
1112 * @remarks
1113 * First line of remarks.
1114 * Second line of remarks.
1115 * Third line with more detail.
1116 *
1117 * @param x A parameter
1118 */
1119 "#,
1120 );
1121
1122 assert!(!doc.notes.is_empty());
1123 let remarks = &doc.notes[0];
1124 assert!(remarks.contains("First line"));
1125 assert!(remarks.contains("Third line"));
1126 }
1127
1128 #[test]
1129 fn test_complex_tsdoc() {
1130 let parser = JsDocParser::with_tsdoc();
1131 let doc = parser.parse(
1132 r#"
1133 /**
1134 * Processes user authentication.
1135 *
1136 * @remarks
1137 * This function handles OAuth2 and JWT tokens.
1138 * It's designed for high-throughput scenarios.
1139 *
1140 * @typeParam T The credential type
1141 * @param credentials User credentials
1142 * @returns The authenticated user
1143 * @throws {AuthError} When authentication fails
1144 * @beta
1145 * @see OAuthProvider
1146 *
1147 * @example
1148 * const user = await authenticate(creds);
1149 * console.log(user.name);
1150 */
1151 "#,
1152 );
1153
1154 assert_eq!(
1155 doc.summary,
1156 Some("Processes user authentication.".to_string())
1157 );
1158 assert!(!doc.notes.is_empty());
1159 assert!(doc.notes[0].contains("OAuth2"));
1160 assert_eq!(doc.params.len(), 1);
1161 assert!(doc.returns.is_some());
1162 assert_eq!(doc.throws.len(), 1);
1163 assert!(doc.see_refs.contains(&"OAuthProvider".to_string()));
1164 assert!(!doc.examples.is_empty());
1165
1166 let has_beta = doc
1167 .custom_tags
1168 .iter()
1169 .any(|(k, v)| k == "stability" && v == "beta");
1170 assert!(has_beta);
1171
1172 let has_type_param = doc.custom_tags.iter().any(|(k, _)| k == "typeParam");
1173 assert!(has_type_param);
1174 }
1175}