1use std::sync::LazyLock;
35
36use regex::Regex;
37
38use super::{DocStandardParser, ParsedDocumentation};
39use crate::annotate::{AnnotationType, Suggestion, SuggestionSource};
40
41static PARAM_TAG: LazyLock<Regex> =
43 LazyLock::new(|| Regex::new(r"^@param\s+(\w+)\s+(.*)$").expect("Invalid param tag regex"));
44
45static RETURN_TAG: LazyLock<Regex> =
47 LazyLock::new(|| Regex::new(r"^@returns?\s+(.*)$").expect("Invalid return tag regex"));
48
49static THROWS_TAG: LazyLock<Regex> = LazyLock::new(|| {
51 Regex::new(r"^@(?:throws|exception)\s+(\S+)\s*(.*)$").expect("Invalid throws tag regex")
52});
53
54static AUTHOR_TAG: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new(r"^@author\s+(.+)$").expect("Invalid author tag regex"));
57
58static SINCE_TAG: LazyLock<Regex> =
60 LazyLock::new(|| Regex::new(r"^@since\s+(.+)$").expect("Invalid since tag regex"));
61
62static VERSION_TAG: LazyLock<Regex> =
64 LazyLock::new(|| Regex::new(r"^@version\s+(.+)$").expect("Invalid version tag regex"));
65
66static SEE_TAG: LazyLock<Regex> =
68 LazyLock::new(|| Regex::new(r"^@see\s+(.+)$").expect("Invalid see tag regex"));
69
70static LINK_INLINE: LazyLock<Regex> =
72 LazyLock::new(|| Regex::new(r"\{@link\s+([^}]+)\}").expect("Invalid link inline regex"));
73
74static CODE_INLINE: LazyLock<Regex> =
76 LazyLock::new(|| Regex::new(r"\{@code\s+([^}]+)\}").expect("Invalid code inline regex"));
77
78static INHERIT_DOC: LazyLock<Regex> =
80 LazyLock::new(|| Regex::new(r"\{@inheritDoc\}").expect("Invalid inheritDoc regex"));
81
82static HTML_TAG: LazyLock<Regex> =
84 LazyLock::new(|| Regex::new(r"<[^>]+>").expect("Invalid HTML tag regex"));
85
86static CODE_BLOCK: LazyLock<Regex> = LazyLock::new(|| {
88 Regex::new(r"<pre>\s*(?:<code>)?([\s\S]*?)(?:</code>)?\s*</pre>")
89 .expect("Invalid code block regex")
90});
91
92#[derive(Debug, Clone, Default)]
94pub struct JavadocExtensions {
95 pub is_package_doc: bool,
97
98 pub is_type_doc: bool,
100
101 pub version: Option<String>,
103
104 pub authors: Vec<String>,
106
107 pub inherits_doc: bool,
109
110 pub code_examples: Vec<String>,
112
113 pub inline_links: Vec<String>,
115}
116
117pub struct JavadocParser {
120 extensions: JavadocExtensions,
122}
123
124impl JavadocParser {
125 pub fn new() -> Self {
127 Self {
128 extensions: JavadocExtensions::default(),
129 }
130 }
131
132 pub fn for_package() -> Self {
134 Self {
135 extensions: JavadocExtensions {
136 is_package_doc: true,
137 ..Default::default()
138 },
139 }
140 }
141
142 pub fn for_type() -> Self {
144 Self {
145 extensions: JavadocExtensions {
146 is_type_doc: true,
147 ..Default::default()
148 },
149 }
150 }
151
152 pub fn extensions(&self) -> &JavadocExtensions {
154 &self.extensions
155 }
156
157 fn strip_comment_markers(line: &str) -> &str {
159 let trimmed = line.trim();
160
161 if let Some(rest) = trimmed.strip_prefix("/**") {
163 return rest.trim();
164 }
165
166 if let Some(rest) = trimmed.strip_suffix("*/") {
168 let rest = rest.trim();
169 if let Some(rest) = rest.strip_prefix('*') {
171 return rest.trim_start();
172 }
173 return rest;
174 }
175
176 if let Some(rest) = trimmed.strip_prefix('*') {
178 if !rest.starts_with('/') {
180 return rest.trim_start();
181 }
182 }
183
184 trimmed
185 }
186
187 fn strip_html(text: &str) -> String {
189 HTML_TAG.replace_all(text, "").to_string()
190 }
191
192 fn process_inline_tags(text: &str) -> (String, Vec<String>) {
194 let mut links = Vec::new();
195
196 for caps in LINK_INLINE.captures_iter(text) {
198 if let Some(link) = caps.get(1) {
199 links.push(link.as_str().trim().to_string());
200 }
201 }
202
203 let processed = LINK_INLINE.replace_all(text, "$1");
205 let processed = CODE_INLINE.replace_all(&processed, "`$1`");
206 let processed = INHERIT_DOC.replace_all(&processed, "[inherited]");
207
208 (processed.to_string(), links)
209 }
210
211 fn extract_summary(text: &str) -> String {
213 let mut summary = String::new();
214
215 for line in text.lines() {
216 let trimmed = line.trim();
217
218 if trimmed.is_empty() {
220 if summary.is_empty() {
221 continue;
222 } else {
223 break;
224 }
225 }
226
227 if !summary.is_empty() {
229 summary.push(' ');
230 }
231
232 for (i, c) in trimmed.char_indices() {
234 if c == '.' || c == '!' || c == '?' {
235 let next_byte = i + c.len_utf8();
236 let rest = &trimmed[next_byte..];
237 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
238 summary.push_str(&trimmed[..next_byte]);
239 return Self::strip_html(&summary);
240 }
241 }
242 }
243
244 summary.push_str(trimmed);
245 }
246
247 Self::strip_html(&summary)
248 }
249
250 fn extract_code_examples(text: &str) -> Vec<String> {
252 CODE_BLOCK
253 .captures_iter(text)
254 .filter_map(|caps| caps.get(1).map(|m| m.as_str().trim().to_string()))
255 .filter(|s| !s.is_empty())
256 .collect()
257 }
258}
259
260impl Default for JavadocParser {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266impl DocStandardParser for JavadocParser {
267 fn parse(&self, raw_comment: &str) -> ParsedDocumentation {
268 let mut doc = ParsedDocumentation::new();
269 let mut extensions = self.extensions.clone();
270
271 let lines: Vec<&str> = raw_comment.lines().collect();
272 let mut content_lines = Vec::new();
273 let mut current_tag: Option<String> = None;
274 let mut tag_content = String::new();
275
276 let process_tag = |tag: &str,
278 content: &str,
279 doc: &mut ParsedDocumentation,
280 ext: &mut JavadocExtensions| {
281 let content = content.trim();
282 if content.is_empty() && tag != "@deprecated" {
283 return;
284 }
285
286 if let Some(caps) = PARAM_TAG.captures(&format!("{} {}", tag, content)) {
287 let name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
288 let desc = caps.get(2).map(|m| m.as_str()).unwrap_or("");
289 doc.params
290 .push((name.to_string(), None, Some(desc.to_string())));
291 } else if let Some(caps) = RETURN_TAG.captures(&format!("{} {}", tag, content)) {
292 let desc = caps.get(1).map(|m| m.as_str()).unwrap_or("");
293 doc.returns = Some((None, Some(desc.to_string())));
294 } else if let Some(caps) = THROWS_TAG.captures(&format!("{} {}", tag, content)) {
295 let exc_type = caps.get(1).map(|m| m.as_str()).unwrap_or("");
296 let desc = caps.get(2).map(|m| m.as_str());
297 doc.throws
298 .push((exc_type.to_string(), desc.map(|s| s.to_string())));
299 } else if let Some(caps) = AUTHOR_TAG.captures(&format!("{} {}", tag, content)) {
300 let author = caps.get(1).map(|m| m.as_str()).unwrap_or("");
301 doc.author = Some(author.to_string());
302 ext.authors.push(author.to_string());
303 } else if let Some(caps) = SINCE_TAG.captures(&format!("{} {}", tag, content)) {
304 let since = caps.get(1).map(|m| m.as_str()).unwrap_or("");
305 doc.since = Some(since.to_string());
306 } else if let Some(caps) = VERSION_TAG.captures(&format!("{} {}", tag, content)) {
307 let version = caps.get(1).map(|m| m.as_str()).unwrap_or("");
308 ext.version = Some(version.to_string());
309 } else if let Some(caps) = SEE_TAG.captures(&format!("{} {}", tag, content)) {
310 let reference = caps.get(1).map(|m| m.as_str()).unwrap_or("");
311 doc.see_refs.push(reference.to_string());
312 } else if tag == "@deprecated" {
313 let msg = if content.is_empty() {
314 "Deprecated".to_string()
315 } else {
316 content.to_string()
317 };
318 doc.deprecated = Some(msg);
319 }
320 };
321
322 for line in &lines {
323 let stripped = Self::strip_comment_markers(line);
324
325 if stripped.starts_with('@') {
327 if let Some(ref tag) = current_tag {
329 process_tag(tag, &tag_content, &mut doc, &mut extensions);
330 }
331
332 let parts: Vec<&str> = stripped.splitn(2, char::is_whitespace).collect();
334 current_tag = Some(parts[0].to_string());
335 tag_content = parts.get(1).map(|s| s.to_string()).unwrap_or_default();
336 } else if current_tag.is_some() {
337 if !tag_content.is_empty() {
339 tag_content.push(' ');
340 }
341 tag_content.push_str(stripped);
342 } else {
343 content_lines.push(stripped.to_string());
345 }
346 }
347
348 if let Some(ref tag) = current_tag {
350 process_tag(tag, &tag_content, &mut doc, &mut extensions);
351 }
352
353 let full_text = content_lines.join("\n");
355
356 if INHERIT_DOC.is_match(&full_text) {
358 extensions.inherits_doc = true;
359 }
360
361 let (processed_text, inline_links) = Self::process_inline_tags(&full_text);
363 extensions.inline_links = inline_links.clone();
364
365 for link in inline_links {
367 if !doc.see_refs.contains(&link) {
368 doc.see_refs.push(link);
369 }
370 }
371
372 extensions.code_examples = Self::extract_code_examples(&full_text);
374 for example in &extensions.code_examples {
375 doc.examples.push(example.clone());
376 }
377
378 let summary = Self::extract_summary(&processed_text);
380 if !summary.is_empty() {
381 doc.summary = Some(summary.clone());
382 }
383
384 let stripped_html = Self::strip_html(&processed_text);
386 let trimmed = stripped_html.trim();
387 if !trimmed.is_empty() && trimmed.len() > summary.len() {
388 doc.description = Some(trimmed.to_string());
389 }
390
391 if extensions.is_package_doc {
393 doc.custom_tags
394 .push(("package_doc".to_string(), "true".to_string()));
395 }
396 if extensions.is_type_doc {
397 doc.custom_tags
398 .push(("type_doc".to_string(), "true".to_string()));
399 }
400 if let Some(ref version) = extensions.version {
401 doc.custom_tags
402 .push(("version".to_string(), version.clone()));
403 }
404 if extensions.inherits_doc {
405 doc.custom_tags
406 .push(("inherits_doc".to_string(), "true".to_string()));
407 }
408 if extensions.authors.len() > 1 {
409 doc.custom_tags
410 .push(("multiple_authors".to_string(), "true".to_string()));
411 }
412
413 doc
414 }
415
416 fn standard_name(&self) -> &'static str {
417 "javadoc"
418 }
419
420 fn to_suggestions(
422 &self,
423 parsed: &ParsedDocumentation,
424 target: &str,
425 line: usize,
426 ) -> Vec<Suggestion> {
427 let mut suggestions = Vec::new();
428
429 if let Some(summary) = &parsed.summary {
431 let truncated = truncate_for_summary(summary, 100);
432 suggestions.push(Suggestion::summary(
433 target,
434 line,
435 truncated,
436 SuggestionSource::Converted,
437 ));
438 }
439
440 if let Some(msg) = &parsed.deprecated {
442 suggestions.push(Suggestion::deprecated(
443 target,
444 line,
445 msg,
446 SuggestionSource::Converted,
447 ));
448 }
449
450 for see_ref in &parsed.see_refs {
452 suggestions.push(Suggestion::new(
453 target,
454 line,
455 AnnotationType::Ref,
456 see_ref,
457 SuggestionSource::Converted,
458 ));
459 }
460
461 if !parsed.throws.is_empty() {
463 let throws_list: Vec<String> = parsed.throws.iter().map(|(t, _)| t.clone()).collect();
464 suggestions.push(Suggestion::ai_hint(
465 target,
466 line,
467 format!("throws {}", throws_list.join(", ")),
468 SuggestionSource::Converted,
469 ));
470 }
471
472 if parsed
474 .custom_tags
475 .iter()
476 .any(|(k, v)| k == "package_doc" && v == "true")
477 {
478 suggestions.push(Suggestion::new(
479 target,
480 line,
481 AnnotationType::Module,
482 parsed.summary.as_deref().unwrap_or(target),
483 SuggestionSource::Converted,
484 ));
485 }
486
487 if parsed
489 .custom_tags
490 .iter()
491 .any(|(k, v)| k == "type_doc" && v == "true")
492 {
493 suggestions.push(Suggestion::ai_hint(
494 target,
495 line,
496 "type-level documentation",
497 SuggestionSource::Converted,
498 ));
499 }
500
501 if parsed
503 .custom_tags
504 .iter()
505 .any(|(k, v)| k == "inherits_doc" && v == "true")
506 {
507 suggestions.push(Suggestion::ai_hint(
508 target,
509 line,
510 "inherits documentation from parent",
511 SuggestionSource::Converted,
512 ));
513 }
514
515 if let Some(since) = &parsed.since {
517 suggestions.push(Suggestion::ai_hint(
518 target,
519 line,
520 format!("since {}", since),
521 SuggestionSource::Converted,
522 ));
523 }
524
525 if let Some((_, version)) = parsed.custom_tags.iter().find(|(k, _)| k == "version") {
527 suggestions.push(Suggestion::ai_hint(
528 target,
529 line,
530 format!("version {}", version),
531 SuggestionSource::Converted,
532 ));
533 }
534
535 if !parsed.examples.is_empty() {
537 suggestions.push(Suggestion::ai_hint(
538 target,
539 line,
540 "has code examples",
541 SuggestionSource::Converted,
542 ));
543 }
544
545 if let Some(author) = &parsed.author {
547 suggestions.push(Suggestion::ai_hint(
548 target,
549 line,
550 format!("author: {}", author),
551 SuggestionSource::Converted,
552 ));
553 }
554
555 suggestions
556 }
557}
558
559fn truncate_for_summary(s: &str, max_len: usize) -> String {
561 let trimmed = s.trim();
562 if trimmed.len() <= max_len {
563 trimmed.to_string()
564 } else {
565 let truncate_at = trimmed[..max_len].rfind(' ').unwrap_or(max_len);
566 format!("{}...", &trimmed[..truncate_at])
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn test_strip_comment_markers() {
576 assert_eq!(JavadocParser::strip_comment_markers("/** Hello"), "Hello");
577 assert_eq!(JavadocParser::strip_comment_markers(" * Hello"), "Hello");
578 assert_eq!(JavadocParser::strip_comment_markers(" */"), "");
579 assert_eq!(
580 JavadocParser::strip_comment_markers(" * Indented"),
581 "Indented"
582 );
583 }
584
585 #[test]
586 fn test_strip_html() {
587 assert_eq!(
588 JavadocParser::strip_html("Hello <b>world</b>!"),
589 "Hello world!"
590 );
591 assert_eq!(JavadocParser::strip_html("<p>Paragraph</p>"), "Paragraph");
592 }
593
594 #[test]
595 fn test_process_inline_tags() {
596 let (processed, links) = JavadocParser::process_inline_tags(
597 "See {@link String} for details about {@code format}",
598 );
599 assert!(processed.contains("String"));
600 assert!(processed.contains("`format`"));
601 assert!(links.contains(&"String".to_string()));
602 }
603
604 #[test]
605 fn test_extract_summary() {
606 let summary = JavadocParser::extract_summary(
607 "Returns the length of this string. The length is equal to the number of characters.",
608 );
609 assert_eq!(summary, "Returns the length of this string.");
610 }
611
612 #[test]
613 fn test_parse_basic_javadoc() {
614 let parser = JavadocParser::new();
615 let doc = parser.parse(
616 r#"
617/**
618 * Returns the character at the specified index.
619 * An index ranges from 0 to length() - 1.
620 */
621"#,
622 );
623
624 assert_eq!(
625 doc.summary,
626 Some("Returns the character at the specified index.".to_string())
627 );
628 }
629
630 #[test]
631 fn test_parse_with_params() {
632 let parser = JavadocParser::new();
633 let doc = parser.parse(
634 r#"
635/**
636 * Copies characters from this string into the destination array.
637 *
638 * @param srcBegin index of the first character to copy
639 * @param srcEnd index after the last character to copy
640 * @param dst the destination array
641 */
642"#,
643 );
644
645 assert_eq!(doc.params.len(), 3);
646 assert_eq!(doc.params[0].0, "srcBegin");
647 assert_eq!(doc.params[1].0, "srcEnd");
648 assert_eq!(doc.params[2].0, "dst");
649 }
650
651 #[test]
652 fn test_parse_with_return() {
653 let parser = JavadocParser::new();
654 let doc = parser.parse(
655 r#"
656/**
657 * Returns the length of this string.
658 *
659 * @return the length of the sequence of characters
660 */
661"#,
662 );
663
664 assert!(doc.returns.is_some());
665 let (_, desc) = doc.returns.as_ref().unwrap();
666 assert!(desc.as_ref().unwrap().contains("length"));
667 }
668
669 #[test]
670 fn test_parse_with_throws() {
671 let parser = JavadocParser::new();
672 let doc = parser.parse(
673 r#"
674/**
675 * Returns the character at the specified index.
676 *
677 * @throws IndexOutOfBoundsException if the index is out of range
678 * @throws NullPointerException if the string is null
679 */
680"#,
681 );
682
683 assert_eq!(doc.throws.len(), 2);
684 assert_eq!(doc.throws[0].0, "IndexOutOfBoundsException");
685 assert_eq!(doc.throws[1].0, "NullPointerException");
686 }
687
688 #[test]
689 fn test_parse_with_exception() {
690 let parser = JavadocParser::new();
691 let doc = parser.parse(
692 r#"
693/**
694 * Parses the string.
695 *
696 * @exception ParseException if parsing fails
697 */
698"#,
699 );
700
701 assert_eq!(doc.throws.len(), 1);
702 assert_eq!(doc.throws[0].0, "ParseException");
703 }
704
705 #[test]
706 fn test_parse_with_see() {
707 let parser = JavadocParser::new();
708 let doc = parser.parse(
709 r#"
710/**
711 * Creates a new string builder.
712 *
713 * @see StringBuilder
714 * @see StringBuffer#append(String)
715 */
716"#,
717 );
718
719 assert!(doc.see_refs.contains(&"StringBuilder".to_string()));
720 assert!(doc
721 .see_refs
722 .contains(&"StringBuffer#append(String)".to_string()));
723 }
724
725 #[test]
726 fn test_parse_with_deprecated() {
727 let parser = JavadocParser::new();
728 let doc = parser.parse(
729 r#"
730/**
731 * Gets the date.
732 *
733 * @deprecated Use {@link LocalDate} instead
734 */
735"#,
736 );
737
738 assert!(doc.deprecated.is_some());
739 assert!(doc.deprecated.as_ref().unwrap().contains("LocalDate"));
740 }
741
742 #[test]
743 fn test_parse_with_author_and_since() {
744 let parser = JavadocParser::new();
745 let doc = parser.parse(
746 r#"
747/**
748 * A utility class for string operations.
749 *
750 * @author John Doe
751 * @since 1.0
752 * @version 2.1
753 */
754"#,
755 );
756
757 assert_eq!(doc.author, Some("John Doe".to_string()));
758 assert_eq!(doc.since, Some("1.0".to_string()));
759 assert!(doc
760 .custom_tags
761 .iter()
762 .any(|(k, v)| k == "version" && v == "2.1"));
763 }
764
765 #[test]
766 fn test_parse_with_inline_link() {
767 let parser = JavadocParser::new();
768 let doc = parser.parse(
769 r#"
770/**
771 * Returns a string similar to {@link String#valueOf(Object)}.
772 */
773"#,
774 );
775
776 assert!(doc.see_refs.contains(&"String#valueOf(Object)".to_string()));
777 }
778
779 #[test]
780 fn test_parse_with_code_block() {
781 let parser = JavadocParser::new();
782 let doc = parser.parse(
783 r#"
784/**
785 * Formats a string.
786 *
787 * <pre>
788 * String result = format("Hello %s", "World");
789 * </pre>
790 */
791"#,
792 );
793
794 assert!(!doc.examples.is_empty());
795 assert!(doc.examples[0].contains("format"));
796 }
797
798 #[test]
799 fn test_parse_with_inherit_doc() {
800 let parser = JavadocParser::new();
801 let doc = parser.parse(
802 r#"
803/**
804 * {@inheritDoc}
805 */
806"#,
807 );
808
809 assert!(doc
810 .custom_tags
811 .iter()
812 .any(|(k, v)| k == "inherits_doc" && v == "true"));
813 }
814
815 #[test]
816 fn test_parse_multiline_param() {
817 let parser = JavadocParser::new();
818 let doc = parser.parse(
819 r#"
820/**
821 * Processes input.
822 *
823 * @param data the input data to process,
824 * which can span multiple lines
825 */
826"#,
827 );
828
829 assert_eq!(doc.params.len(), 1);
830 let (_, _, desc) = &doc.params[0];
831 assert!(desc.as_ref().unwrap().contains("multiple lines"));
832 }
833
834 #[test]
835 fn test_parse_html_in_description() {
836 let parser = JavadocParser::new();
837 let doc = parser.parse(
838 r#"
839/**
840 * <p>Returns the <b>formatted</b> string.</p>
841 *
842 * <ul>
843 * <li>Item 1</li>
844 * <li>Item 2</li>
845 * </ul>
846 */
847"#,
848 );
849
850 assert!(doc.summary.is_some());
852 let summary = doc.summary.as_ref().unwrap();
853 assert!(!summary.contains("<p>"));
854 assert!(!summary.contains("<b>"));
855 }
856
857 #[test]
858 fn test_package_doc_parser() {
859 let parser = JavadocParser::for_package();
860 let doc = parser.parse(
861 r#"
862/**
863 * Provides utility classes for string manipulation.
864 */
865"#,
866 );
867
868 assert!(doc
869 .custom_tags
870 .iter()
871 .any(|(k, v)| k == "package_doc" && v == "true"));
872 }
873
874 #[test]
875 fn test_type_doc_parser() {
876 let parser = JavadocParser::for_type();
877 let doc = parser.parse(
878 r#"
879/**
880 * A class representing a person.
881 */
882"#,
883 );
884
885 assert!(doc
886 .custom_tags
887 .iter()
888 .any(|(k, v)| k == "type_doc" && v == "true"));
889 }
890
891 #[test]
892 fn test_to_suggestions_basic() {
893 let parser = JavadocParser::new();
894 let doc = parser.parse(
895 r#"
896/**
897 * Creates a new instance of the class.
898 */
899"#,
900 );
901
902 let suggestions = parser.to_suggestions(&doc, "MyClass", 10);
903
904 assert!(suggestions
905 .iter()
906 .any(|s| s.annotation_type == AnnotationType::Summary
907 && s.value.contains("Creates a new instance")));
908 }
909
910 #[test]
911 fn test_to_suggestions_deprecated() {
912 let parser = JavadocParser::new();
913 let doc = parser.parse(
914 r#"
915/**
916 * Old method.
917 * @deprecated Use newMethod instead
918 */
919"#,
920 );
921
922 let suggestions = parser.to_suggestions(&doc, "oldMethod", 1);
923
924 assert!(suggestions
925 .iter()
926 .any(|s| s.annotation_type == AnnotationType::Deprecated));
927 }
928
929 #[test]
930 fn test_to_suggestions_throws() {
931 let parser = JavadocParser::new();
932 let doc = parser.parse(
933 r#"
934/**
935 * Parses input.
936 * @throws IOException if reading fails
937 * @throws ParseException if parsing fails
938 */
939"#,
940 );
941
942 let suggestions = parser.to_suggestions(&doc, "parse", 1);
943
944 assert!(suggestions
945 .iter()
946 .any(|s| s.annotation_type == AnnotationType::AiHint && s.value.contains("throws")));
947 }
948
949 #[test]
950 fn test_to_suggestions_refs() {
951 let parser = JavadocParser::new();
952 let doc = parser.parse(
953 r#"
954/**
955 * Gets the value.
956 * @see OtherClass
957 */
958"#,
959 );
960
961 let suggestions = parser.to_suggestions(&doc, "getValue", 1);
962
963 assert!(suggestions
964 .iter()
965 .any(|s| s.annotation_type == AnnotationType::Ref && s.value == "OtherClass"));
966 }
967
968 #[test]
969 fn test_to_suggestions_since() {
970 let parser = JavadocParser::new();
971 let doc = parser.parse(
972 r#"
973/**
974 * New feature.
975 * @since 2.0
976 */
977"#,
978 );
979
980 let suggestions = parser.to_suggestions(&doc, "feature", 1);
981
982 assert!(suggestions
983 .iter()
984 .any(|s| s.annotation_type == AnnotationType::AiHint && s.value.contains("since 2.0")));
985 }
986
987 #[test]
988 fn test_to_suggestions_examples() {
989 let parser = JavadocParser::new();
990 let doc = parser.parse(
991 r#"
992/**
993 * Formats output.
994 * <pre>
995 * format("test");
996 * </pre>
997 */
998"#,
999 );
1000
1001 let suggestions = parser.to_suggestions(&doc, "format", 1);
1002
1003 assert!(suggestions
1004 .iter()
1005 .any(|s| s.annotation_type == AnnotationType::AiHint && s.value.contains("examples")));
1006 }
1007
1008 #[test]
1009 fn test_truncate_for_summary() {
1010 assert_eq!(truncate_for_summary("Short", 100), "Short");
1011 assert_eq!(
1012 truncate_for_summary("This is a very long summary that needs truncation", 20),
1013 "This is a very long..."
1014 );
1015 }
1016
1017 #[test]
1018 fn test_extract_summary_multi_sentence() {
1019 let summary = JavadocParser::extract_summary("First sentence. Second sentence.");
1020 assert_eq!(summary, "First sentence.");
1021 }
1022
1023 #[test]
1024 fn test_deprecated_empty() {
1025 let parser = JavadocParser::new();
1026 let doc = parser.parse(
1027 r#"
1028/**
1029 * Old method.
1030 * @deprecated
1031 */
1032"#,
1033 );
1034
1035 assert!(doc.deprecated.is_some());
1036 assert_eq!(doc.deprecated.as_ref().unwrap(), "Deprecated");
1037 }
1038}