1use crate::model::{ParamDoc, ParsedDocstring, RaisesDoc, ReturnDoc};
12
13pub fn parse_docstring(docstring: &str) -> ParsedDocstring {
15 let docstring = docstring.trim();
16 if docstring.is_empty() {
17 return ParsedDocstring::empty();
18 }
19
20 let style = detect_style(docstring);
22
23 match style {
24 DocstringStyle::Google => parse_google_style(docstring),
25 DocstringStyle::NumPy => parse_numpy_style(docstring),
26 DocstringStyle::Plain => parse_plain(docstring),
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31enum DocstringStyle {
32 Google,
33 NumPy,
34 Plain,
35}
36
37fn detect_style(docstring: &str) -> DocstringStyle {
39 if docstring.contains("\n----------")
43 || docstring.contains("\n---------")
44 || docstring.contains("\n--------")
45 {
46 return DocstringStyle::NumPy;
47 }
48
49 let google_markers = [
51 "Args:",
52 "Arguments:",
53 "Parameters:",
54 "Returns:",
55 "Raises:",
56 "Raises:",
57 "Example:",
58 "Examples:",
59 "Attributes:",
60 "Note:",
61 "Notes:",
62 "Yields:",
63 ];
64
65 for marker in &google_markers {
66 if docstring.contains(marker) {
67 return DocstringStyle::Google;
68 }
69 }
70
71 DocstringStyle::Plain
72}
73
74fn parse_google_style(docstring: &str) -> ParsedDocstring {
76 let lines: Vec<&str> = docstring.lines().collect();
77
78 let (summary, description, section_start) = extract_summary_and_description(&lines);
80
81 let mut params = Vec::new();
82 let mut returns = None;
83 let mut raises = Vec::new();
84 let mut examples = Vec::new();
85
86 let mut i = section_start;
88 while i < lines.len() {
89 let line = lines[i].trim();
90
91 if line.ends_with(':') && !line.contains(' ') {
92 let section_name = &line[..line.len() - 1];
94 match section_name.to_lowercase().as_str() {
95 "args" | "arguments" | "parameters" | "params" => {
96 let (parsed, next_i) = parse_google_params(&lines, i + 1);
97 params = parsed;
98 i = next_i;
99 }
100 "returns" | "return" => {
101 let (parsed, next_i) = parse_google_returns(&lines, i + 1);
102 returns = parsed;
103 i = next_i;
104 }
105 "raises" | "raise" | "exceptions" | "except" => {
106 let (parsed, next_i) = parse_google_raises(&lines, i + 1);
107 raises = parsed;
108 i = next_i;
109 }
110 "example" | "examples" => {
111 let (parsed, next_i) = parse_google_examples(&lines, i + 1);
112 examples = parsed;
113 i = next_i;
114 }
115 _ => {
116 i += 1;
117 }
118 }
119 } else {
120 i += 1;
121 }
122 }
123
124 ParsedDocstring {
125 summary,
126 description,
127 params,
128 returns,
129 raises,
130 examples,
131 }
132}
133
134fn parse_numpy_style(docstring: &str) -> ParsedDocstring {
136 let lines: Vec<&str> = docstring.lines().collect();
137
138 let (summary, description, section_start) = extract_summary_and_description(&lines);
140
141 let mut params = Vec::new();
142 let mut returns = None;
143 let mut raises = Vec::new();
144 let mut examples = Vec::new();
145
146 let mut i = section_start;
148 while i < lines.len() {
149 let line = lines[i].trim();
150
151 if i + 1 < lines.len() {
153 let next_line = lines[i + 1].trim();
154 if next_line.chars().all(|c| c == '-') && !next_line.is_empty() {
155 match line.to_lowercase().as_str() {
156 "parameters" | "params" | "arguments" => {
157 let (parsed, next_i) = parse_numpy_params(&lines, i + 2);
158 params = parsed;
159 i = next_i;
160 continue;
161 }
162 "returns" => {
163 let (parsed, next_i) = parse_numpy_returns(&lines, i + 2);
164 returns = parsed;
165 i = next_i;
166 continue;
167 }
168 "raises" | "exceptions" => {
169 let (parsed, next_i) = parse_numpy_raises(&lines, i + 2);
170 raises = parsed;
171 i = next_i;
172 continue;
173 }
174 "examples" | "example" => {
175 let (parsed, next_i) = parse_numpy_examples(&lines, i + 2);
176 examples = parsed;
177 i = next_i;
178 continue;
179 }
180 _ => {}
181 }
182 }
183 }
184 i += 1;
185 }
186
187 ParsedDocstring {
188 summary,
189 description,
190 params,
191 returns,
192 raises,
193 examples,
194 }
195}
196
197fn parse_plain(docstring: &str) -> ParsedDocstring {
199 let lines: Vec<&str> = docstring.lines().collect();
200 let (summary, description, _) = extract_summary_and_description(&lines);
201
202 ParsedDocstring {
203 summary,
204 description,
205 params: Vec::new(),
206 returns: None,
207 raises: Vec::new(),
208 examples: Vec::new(),
209 }
210}
211
212fn extract_summary_and_description(lines: &[&str]) -> (Option<String>, Option<String>, usize) {
214 if lines.is_empty() {
215 return (None, None, 0);
216 }
217
218 let mut summary_lines = Vec::new();
219 let mut description_lines = Vec::new();
220 let mut in_description = false;
221 let mut i = 0;
222
223 while i < lines.len() {
225 let line = lines[i].trim();
226
227 if line.is_empty() {
229 if !summary_lines.is_empty() {
230 in_description = true;
231 }
232 i += 1;
233 continue;
234 }
235
236 if line.ends_with(':') && !line.contains(' ') {
238 let section = &line[..line.len() - 1].to_lowercase();
239 if is_known_section(section) {
240 break;
241 }
242 }
243
244 if i + 1 < lines.len() {
246 let next_line = lines[i + 1].trim();
247 if next_line.chars().all(|c| c == '-')
248 && !next_line.is_empty()
249 && is_known_section(&line.to_lowercase())
250 {
251 break;
252 }
253 }
254
255 if in_description {
256 description_lines.push(line);
257 } else {
258 summary_lines.push(line);
259 }
260 i += 1;
261 }
262
263 let summary = if summary_lines.is_empty() {
264 None
265 } else {
266 Some(summary_lines.join(" "))
267 };
268
269 let description = if description_lines.is_empty() {
270 None
271 } else {
272 Some(description_lines.join("\n"))
273 };
274
275 (summary, description, i)
276}
277
278fn is_known_section(name: &str) -> bool {
279 matches!(
280 name,
281 "args"
282 | "arguments"
283 | "parameters"
284 | "params"
285 | "returns"
286 | "return"
287 | "raises"
288 | "raise"
289 | "exceptions"
290 | "except"
291 | "example"
292 | "examples"
293 | "attributes"
294 | "note"
295 | "notes"
296 | "yields"
297 | "yield"
298 | "see also"
299 | "references"
300 | "warnings"
301 | "warning"
302 )
303}
304
305fn parse_google_params(lines: &[&str], start: usize) -> (Vec<ParamDoc>, usize) {
307 let mut params = Vec::new();
308 let mut i = start;
309 let mut current_name = String::new();
310 let mut current_ty: Option<String> = None;
311 let mut current_desc = Vec::new();
312
313 while i < lines.len() {
314 let line = lines[i];
315 let trimmed = line.trim();
316
317 if trimmed.is_empty() {
319 if !current_name.is_empty() {
321 params.push(ParamDoc {
322 name: current_name.clone(),
323 ty: current_ty.clone(),
324 description: current_desc.join(" ").trim().to_string(),
325 });
326 current_name.clear();
327 current_ty = None;
328 current_desc.clear();
329 }
330 i += 1;
331 continue;
332 }
333
334 if trimmed.ends_with(':') && !trimmed.contains(' ') {
336 let section = &trimmed[..trimmed.len() - 1].to_lowercase();
337 if is_known_section(section) {
338 break;
339 }
340 }
341
342 let leading_spaces = line.len() - line.trim_start().len();
344
345 if leading_spaces <= 4 && trimmed.contains(':') {
347 if !current_name.is_empty() {
349 params.push(ParamDoc {
350 name: current_name,
351 ty: current_ty,
352 description: current_desc.join(" ").trim().to_string(),
353 });
354 }
355
356 let (name, ty, desc) = parse_param_line(trimmed);
358 current_name = name;
359 current_ty = ty;
360 current_desc = vec![desc];
361 } else if !current_name.is_empty() {
362 current_desc.push(trimmed.to_string());
364 }
365
366 i += 1;
367 }
368
369 if !current_name.is_empty() {
371 params.push(ParamDoc {
372 name: current_name,
373 ty: current_ty,
374 description: current_desc.join(" ").trim().to_string(),
375 });
376 }
377
378 (params, i)
379}
380
381fn parse_param_line(line: &str) -> (String, Option<String>, String) {
383 if let Some(colon_pos) = line.find(':') {
389 let before_colon = &line[..colon_pos];
390
391 if let Some(paren_start) = before_colon.find('(')
393 && let Some(paren_end) = before_colon.rfind(')')
394 && paren_start < paren_end
395 {
396 let name = before_colon[..paren_start].trim().to_string();
397 let ty = before_colon[paren_start + 1..paren_end].trim().to_string();
398 let desc = line[colon_pos + 1..].trim().to_string();
399 return (name, Some(ty), desc);
400 }
401
402 let name = before_colon.trim().to_string();
404 let desc = line[colon_pos + 1..].trim().to_string();
405 return (name, None, desc);
406 }
407
408 (line.trim().to_string(), None, String::new())
409}
410
411fn parse_google_returns(lines: &[&str], start: usize) -> (Option<ReturnDoc>, usize) {
413 let mut i = start;
414 let mut desc_lines = Vec::new();
415 let mut ty: Option<String> = None;
416
417 while i < lines.len() {
418 let line = lines[i];
419 let trimmed = line.trim();
420
421 if trimmed.is_empty() {
422 if !desc_lines.is_empty() {
423 break;
424 }
425 i += 1;
426 continue;
427 }
428
429 if trimmed.ends_with(':') && !trimmed.contains(' ') {
431 let section = &trimmed[..trimmed.len() - 1].to_lowercase();
432 if is_known_section(section) {
433 break;
434 }
435 }
436
437 if desc_lines.is_empty() && trimmed.contains(':') {
439 let colon_pos = trimmed.find(':').unwrap();
440 let potential_type = &trimmed[..colon_pos];
441 if !potential_type.contains(' ') || potential_type.contains('[') {
443 ty = Some(potential_type.trim().to_string());
444 desc_lines.push(trimmed[colon_pos + 1..].trim().to_string());
445 } else {
446 desc_lines.push(trimmed.to_string());
447 }
448 } else {
449 desc_lines.push(trimmed.to_string());
450 }
451
452 i += 1;
453 }
454
455 if desc_lines.is_empty() {
456 return (None, i);
457 }
458
459 let description = desc_lines.join(" ").trim().to_string();
460 (Some(ReturnDoc { ty, description }), i)
461}
462
463fn parse_google_raises(lines: &[&str], start: usize) -> (Vec<RaisesDoc>, usize) {
465 let mut raises = Vec::new();
466 let mut i = start;
467 let mut current_ty = String::new();
468 let mut current_desc = Vec::new();
469
470 while i < lines.len() {
471 let line = lines[i];
472 let trimmed = line.trim();
473
474 if trimmed.is_empty() {
475 if !current_ty.is_empty() {
476 raises.push(RaisesDoc {
477 ty: current_ty.clone(),
478 description: current_desc.join(" ").trim().to_string(),
479 });
480 current_ty.clear();
481 current_desc.clear();
482 }
483 i += 1;
484 continue;
485 }
486
487 if trimmed.ends_with(':') && !trimmed.contains(' ') {
489 let section = &trimmed[..trimmed.len() - 1].to_lowercase();
490 if is_known_section(section) {
491 break;
492 }
493 }
494
495 let leading_spaces = line.len() - line.trim_start().len();
496
497 if leading_spaces <= 4 && trimmed.contains(':') {
499 if !current_ty.is_empty() {
500 raises.push(RaisesDoc {
501 ty: current_ty,
502 description: current_desc.join(" ").trim().to_string(),
503 });
504 }
505
506 let colon_pos = trimmed.find(':').unwrap();
507 current_ty = trimmed[..colon_pos].trim().to_string();
508 current_desc = vec![trimmed[colon_pos + 1..].trim().to_string()];
509 } else if !current_ty.is_empty() {
510 current_desc.push(trimmed.to_string());
511 }
512
513 i += 1;
514 }
515
516 if !current_ty.is_empty() {
517 raises.push(RaisesDoc {
518 ty: current_ty,
519 description: current_desc.join(" ").trim().to_string(),
520 });
521 }
522
523 (raises, i)
524}
525
526fn parse_google_examples(lines: &[&str], start: usize) -> (Vec<String>, usize) {
528 let mut examples = Vec::new();
529 let mut current_example = Vec::new();
530 let mut in_code_block = false;
531 let mut i = start;
532
533 while i < lines.len() {
534 let line = lines[i];
535 let trimmed = line.trim();
536
537 if !in_code_block && trimmed.ends_with(':') && !trimmed.contains(' ') {
539 let section = &trimmed[..trimmed.len() - 1].to_lowercase();
540 if is_known_section(section) {
541 break;
542 }
543 }
544
545 if trimmed.starts_with("```") {
547 in_code_block = !in_code_block;
548 current_example.push(line.to_string());
549 i += 1;
550 continue;
551 }
552
553 if trimmed.is_empty() && !in_code_block {
555 if !current_example.is_empty() {
556 examples.push(current_example.join("\n"));
557 current_example.clear();
558 }
559 i += 1;
560 continue;
561 }
562
563 current_example.push(line.to_string());
564 i += 1;
565 }
566
567 if !current_example.is_empty() {
568 examples.push(current_example.join("\n"));
569 }
570
571 (examples, i)
572}
573
574fn parse_numpy_params(lines: &[&str], start: usize) -> (Vec<ParamDoc>, usize) {
576 let mut params = Vec::new();
577 let mut i = start;
578 let mut current_name = String::new();
579 let mut current_ty: Option<String> = None;
580 let mut current_desc = Vec::new();
581
582 while i < lines.len() {
583 let line = lines[i];
584 let trimmed = line.trim();
585
586 if i + 1 < lines.len() {
588 let next_line = lines[i + 1].trim();
589 if next_line.chars().all(|c| c == '-') && !next_line.is_empty() {
590 break;
591 }
592 }
593
594 if trimmed.is_empty() {
595 i += 1;
596 continue;
597 }
598
599 let leading_spaces = line.len() - line.trim_start().len();
600
601 if leading_spaces == 0 && trimmed.contains(':') {
603 if !current_name.is_empty() {
605 params.push(ParamDoc {
606 name: current_name,
607 ty: current_ty,
608 description: current_desc.join(" ").trim().to_string(),
609 });
610 }
611
612 let colon_pos = trimmed.find(':').unwrap();
613 current_name = trimmed[..colon_pos].trim().to_string();
614 let type_part = trimmed[colon_pos + 1..].trim();
615 current_ty = if type_part.is_empty() {
616 None
617 } else {
618 Some(type_part.to_string())
619 };
620 current_desc.clear();
621 } else if leading_spaces > 0 && !current_name.is_empty() {
622 current_desc.push(trimmed.to_string());
624 }
625
626 i += 1;
627 }
628
629 if !current_name.is_empty() {
630 params.push(ParamDoc {
631 name: current_name,
632 ty: current_ty,
633 description: current_desc.join(" ").trim().to_string(),
634 });
635 }
636
637 (params, i)
638}
639
640fn parse_numpy_returns(lines: &[&str], start: usize) -> (Option<ReturnDoc>, usize) {
642 let mut i = start;
643 let mut ty: Option<String> = None;
644 let mut desc_lines = Vec::new();
645
646 while i < lines.len() {
647 let line = lines[i];
648 let trimmed = line.trim();
649
650 if i + 1 < lines.len() {
652 let next_line = lines[i + 1].trim();
653 if next_line.chars().all(|c| c == '-') && !next_line.is_empty() {
654 break;
655 }
656 }
657
658 if trimmed.is_empty() {
659 if !desc_lines.is_empty() || ty.is_some() {
660 break;
661 }
662 i += 1;
663 continue;
664 }
665
666 let leading_spaces = line.len() - line.trim_start().len();
667
668 if ty.is_none() && leading_spaces == 0 {
670 if trimmed.contains(':') {
671 let colon_pos = trimmed.find(':').unwrap();
672 ty = Some(trimmed[colon_pos + 1..].trim().to_string());
673 } else {
674 ty = Some(trimmed.to_string());
675 }
676 } else if leading_spaces > 0 {
677 desc_lines.push(trimmed.to_string());
678 }
679
680 i += 1;
681 }
682
683 if ty.is_none() && desc_lines.is_empty() {
684 return (None, i);
685 }
686
687 (
688 Some(ReturnDoc {
689 ty,
690 description: desc_lines.join(" ").trim().to_string(),
691 }),
692 i,
693 )
694}
695
696fn parse_numpy_raises(lines: &[&str], start: usize) -> (Vec<RaisesDoc>, usize) {
698 let mut raises = Vec::new();
699 let mut i = start;
700 let mut current_ty = String::new();
701 let mut current_desc = Vec::new();
702
703 while i < lines.len() {
704 let line = lines[i];
705 let trimmed = line.trim();
706
707 if i + 1 < lines.len() {
709 let next_line = lines[i + 1].trim();
710 if next_line.chars().all(|c| c == '-') && !next_line.is_empty() {
711 break;
712 }
713 }
714
715 if trimmed.is_empty() {
716 i += 1;
717 continue;
718 }
719
720 let leading_spaces = line.len() - line.trim_start().len();
721
722 if leading_spaces == 0 {
723 if !current_ty.is_empty() {
725 raises.push(RaisesDoc {
726 ty: current_ty,
727 description: current_desc.join(" ").trim().to_string(),
728 });
729 }
730 current_ty = trimmed.to_string();
731 current_desc.clear();
732 } else if !current_ty.is_empty() {
733 current_desc.push(trimmed.to_string());
734 }
735
736 i += 1;
737 }
738
739 if !current_ty.is_empty() {
740 raises.push(RaisesDoc {
741 ty: current_ty,
742 description: current_desc.join(" ").trim().to_string(),
743 });
744 }
745
746 (raises, i)
747}
748
749fn parse_numpy_examples(lines: &[&str], start: usize) -> (Vec<String>, usize) {
751 parse_google_examples(lines, start)
753}
754
755impl ParsedDocstring {
756 pub fn empty() -> Self {
758 Self {
759 summary: None,
760 description: None,
761 params: Vec::new(),
762 returns: None,
763 raises: Vec::new(),
764 examples: Vec::new(),
765 }
766 }
767
768 pub fn is_empty(&self) -> bool {
770 self.summary.is_none()
771 && self.description.is_none()
772 && self.params.is_empty()
773 && self.returns.is_none()
774 && self.raises.is_empty()
775 && self.examples.is_empty()
776 }
777}
778
779pub fn parse_rust_doc(doc: &str) -> ParsedDocstring {
796 let doc = doc.trim();
797 if doc.is_empty() {
798 return ParsedDocstring::empty();
799 }
800
801 let lines: Vec<&str> = doc.lines().collect();
802
803 let (summary, description, section_start) = extract_rust_summary(&lines);
805
806 let mut params = Vec::new();
807 let mut returns = None;
808 let mut raises = Vec::new();
809 let mut examples = Vec::new();
810 let mut safety_notes = Vec::new();
811
812 let mut i = section_start;
814 while i < lines.len() {
815 let line = lines[i].trim();
816
817 if let Some(section_name) = parse_markdown_header(line) {
819 let section_lower = section_name.to_lowercase();
820 match section_lower.as_str() {
821 "arguments" | "parameters" | "args" | "params" => {
822 let (parsed, next_i) = parse_rust_arguments(&lines, i + 1);
823 params = parsed;
824 i = next_i;
825 }
826 "returns" | "return" => {
827 let (parsed, next_i) = parse_rust_returns(&lines, i + 1);
828 returns = parsed;
829 i = next_i;
830 }
831 "errors" | "error" => {
832 let (parsed, next_i) = parse_rust_errors(&lines, i + 1, "Error");
833 raises.extend(parsed);
834 i = next_i;
835 }
836 "panics" | "panic" => {
837 let (parsed, next_i) = parse_rust_errors(&lines, i + 1, "Panic");
838 raises.extend(parsed);
839 i = next_i;
840 }
841 "safety" => {
842 let (notes, next_i) = parse_rust_section_text(&lines, i + 1);
843 safety_notes.push(notes);
844 i = next_i;
845 }
846 "examples" | "example" => {
847 let (parsed, next_i) = parse_rust_examples(&lines, i + 1);
848 examples = parsed;
849 i = next_i;
850 }
851 _ => {
852 i += 1;
854 }
855 }
856 } else {
857 i += 1;
858 }
859 }
860
861 let final_description = if safety_notes.is_empty() {
863 description
864 } else {
865 let safety_text = format!("\n\n# Safety\n{}", safety_notes.join("\n"));
866 match description {
867 Some(desc) => Some(format!("{}{}", desc, safety_text)),
868 None => Some(safety_text.trim_start().to_string()),
869 }
870 };
871
872 ParsedDocstring {
873 summary,
874 description: final_description,
875 params,
876 returns,
877 raises,
878 examples,
879 }
880}
881
882fn extract_rust_summary(lines: &[&str]) -> (Option<String>, Option<String>, usize) {
884 if lines.is_empty() {
885 return (None, None, 0);
886 }
887
888 let mut summary_lines = Vec::new();
889 let mut description_lines = Vec::new();
890 let mut in_description = false;
891 let mut i = 0;
892
893 while i < lines.len() {
894 let line = lines[i].trim();
895
896 if parse_markdown_header(line).is_some() {
898 break;
899 }
900
901 if line.is_empty() {
903 if !summary_lines.is_empty() {
904 in_description = true;
905 }
906 i += 1;
907 continue;
908 }
909
910 if in_description {
911 description_lines.push(line);
912 } else {
913 summary_lines.push(line);
914 }
915 i += 1;
916 }
917
918 let summary = if summary_lines.is_empty() {
919 None
920 } else {
921 Some(summary_lines.join(" "))
922 };
923
924 let description = if description_lines.is_empty() {
925 None
926 } else {
927 Some(description_lines.join("\n"))
928 };
929
930 (summary, description, i)
931}
932
933fn parse_markdown_header(line: &str) -> Option<&str> {
935 let trimmed = line.trim();
936
937 if let Some(rest) = trimmed.strip_prefix("### ") {
939 Some(rest.trim())
940 } else if let Some(rest) = trimmed.strip_prefix("## ") {
941 Some(rest.trim())
942 } else if let Some(rest) = trimmed.strip_prefix("# ") {
943 Some(rest.trim())
944 } else {
945 None
946 }
947}
948
949fn parse_rust_arguments(lines: &[&str], start: usize) -> (Vec<ParamDoc>, usize) {
961 let mut params = Vec::new();
962 let mut i = start;
963 let mut current_name = String::new();
964 let mut current_desc = Vec::new();
965
966 while i < lines.len() {
967 let line = lines[i];
968 let trimmed = line.trim();
969
970 if parse_markdown_header(trimmed).is_some() {
972 break;
973 }
974
975 if trimmed.is_empty() {
977 if !current_name.is_empty() {
978 params.push(ParamDoc {
979 name: current_name.clone(),
980 ty: None,
981 description: current_desc.join(" ").trim().to_string(),
982 });
983 current_name.clear();
984 current_desc.clear();
985 }
986 i += 1;
987 continue;
988 }
989
990 if let Some(param) = parse_rust_param_line(trimmed) {
992 if !current_name.is_empty() {
994 params.push(ParamDoc {
995 name: current_name,
996 ty: None,
997 description: current_desc.join(" ").trim().to_string(),
998 });
999 }
1000 current_name = param.0;
1001 current_desc = vec![param.1];
1002 } else if !current_name.is_empty()
1003 && (trimmed.starts_with(' ') || !trimmed.starts_with('*') && !trimmed.starts_with('-'))
1004 {
1005 current_desc.push(trimmed.to_string());
1007 }
1008
1009 i += 1;
1010 }
1011
1012 if !current_name.is_empty() {
1014 params.push(ParamDoc {
1015 name: current_name,
1016 ty: None,
1017 description: current_desc.join(" ").trim().to_string(),
1018 });
1019 }
1020
1021 (params, i)
1022}
1023
1024fn parse_rust_param_line(line: &str) -> Option<(String, String)> {
1027 let trimmed = line.trim();
1028
1029 if !trimmed.starts_with('*') && !trimmed.starts_with('-') {
1031 return None;
1032 }
1033
1034 let rest = trimmed[1..].trim();
1035
1036 if rest.starts_with('`')
1038 && let Some(end_tick) = rest[1..].find('`')
1039 {
1040 let name = rest[1..end_tick + 1].to_string();
1041 let after_name = rest[end_tick + 2..].trim();
1042
1043 let desc = if let Some(rest) = after_name
1045 .strip_prefix('-')
1046 .or_else(|| after_name.strip_prefix(':'))
1047 {
1048 rest.trim().to_string()
1049 } else {
1050 after_name.to_string()
1051 };
1052
1053 return Some((name, desc));
1054 }
1055
1056 if let Some(sep_pos) = rest.find(" - ") {
1058 let name = rest[..sep_pos].trim().to_string();
1059 let desc = rest[sep_pos + 3..].trim().to_string();
1060 return Some((name, desc));
1061 }
1062
1063 if let Some(sep_pos) = rest.find(':') {
1065 let name = rest[..sep_pos].trim().to_string();
1066 let desc = rest[sep_pos + 1..].trim().to_string();
1067 return Some((name, desc));
1068 }
1069
1070 None
1071}
1072
1073fn parse_rust_returns(lines: &[&str], start: usize) -> (Option<ReturnDoc>, usize) {
1075 let (text, next_i) = parse_rust_section_text(lines, start);
1076
1077 if text.is_empty() {
1078 return (None, next_i);
1079 }
1080
1081 (
1082 Some(ReturnDoc {
1083 ty: None,
1084 description: text,
1085 }),
1086 next_i,
1087 )
1088}
1089
1090fn parse_rust_errors(lines: &[&str], start: usize, error_kind: &str) -> (Vec<RaisesDoc>, usize) {
1092 let mut raises = Vec::new();
1093 let mut i = start;
1094 let mut current_ty = String::new();
1095 let mut current_desc = Vec::new();
1096
1097 while i < lines.len() {
1098 let line = lines[i];
1099 let trimmed = line.trim();
1100
1101 if parse_markdown_header(trimmed).is_some() {
1103 break;
1104 }
1105
1106 if trimmed.is_empty() {
1107 if !current_ty.is_empty() || !current_desc.is_empty() {
1108 raises.push(RaisesDoc {
1109 ty: if current_ty.is_empty() {
1110 error_kind.to_string()
1111 } else {
1112 current_ty.clone()
1113 },
1114 description: current_desc.join(" ").trim().to_string(),
1115 });
1116 current_ty.clear();
1117 current_desc.clear();
1118 }
1119 i += 1;
1120 continue;
1121 }
1122
1123 if trimmed.starts_with('*') || trimmed.starts_with('-') {
1125 if !current_ty.is_empty() || !current_desc.is_empty() {
1127 raises.push(RaisesDoc {
1128 ty: if current_ty.is_empty() {
1129 error_kind.to_string()
1130 } else {
1131 current_ty
1132 },
1133 description: current_desc.join(" ").trim().to_string(),
1134 });
1135 }
1136
1137 let rest = trimmed[1..].trim();
1138
1139 if let Some(after_tick) = rest.strip_prefix('`') {
1141 if let Some(end_tick) = after_tick.find('`') {
1142 current_ty = after_tick[..end_tick].to_string();
1143 let after = after_tick[end_tick + 1..].trim();
1144 current_desc = vec![
1145 after
1146 .trim_start_matches('-')
1147 .trim_start_matches(':')
1148 .trim()
1149 .to_string(),
1150 ];
1151 } else {
1152 current_ty = error_kind.to_string();
1153 current_desc = vec![rest.to_string()];
1154 }
1155 } else {
1156 current_ty = error_kind.to_string();
1157 current_desc = vec![rest.to_string()];
1158 }
1159 } else if !current_desc.is_empty() {
1160 current_desc.push(trimmed.to_string());
1162 } else {
1163 current_ty = error_kind.to_string();
1165 current_desc.push(trimmed.to_string());
1166 }
1167
1168 i += 1;
1169 }
1170
1171 if !current_ty.is_empty() || !current_desc.is_empty() {
1173 raises.push(RaisesDoc {
1174 ty: if current_ty.is_empty() {
1175 error_kind.to_string()
1176 } else {
1177 current_ty
1178 },
1179 description: current_desc.join(" ").trim().to_string(),
1180 });
1181 }
1182
1183 (raises, i)
1184}
1185
1186fn parse_rust_examples(lines: &[&str], start: usize) -> (Vec<String>, usize) {
1188 let mut examples = Vec::new();
1189 let mut current_example = Vec::new();
1190 let mut in_code_block = false;
1191 let mut i = start;
1192
1193 while i < lines.len() {
1194 let line = lines[i];
1195 let trimmed = line.trim();
1196
1197 if !in_code_block && parse_markdown_header(trimmed).is_some() {
1199 break;
1200 }
1201
1202 if trimmed.starts_with("```") {
1204 in_code_block = !in_code_block;
1205 current_example.push(line.to_string());
1206 i += 1;
1207 continue;
1208 }
1209
1210 if trimmed.is_empty() && !in_code_block {
1212 if !current_example.is_empty() {
1213 examples.push(current_example.join("\n"));
1214 current_example.clear();
1215 }
1216 i += 1;
1217 continue;
1218 }
1219
1220 current_example.push(line.to_string());
1221 i += 1;
1222 }
1223
1224 if !current_example.is_empty() {
1225 examples.push(current_example.join("\n"));
1226 }
1227
1228 (examples, i)
1229}
1230
1231fn parse_rust_section_text(lines: &[&str], start: usize) -> (String, usize) {
1233 let mut text_lines = Vec::new();
1234 let mut i = start;
1235
1236 while i < lines.len() {
1237 let line = lines[i];
1238 let trimmed = line.trim();
1239
1240 if parse_markdown_header(trimmed).is_some() {
1242 break;
1243 }
1244
1245 if trimmed.is_empty() && text_lines.is_empty() {
1247 i += 1;
1248 continue;
1249 }
1250
1251 text_lines.push(trimmed);
1252 i += 1;
1253 }
1254
1255 while text_lines.last().map(|s| s.is_empty()).unwrap_or(false) {
1257 text_lines.pop();
1258 }
1259
1260 (text_lines.join(" ").trim().to_string(), i)
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use super::*;
1266
1267 #[test]
1268 fn test_parse_empty() {
1269 let result = parse_docstring("");
1270 assert!(result.is_empty());
1271 }
1272
1273 #[test]
1274 fn test_parse_summary_only() {
1275 let docstring = "A simple summary.";
1276 let result = parse_docstring(docstring);
1277
1278 assert_eq!(result.summary, Some("A simple summary.".to_string()));
1279 assert!(result.description.is_none());
1280 assert!(result.params.is_empty());
1281 }
1282
1283 #[test]
1284 fn test_parse_summary_and_description() {
1285 let docstring = "A short summary.
1286
1287This is a longer description that spans
1288multiple lines and provides more detail.";
1289
1290 let result = parse_docstring(docstring);
1291
1292 assert_eq!(result.summary, Some("A short summary.".to_string()));
1293 assert!(result.description.is_some());
1294 assert!(
1295 result
1296 .description
1297 .as_ref()
1298 .unwrap()
1299 .contains("longer description")
1300 );
1301 }
1302
1303 #[test]
1304 fn test_parse_google_args() {
1305 let docstring = "Do something.
1306
1307Args:
1308 name: The name of the thing.
1309 value (int): The value to use.
1310 optional: An optional parameter that
1311 spans multiple lines.
1312";
1313
1314 let result = parse_docstring(docstring);
1315
1316 assert_eq!(result.summary, Some("Do something.".to_string()));
1317 assert_eq!(result.params.len(), 3);
1318
1319 assert_eq!(result.params[0].name, "name");
1320 assert!(result.params[0].ty.is_none());
1321 assert_eq!(result.params[0].description, "The name of the thing.");
1322
1323 assert_eq!(result.params[1].name, "value");
1324 assert_eq!(result.params[1].ty, Some("int".to_string()));
1325 assert_eq!(result.params[1].description, "The value to use.");
1326
1327 assert_eq!(result.params[2].name, "optional");
1328 assert!(result.params[2].description.contains("multiple lines"));
1329 }
1330
1331 #[test]
1332 fn test_parse_google_returns() {
1333 let docstring = "Calculate result.
1334
1335Returns:
1336 The calculated result as an integer.
1337";
1338
1339 let result = parse_docstring(docstring);
1340
1341 assert!(result.returns.is_some());
1342 let ret = result.returns.unwrap();
1343 assert!(ret.description.contains("calculated result"));
1344 }
1345
1346 #[test]
1347 fn test_parse_google_returns_with_type() {
1348 let docstring = "Get value.
1349
1350Returns:
1351 int: The integer value.
1352";
1353
1354 let result = parse_docstring(docstring);
1355
1356 assert!(result.returns.is_some());
1357 let ret = result.returns.unwrap();
1358 assert_eq!(ret.ty, Some("int".to_string()));
1359 assert_eq!(ret.description, "The integer value.");
1360 }
1361
1362 #[test]
1363 fn test_parse_google_raises() {
1364 let docstring = "Do dangerous thing.
1365
1366Raises:
1367 ValueError: If the value is invalid.
1368 RuntimeError: If something goes wrong
1369 during execution.
1370";
1371
1372 let result = parse_docstring(docstring);
1373
1374 assert_eq!(result.raises.len(), 2);
1375 assert_eq!(result.raises[0].ty, "ValueError");
1376 assert!(result.raises[0].description.contains("invalid"));
1377 assert_eq!(result.raises[1].ty, "RuntimeError");
1378 assert!(result.raises[1].description.contains("execution"));
1379 }
1380
1381 #[test]
1382 fn test_parse_google_examples() {
1383 let docstring = "Do something.
1384
1385Example:
1386 >>> x = do_something()
1387 >>> print(x)
1388 42
1389";
1390
1391 let result = parse_docstring(docstring);
1392
1393 assert_eq!(result.examples.len(), 1);
1394 assert!(result.examples[0].contains(">>> x = do_something()"));
1395 }
1396
1397 #[test]
1398 fn test_parse_google_examples_with_code_fence() {
1399 let docstring = r#"A data processing pipeline.
1402
1403Example:
1404 ```python
1405 from separate_bindings import Pipeline, DataBatch
1406
1407 pipeline = Pipeline("etl")
1408 pipeline.add_stage("transform", lambda batch: batch)
1409
1410 result = pipeline.run(DataBatch.from_dicts([{"a": 1}]))
1411 print(f"Processed {result.rows_out} rows")
1412 ```
1413"#;
1414
1415 let result = parse_docstring(docstring);
1416
1417 assert_eq!(result.examples.len(), 1);
1419 assert!(result.examples[0].contains("```python"));
1421 assert!(result.examples[0].contains("```"));
1422 assert!(result.examples[0].contains("Pipeline"));
1424 assert!(result.examples[0].contains("rows_out"));
1425 }
1426
1427 #[test]
1428 fn test_parse_google_full() {
1429 let docstring = "Create a new task runner.
1430
1431Args:
1432 max_parallel: Maximum number of concurrent tasks (default: 4).
1433
1434Returns:
1435 A new Runner instance.
1436
1437Raises:
1438 RuntimeError: If initialization fails.
1439
1440Example:
1441 >>> runner = Runner(max_parallel=8)
1442";
1443
1444 let result = parse_docstring(docstring);
1445
1446 assert_eq!(
1447 result.summary,
1448 Some("Create a new task runner.".to_string())
1449 );
1450 assert_eq!(result.params.len(), 1);
1451 assert_eq!(result.params[0].name, "max_parallel");
1452 assert!(result.returns.is_some());
1453 assert_eq!(result.raises.len(), 1);
1454 assert_eq!(result.examples.len(), 1);
1455 }
1456
1457 #[test]
1458 fn test_parse_numpy_style() {
1459 let docstring = "Calculate the mean.
1460
1461Parameters
1462----------
1463values : array-like
1464 The values to average.
1465weights : array-like, optional
1466 Optional weights.
1467
1468Returns
1469-------
1470float
1471 The weighted mean.
1472
1473Raises
1474------
1475ValueError
1476 If arrays have different lengths.
1477";
1478
1479 let result = parse_docstring(docstring);
1480
1481 assert_eq!(result.summary, Some("Calculate the mean.".to_string()));
1482
1483 assert_eq!(result.params.len(), 2);
1484 assert_eq!(result.params[0].name, "values");
1485 assert_eq!(result.params[0].ty, Some("array-like".to_string()));
1486 assert_eq!(result.params[1].name, "weights");
1487
1488 assert!(result.returns.is_some());
1489 let ret = result.returns.unwrap();
1490 assert_eq!(ret.ty, Some("float".to_string()));
1491
1492 assert_eq!(result.raises.len(), 1);
1493 assert_eq!(result.raises[0].ty, "ValueError");
1494 }
1495
1496 #[test]
1497 fn test_parse_rust_docstring() {
1498 let docstring = "Create a new task.
1500
1501Args:
1502 name: The unique identifier for this task.
1503 description: Optional human-readable description.
1504
1505Returns:
1506 A new Task instance.";
1507
1508 let result = parse_docstring(docstring);
1509
1510 assert_eq!(result.summary, Some("Create a new task.".to_string()));
1511 assert_eq!(result.params.len(), 2);
1512 assert!(result.returns.is_some());
1513 }
1514
1515 #[test]
1516 fn test_detect_google_style() {
1517 assert_eq!(
1518 detect_style("Summary.\n\nArgs:\n x: value"),
1519 DocstringStyle::Google
1520 );
1521 assert_eq!(
1522 detect_style("Summary.\n\nReturns:\n value"),
1523 DocstringStyle::Google
1524 );
1525 }
1526
1527 #[test]
1528 fn test_detect_numpy_style() {
1529 assert_eq!(
1530 detect_style("Summary.\n\nParameters\n----------\n"),
1531 DocstringStyle::NumPy
1532 );
1533 }
1534
1535 #[test]
1536 fn test_detect_plain_style() {
1537 assert_eq!(
1538 detect_style("Just a simple docstring."),
1539 DocstringStyle::Plain
1540 );
1541 }
1542
1543 #[test]
1544 fn test_parse_scheduler_docstring() {
1545 let docstring = r#"A task scheduler that runs tasks on configured schedules.
1547
1548The scheduler supports both interval-based and cron-based scheduling.
1549Tasks are registered using the `@scheduler.task()` decorator.
1550
1551Attributes:
1552 tasks: Dictionary of registered tasks by name.
1553 running: Whether the scheduler is currently running.
1554
1555Example:
1556 >>> scheduler = Scheduler()
1557 >>> @scheduler.task(every(seconds=30))
1558 ... def heartbeat():
1559 ... print("alive")
1560 >>> scheduler.run()"#;
1561
1562 let result = parse_docstring(docstring);
1563
1564 assert!(result.summary.is_some());
1565 assert!(result.summary.as_ref().unwrap().contains("task scheduler"));
1566 assert!(result.description.is_some());
1567 assert_eq!(result.examples.len(), 1);
1568 }
1569
1570 #[test]
1571 fn test_parse_fixture_method_docstring() {
1572 let docstring = r#"Decorator to register a function as a scheduled task.
1573
1574Args:
1575 schedule: When to run the task (use `every()` or `cron()`).
1576 name: Optional task name. Defaults to the function name.
1577 max_retries: Number of retry attempts on failure.
1578 timeout_seconds: Maximum execution time before killing the task.
1579
1580Returns:
1581 A decorator that registers the function.
1582
1583Raises:
1584 ValueError: If a task with this name already exists.
1585
1586Example:
1587 >>> @scheduler.task(every(hours=1), max_retries=3)
1588 ... def sync_data():
1589 ... external_api.sync()"#;
1590
1591 let result = parse_docstring(docstring);
1592
1593 assert_eq!(result.params.len(), 4);
1594 assert_eq!(result.params[0].name, "schedule");
1595 assert!(result.returns.is_some());
1596 assert_eq!(result.raises.len(), 1);
1597 assert_eq!(result.raises[0].ty, "ValueError");
1598 assert_eq!(result.examples.len(), 1);
1599 }
1600
1601 #[test]
1606 fn test_parse_rust_doc_empty() {
1607 let result = parse_rust_doc("");
1608 assert!(result.is_empty());
1609 }
1610
1611 #[test]
1612 fn test_parse_rust_doc_summary_only() {
1613 let doc = "Returns the length of the string.";
1614 let result = parse_rust_doc(doc);
1615
1616 assert_eq!(
1617 result.summary,
1618 Some("Returns the length of the string.".to_string())
1619 );
1620 assert!(result.description.is_none());
1621 }
1622
1623 #[test]
1624 fn test_parse_rust_doc_summary_and_description() {
1625 let doc = "Returns the length of the string.
1626
1627This is a longer description that provides
1628more detail about how the function works.";
1629
1630 let result = parse_rust_doc(doc);
1631
1632 assert_eq!(
1633 result.summary,
1634 Some("Returns the length of the string.".to_string())
1635 );
1636 assert!(result.description.is_some());
1637 assert!(
1638 result
1639 .description
1640 .as_ref()
1641 .unwrap()
1642 .contains("longer description")
1643 );
1644 }
1645
1646 #[test]
1647 fn test_parse_rust_doc_arguments() {
1648 let doc = "Creates a new buffer.
1649
1650# Arguments
1651
1652* `capacity` - The initial capacity of the buffer
1653* `fill` - The value to fill the buffer with";
1654
1655 let result = parse_rust_doc(doc);
1656
1657 assert_eq!(result.params.len(), 2);
1658 assert_eq!(result.params[0].name, "capacity");
1659 assert!(result.params[0].description.contains("initial capacity"));
1660 assert_eq!(result.params[1].name, "fill");
1661 }
1662
1663 #[test]
1664 fn test_parse_rust_doc_arguments_backticks() {
1665 let doc = "Process data.
1666
1667# Arguments
1668
1669* `data` - The data to process
1670* `options` - Processing options";
1671
1672 let result = parse_rust_doc(doc);
1673
1674 assert_eq!(result.params.len(), 2);
1675 assert_eq!(result.params[0].name, "data");
1676 assert_eq!(result.params[1].name, "options");
1677 }
1678
1679 #[test]
1680 fn test_parse_rust_doc_returns() {
1681 let doc = "Computes the hash.
1682
1683# Returns
1684
1685The computed hash value as a 64-bit integer.";
1686
1687 let result = parse_rust_doc(doc);
1688
1689 assert!(result.returns.is_some());
1690 let ret = result.returns.unwrap();
1691 assert!(ret.description.contains("64-bit integer"));
1692 }
1693
1694 #[test]
1695 fn test_parse_rust_doc_errors() {
1696 let doc = "Opens a file.
1697
1698# Errors
1699
1700Returns an error if the file does not exist or
1701cannot be opened.";
1702
1703 let result = parse_rust_doc(doc);
1704
1705 assert_eq!(result.raises.len(), 1);
1706 assert_eq!(result.raises[0].ty, "Error");
1707 assert!(result.raises[0].description.contains("file does not exist"));
1708 }
1709
1710 #[test]
1711 fn test_parse_rust_doc_errors_with_types() {
1712 let doc = "Parses the input.
1713
1714# Errors
1715
1716* `ParseError` - If the input is malformed
1717* `IoError` - If reading fails";
1718
1719 let result = parse_rust_doc(doc);
1720
1721 assert_eq!(result.raises.len(), 2);
1722 assert_eq!(result.raises[0].ty, "ParseError");
1723 assert!(result.raises[0].description.contains("malformed"));
1724 assert_eq!(result.raises[1].ty, "IoError");
1725 }
1726
1727 #[test]
1728 fn test_parse_rust_doc_panics() {
1729 let doc = "Gets the element.
1730
1731# Panics
1732
1733Panics if the index is out of bounds.";
1734
1735 let result = parse_rust_doc(doc);
1736
1737 assert_eq!(result.raises.len(), 1);
1738 assert_eq!(result.raises[0].ty, "Panic");
1739 assert!(result.raises[0].description.contains("out of bounds"));
1740 }
1741
1742 #[test]
1743 fn test_parse_rust_doc_examples() {
1744 let doc = r#"Creates a new instance.
1745
1746# Examples
1747
1748```rust
1749let x = MyType::new();
1750assert!(x.is_valid());
1751```"#;
1752
1753 let result = parse_rust_doc(doc);
1754
1755 assert_eq!(result.examples.len(), 1);
1756 assert!(result.examples[0].contains("let x = MyType::new()"));
1757 assert!(result.examples[0].contains("```"));
1758 }
1759
1760 #[test]
1761 fn test_parse_rust_doc_safety() {
1762 let doc = "Dereferences a raw pointer.
1763
1764# Safety
1765
1766The pointer must be valid and properly aligned.
1767The caller must ensure the pointed-to data is valid.";
1768
1769 let result = parse_rust_doc(doc);
1770
1771 assert!(result.description.is_some());
1772 assert!(result.description.as_ref().unwrap().contains("Safety"));
1773 assert!(
1774 result
1775 .description
1776 .as_ref()
1777 .unwrap()
1778 .contains("pointer must be valid")
1779 );
1780 }
1781
1782 #[test]
1783 fn test_parse_rust_doc_full() {
1784 let doc = r#"Processes the input data and returns the result.
1785
1786This function performs complex processing on the input,
1787applying various transformations.
1788
1789# Arguments
1790
1791* `input` - The input data to process
1792* `config` - Configuration options
1793
1794# Returns
1795
1796The processed result, or an error if processing fails.
1797
1798# Errors
1799
1800* `InvalidInput` - If the input is malformed
1801* `ProcessingError` - If processing fails
1802
1803# Panics
1804
1805Panics if the config is invalid.
1806
1807# Examples
1808
1809```rust
1810let result = process(&data, &config)?;
1811println!("{:?}", result);
1812```"#;
1813
1814 let result = parse_rust_doc(doc);
1815
1816 assert!(result.summary.is_some());
1817 assert!(
1818 result
1819 .summary
1820 .as_ref()
1821 .unwrap()
1822 .contains("Processes the input")
1823 );
1824 assert!(result.description.is_some());
1825 assert_eq!(result.params.len(), 2);
1826 assert_eq!(result.params[0].name, "input");
1827 assert!(result.returns.is_some());
1828 assert_eq!(result.raises.len(), 3); assert_eq!(result.examples.len(), 1);
1830 }
1831
1832 #[test]
1833 fn test_parse_rust_doc_no_sections() {
1834 let doc = "A simple function that does something useful.";
1836 let result = parse_rust_doc(doc);
1837
1838 assert_eq!(
1839 result.summary,
1840 Some("A simple function that does something useful.".to_string())
1841 );
1842 assert!(result.params.is_empty());
1843 assert!(result.returns.is_none());
1844 }
1845
1846 #[test]
1847 fn test_parse_markdown_header() {
1848 assert_eq!(parse_markdown_header("# Arguments"), Some("Arguments"));
1849 assert_eq!(parse_markdown_header("## Returns"), Some("Returns"));
1850 assert_eq!(parse_markdown_header("### Examples"), Some("Examples"));
1851 assert_eq!(parse_markdown_header("Not a header"), None);
1852 assert_eq!(parse_markdown_header("#NoSpace"), None);
1853 }
1854}