1use std::any::Any;
7use std::collections::HashMap;
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11
12use crate::perl_config::{get_perl_version, PerlConfigError};
13
14use serde::{Deserialize, Serialize};
15
16use crate::intern::StringInterner;
17use crate::macro_def::{MacroKind, MacroTable};
18use crate::preprocessor::CommentCallback;
19use crate::source::FileId;
20use crate::token::Comment;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
24pub enum Nullability {
25 NotNull,
27 Nullable,
29 #[default]
31 Unspecified,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ApidocArg {
37 pub nullability: Nullability,
39 pub non_zero: bool,
41 pub ty: String,
43 pub name: String,
45 pub raw: String,
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct ApidocFlags {
52 pub api: bool, pub core_only: bool, pub ext_visible: bool, pub exported: bool, pub not_exported: bool, pub perl_prefix: bool, pub static_fn: bool, pub static_perl: bool, pub inline: bool, pub force_inline: bool, pub is_macro: bool, pub custom_macro: bool, pub no_thread_ctx: bool, pub documented: bool, pub hide_docs: bool, pub no_usage: bool, pub allocates: bool, pub pure: bool, pub return_required: bool, pub no_return: bool, pub deprecated: bool, pub compat: bool, pub format_string: bool, pub varargs_no_fmt: bool, pub no_args: bool, pub unorthodox: bool, pub experimental: bool, pub is_typedef: bool, pub raw: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ApidocEntry {
97 pub flags: ApidocFlags,
99 pub return_type: Option<String>,
101 pub name: String,
103 pub args: Vec<ApidocArg>,
105 pub source_file: Option<String>,
107 pub line_number: Option<usize>,
109 #[serde(default)]
111 pub has_token_pasting: bool,
112}
113
114#[derive(Debug, Default, Serialize, Deserialize)]
116pub struct ApidocDict {
117 entries: HashMap<String, ApidocEntry>,
118}
119
120impl ApidocFlags {
121 pub fn parse(flags: &str) -> Self {
123 let mut result = Self {
124 raw: flags.to_string(),
125 ..Default::default()
126 };
127
128 for ch in flags.chars() {
129 match ch {
130 'A' => result.api = true,
132 'C' => result.core_only = true,
133 'E' => result.ext_visible = true,
134 'X' => result.exported = true,
135 'e' => result.not_exported = true,
136
137 'p' => result.perl_prefix = true,
139 'S' => result.static_fn = true,
140 's' => result.static_perl = true,
141 'i' => result.inline = true,
142 'I' => result.force_inline = true,
143 'm' => result.is_macro = true,
144 'M' => result.custom_macro = true,
145 'T' => result.no_thread_ctx = true,
146
147 'd' => result.documented = true,
149 'h' => result.hide_docs = true,
150 'U' => result.no_usage = true,
151
152 'a' => {
154 result.allocates = true;
155 result.return_required = true; }
157 'P' => {
158 result.pure = true;
159 result.return_required = true; }
161 'R' => result.return_required = true,
162 'r' => result.no_return = true,
163 'D' => result.deprecated = true,
164 'b' => result.compat = true,
165
166 'f' => result.format_string = true,
168 'F' => result.varargs_no_fmt = true,
169 'n' => result.no_args = true,
170 'u' => result.unorthodox = true,
171 'x' => result.experimental = true,
172 'y' => result.is_typedef = true,
173
174 'G' | 'N' | 'O' | 'o' | 'v' | 'W' | ';' | '#' | '?' => {}
176
177 _ => {}
179 }
180 }
181
182 result
183 }
184}
185
186impl ApidocArg {
187 pub fn parse(arg: &str) -> Option<Self> {
189 let raw = arg.to_string();
190 let trimmed = arg.trim();
191
192 if trimmed.is_empty() {
193 return None;
194 }
195
196 let mut nullability = Nullability::Unspecified;
197 let mut non_zero = false;
198 let mut remaining = trimmed;
199
200 loop {
202 if remaining.starts_with("NN ") {
203 nullability = Nullability::NotNull;
204 remaining = remaining[3..].trim_start();
205 } else if remaining.starts_with("NULLOK ") {
206 nullability = Nullability::Nullable;
207 remaining = remaining[7..].trim_start();
208 } else if remaining.starts_with("NZ ") {
209 non_zero = true;
210 remaining = remaining[3..].trim_start();
211 } else {
212 break;
213 }
214 }
215
216 let (ty, name) = Self::split_type_and_name(remaining);
220
221 Some(Self {
222 nullability,
223 non_zero,
224 ty,
225 name,
226 raw,
227 })
228 }
229
230 fn split_type_and_name(s: &str) -> (String, String) {
232 let s = s.trim();
233
234 if s == "..." {
236 return ("...".to_string(), String::new());
237 }
238
239 if s == "type" || s == "cast" || s == "SP" || s == "block"
242 || s == "number" || s == "token" || s.starts_with('"')
243 {
244 return (s.to_string(), String::new());
245 }
246
247 let bytes = s.as_bytes();
254 let mut name_end = bytes.len();
255 let mut name_start;
256
257 while name_end > 0 && bytes[name_end - 1].is_ascii_whitespace() {
259 name_end -= 1;
260 }
261
262 name_start = name_end;
264 while name_start > 0 {
265 let ch = bytes[name_start - 1];
266 if ch.is_ascii_alphanumeric() || ch == b'_' {
267 name_start -= 1;
268 } else {
269 break;
270 }
271 }
272
273 if name_start == name_end {
274 return (s.to_string(), String::new());
276 }
277
278 let name = &s[name_start..name_end];
279 let ty = s[..name_start].trim_end();
280
281 if ty.is_empty() {
287 return (name.to_string(), String::new());
288 }
289
290 let type_keywords = ["const", "struct", "union", "enum", "unsigned", "signed", "volatile"];
292 let ty_lower = ty.to_lowercase();
293 for kw in &type_keywords {
294 if ty_lower == *kw {
295 return (s.to_string(), String::new());
297 }
298 }
299
300 (ty.to_string(), name.to_string())
301 }
302}
303
304impl ApidocEntry {
305 pub fn parse_line(line: &str) -> Option<Self> {
308 let trimmed = line.trim();
309
310 if trimmed.starts_with(": ") || trimmed == ":" || trimmed.is_empty() {
312 return None;
313 }
314
315 Self::parse_fields(trimmed)
316 }
317
318 pub fn parse_apidoc_line(line: &str) -> Option<Self> {
322 let trimmed = line.trim();
323
324 let rest = if let Some(rest) = trimmed.strip_prefix("=for apidoc_item") {
326 rest.trim()
327 } else if let Some(rest) = trimmed.strip_prefix("=for apidoc") {
328 rest.trim()
329 } else {
330 return None;
331 };
332
333 if rest.is_empty() {
334 return None;
335 }
336
337 if rest.contains('|') {
339 Self::parse_fields(rest)
340 } else {
341 Some(Self {
343 flags: ApidocFlags::default(),
344 return_type: None,
345 name: rest.to_string(),
346 args: Vec::new(),
347 source_file: None,
348 line_number: None,
349 has_token_pasting: false,
350 })
351 }
352 }
353
354 fn parse_fields(s: &str) -> Option<Self> {
356 let fields: Vec<&str> = s.split('|').collect();
357
358 if fields.len() < 3 {
359 return None;
360 }
361
362 let flags = ApidocFlags::parse(fields[0].trim());
363 let return_type = {
364 let rt = fields[1].trim();
365 if rt.is_empty() {
366 None
367 } else {
368 Some(rt.to_string())
369 }
370 };
371 let name = fields[2].trim().to_string();
372
373 if name.is_empty() {
374 return None;
375 }
376
377 let args: Vec<ApidocArg> = fields[3..]
378 .iter()
379 .filter_map(|arg| ApidocArg::parse(arg))
380 .collect();
381
382 let has_token_pasting = args.iter().any(|arg| {
385 arg.ty.starts_with('"') && arg.ty.ends_with('"')
386 });
387
388 Some(Self {
389 flags,
390 return_type,
391 name,
392 args,
393 source_file: None,
394 line_number: None,
395 has_token_pasting,
396 })
397 }
398
399 pub fn is_public_api(&self) -> bool {
401 self.flags.api
402 }
403
404 pub fn is_macro(&self) -> bool {
406 self.flags.is_macro
407 }
408
409 pub fn is_inline(&self) -> bool {
411 self.flags.inline || self.flags.force_inline
412 }
413
414 pub fn is_type_param_keyword(ty: &str) -> bool {
419 ty == "type" || ty == "cast"
420 }
421
422 pub fn type_param_indices(&self) -> Vec<usize> {
424 self.args
425 .iter()
426 .enumerate()
427 .filter(|(_, arg)| Self::is_type_param_keyword(&arg.ty))
428 .map(|(i, _)| i)
429 .collect()
430 }
431
432 pub fn returns_type_param(&self) -> bool {
434 self.return_type
435 .as_ref()
436 .map_or(false, |t| Self::is_type_param_keyword(t))
437 }
438
439 pub fn is_generic(&self) -> bool {
441 self.returns_type_param() || !self.type_param_indices().is_empty()
442 }
443
444 pub fn is_literal_string_keyword(ty: &str) -> bool {
449 ty.starts_with('"')
450 }
451
452 pub fn has_token_arg(&self) -> bool {
457 self.args.iter().any(|arg| arg.ty == "token")
458 }
459}
460
461impl ApidocDict {
462 pub fn new() -> Self {
464 Self::default()
465 }
466
467 pub fn insert(&mut self, name: String, entry: ApidocEntry) {
469 self.entries.insert(name, entry);
470 }
471
472 pub fn parse_embed_fnc<P: AsRef<Path>>(path: P) -> io::Result<Self> {
474 let content = fs::read_to_string(path)?;
475 Ok(Self::parse_embed_fnc_str(&content))
476 }
477
478 pub fn parse_embed_fnc_str(content: &str) -> Self {
480 let mut dict = Self::new();
481 let mut continued_line = String::new();
482 let mut line_number = 0usize;
483
484 for line in content.lines() {
485 line_number += 1;
486
487 if line.ends_with('\\') {
489 continued_line.push_str(line.trim_end_matches('\\'));
491 continued_line.push(' ');
492 continue;
493 }
494
495 let full_line = if continued_line.is_empty() {
496 line.to_string()
497 } else {
498 continued_line.push_str(line);
499 let result = continued_line.clone();
500 continued_line.clear();
501 result
502 };
503
504 if let Some(mut entry) = ApidocEntry::parse_line(&full_line) {
505 entry.line_number = Some(line_number);
506 dict.entries.insert(entry.name.clone(), entry);
507 }
508 }
509
510 dict
511 }
512
513 pub fn parse_header_apidoc<P: AsRef<Path>>(path: P) -> io::Result<Self> {
515 let content = fs::read_to_string(&path)?;
516 let mut dict = Self::parse_header_apidoc_str(&content);
517
518 let path_str = path.as_ref().to_string_lossy().to_string();
520 for entry in dict.entries.values_mut() {
521 entry.source_file = Some(path_str.clone());
522 }
523
524 Ok(dict)
525 }
526
527 pub fn parse_header_apidoc_str(content: &str) -> Self {
529 let mut dict = Self::new();
530 let mut line_number = 0usize;
531
532 for line in content.lines() {
533 line_number += 1;
534
535 if let Some(idx) = line.find("=for apidoc") {
537 let apidoc_part = &line[idx..];
538 if let Some(mut entry) = ApidocEntry::parse_apidoc_line(apidoc_part) {
539 entry.line_number = Some(line_number);
540 dict.entries.insert(entry.name.clone(), entry);
541 }
542 }
543 }
544
545 dict
546 }
547
548 pub fn merge(&mut self, other: Self) {
550 for (name, entry) in other.entries {
551 self.entries.entry(name).or_insert(entry);
552 }
553 }
554
555 pub fn len(&self) -> usize {
557 self.entries.len()
558 }
559
560 pub fn is_empty(&self) -> bool {
562 self.entries.is_empty()
563 }
564
565 pub fn get(&self, name: &str) -> Option<&ApidocEntry> {
567 self.entries.get(name)
568 }
569
570 pub fn get_mut(&mut self, name: &str) -> Option<&mut ApidocEntry> {
572 self.entries.get_mut(name)
573 }
574
575 pub fn iter(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
577 self.entries.iter()
578 }
579
580 pub fn functions(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
582 self.entries.iter().filter(|(_, e)| !e.is_macro())
583 }
584
585 pub fn macros(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
587 self.entries.iter().filter(|(_, e)| e.is_macro())
588 }
589
590 pub fn dump_filtered(&self, filter: &str) {
595 let mut names: Vec<_> = self.entries.keys().collect();
596 names.sort();
597
598 for name in names {
599 if !filter.is_empty() && !name.contains(filter) {
601 continue;
602 }
603
604 if let Some(entry) = self.entries.get(name) {
605 eprintln!("{}:", name);
606 eprintln!(" flags: {}", entry.flags.raw);
607 if let Some(ref ret) = entry.return_type {
608 eprintln!(" return_type: {}", ret);
609 } else {
610 eprintln!(" return_type: (none)");
611 }
612 eprintln!(" args:");
613 for (i, arg) in entry.args.iter().enumerate() {
614 eprintln!(" [{}] {} {} ({:?}{})",
615 i,
616 arg.ty,
617 arg.name,
618 arg.nullability,
619 if arg.non_zero { ", NZ" } else { "" }
620 );
621 }
622 if let Some(ref src) = entry.source_file {
623 eprintln!(" source: {}:{}", src, entry.line_number.unwrap_or(0));
624 }
625 eprintln!();
626 }
627 }
628 }
629
630 pub fn stats(&self) -> ApidocStats {
632 let mut stats = ApidocStats::default();
633
634 for entry in self.entries.values() {
635 if entry.is_macro() {
636 stats.macro_count += 1;
637 } else if entry.is_inline() {
638 stats.inline_count += 1;
639 } else {
640 stats.function_count += 1;
641 }
642
643 if entry.is_public_api() {
644 stats.api_count += 1;
645 }
646 }
647
648 stats.total = self.entries.len();
649 stats
650 }
651
652 pub fn save_json<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
654 let json = serde_json::to_string_pretty(self)
655 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
656 fs::write(path, json)
657 }
658
659 pub fn to_json(&self) -> Result<String, serde_json::Error> {
661 serde_json::to_string_pretty(self)
662 }
663
664 pub fn load_json<P: AsRef<Path>>(path: P) -> io::Result<Self> {
666 let content = fs::read_to_string(path)?;
667 Self::from_json(&content)
668 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
669 }
670
671 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
673 serde_json::from_str(json)
674 }
675
676 pub fn load_auto<P: AsRef<Path>>(path: P) -> io::Result<Self> {
680 let path_ref = path.as_ref();
681 if path_ref.extension().is_some_and(|ext| ext == "json") {
682 Self::load_json(path_ref)
683 } else {
684 Self::parse_embed_fnc(path_ref)
685 }
686 }
687
688 pub fn find_json_for_version<P: AsRef<Path>>(
693 apidoc_dir: P,
694 major: u32,
695 minor: u32,
696 ) -> Option<std::path::PathBuf> {
697 let filename = format!("v{}.{}.json", major, minor);
698 let path = apidoc_dir.as_ref().join(&filename);
699 if path.exists() {
700 Some(path)
701 } else {
702 None
703 }
704 }
705
706 pub fn load_for_perl_version<P: AsRef<Path>>(
712 apidoc_dir: P,
713 major: u32,
714 minor: u32,
715 ) -> io::Result<Self> {
716 let path = Self::find_json_for_version(&apidoc_dir, major, minor).ok_or_else(|| {
717 io::Error::new(
718 io::ErrorKind::NotFound,
719 format!(
720 "{}/v{}.{}.json not found for Perl {}.{}.\n\
721 Please specify --apidoc explicitly or add the JSON file.",
722 apidoc_dir.as_ref().display(),
723 major,
724 minor,
725 major,
726 minor
727 ),
728 )
729 })?;
730 Self::load_json(&path)
731 }
732
733 pub fn expand_type_macros(&mut self, macro_table: &MacroTable, interner: &StringInterner) {
738 for entry in self.entries.values_mut() {
739 if let Some(ref mut return_type) = entry.return_type {
741 *return_type = expand_type_string(return_type, macro_table, interner);
742 }
743 for arg in &mut entry.args {
745 arg.ty = expand_type_string(&arg.ty, macro_table, interner);
746 }
747 }
748 }
749}
750
751fn expand_type_string(
756 type_str: &str,
757 macro_table: &MacroTable,
758 interner: &StringInterner,
759) -> String {
760 let mut result = String::new();
761 let mut chars = type_str.chars().peekable();
762
763 while let Some(c) = chars.next() {
764 if c.is_alphabetic() || c == '_' {
765 let mut ident = String::from(c);
767 while let Some(&nc) = chars.peek() {
768 if nc.is_alphanumeric() || nc == '_' {
769 ident.push(chars.next().unwrap());
770 } else {
771 break;
772 }
773 }
774
775 if let Some(interned) = interner.lookup(&ident) {
777 if let Some(macro_def) = macro_table.get(interned) {
778 if matches!(macro_def.kind, MacroKind::Object) && !macro_def.body.is_empty() {
779 let expanded: String = macro_def
781 .body
782 .iter()
783 .map(|t| t.kind.format(interner))
784 .collect::<Vec<_>>()
785 .join("");
786 result.push_str(&expanded);
787 continue;
788 }
789 }
790 }
791 result.push_str(&ident);
793 } else {
794 result.push(c);
795 }
796 }
797
798 result
799}
800
801#[derive(Debug)]
803pub enum ApidocResolveError {
804 DevelopmentVersion { major: u32, minor: u32 },
806 DirectoryNotFound,
808 JsonNotFound { path: PathBuf, major: u32, minor: u32 },
810 VersionError(PerlConfigError),
812}
813
814impl std::fmt::Display for ApidocResolveError {
815 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
816 match self {
817 ApidocResolveError::DevelopmentVersion { major, minor } => {
818 write!(
819 f,
820 "Perl {}.{} is a development version.\n\
821 Please specify --apidoc explicitly (e.g., --apidoc path/to/embed.fnc)",
822 major, minor
823 )
824 }
825 ApidocResolveError::DirectoryNotFound => {
826 write!(
827 f,
828 "apidoc directory not found.\n\
829 Please specify --apidoc explicitly."
830 )
831 }
832 ApidocResolveError::JsonNotFound { path, major, minor } => {
833 write!(
834 f,
835 "{}/v{}.{}.json not found for Perl {}.{}.\n\
836 Please specify --apidoc explicitly or add the JSON file.",
837 path.display(),
838 major, minor, major, minor
839 )
840 }
841 ApidocResolveError::VersionError(e) => {
842 write!(f, "Failed to get Perl version: {}", e)
843 }
844 }
845 }
846}
847
848impl std::error::Error for ApidocResolveError {
849 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
850 match self {
851 ApidocResolveError::VersionError(e) => Some(e),
852 _ => None,
853 }
854 }
855}
856
857impl From<PerlConfigError> for ApidocResolveError {
858 fn from(e: PerlConfigError) -> Self {
859 ApidocResolveError::VersionError(e)
860 }
861}
862
863pub fn find_apidoc_dir_from(base_dir: Option<&Path>) -> Option<PathBuf> {
872 if let Some(base) = base_dir {
874 let apidoc_dir = base.join("apidoc");
875 if apidoc_dir.is_dir() {
876 return Some(apidoc_dir);
877 }
878 if base.is_dir() && base.file_name().is_some_and(|n| n == "apidoc") {
880 return Some(base.to_path_buf());
881 }
882 }
883
884 if let Some(embedded_dir) = crate::apidoc_data::get_apidoc_dir() {
886 if embedded_dir.is_dir() {
887 return Some(embedded_dir);
888 }
889 }
890
891 if let Ok(exe_path) = std::env::current_exe() {
893 if let Some(exe_dir) = exe_path.parent() {
894 let apidoc_dir = exe_dir.join("apidoc");
895 if apidoc_dir.is_dir() {
896 return Some(apidoc_dir);
897 }
898
899 if let Some(parent_dir) = exe_dir.parent() {
901 let apidoc_dir = parent_dir.join("apidoc");
902 if apidoc_dir.is_dir() {
903 return Some(apidoc_dir);
904 }
905
906 if let Some(grandparent_dir) = parent_dir.parent() {
908 let apidoc_dir = grandparent_dir.join("apidoc");
909 if apidoc_dir.is_dir() {
910 return Some(apidoc_dir);
911 }
912 }
913 }
914 }
915 }
916
917 if let Ok(cwd) = std::env::current_dir() {
919 let apidoc_dir = cwd.join("apidoc");
920 if apidoc_dir.is_dir() {
921 return Some(apidoc_dir);
922 }
923 }
924
925 None
926}
927
928pub fn resolve_apidoc_path(
944 explicit_path: Option<&Path>,
945 auto_mode: bool,
946 apidoc_dir: Option<&Path>,
947) -> Result<Option<PathBuf>, ApidocResolveError> {
948 if let Some(path) = explicit_path {
950 return Ok(Some(path.to_path_buf()));
951 }
952
953 if !auto_mode {
955 return Ok(None);
956 }
957
958 let (major, minor) = get_perl_version()?;
960
961 if minor % 2 == 1 {
963 return Err(ApidocResolveError::DevelopmentVersion { major, minor });
964 }
965
966 let resolved_apidoc_dir = find_apidoc_dir_from(apidoc_dir)
968 .ok_or(ApidocResolveError::DirectoryNotFound)?;
969
970 let json_path = ApidocDict::find_json_for_version(&resolved_apidoc_dir, major, minor)
972 .ok_or_else(|| ApidocResolveError::JsonNotFound {
973 path: resolved_apidoc_dir,
974 major,
975 minor,
976 })?;
977
978 Ok(Some(json_path))
979}
980
981#[derive(Debug, Default, Serialize, Deserialize)]
983pub struct ApidocStats {
984 pub total: usize,
985 pub function_count: usize,
986 pub macro_count: usize,
987 pub inline_count: usize,
988 pub api_count: usize,
989}
990
991pub struct ApidocCollector {
996 entries: HashMap<String, ApidocEntry>,
997 token_type_macros: Vec<String>,
999}
1000
1001impl ApidocCollector {
1002 pub fn new() -> Self {
1004 Self {
1005 entries: HashMap::new(),
1006 token_type_macros: Vec::new(),
1007 }
1008 }
1009
1010 pub fn merge_into(self, dict: &mut ApidocDict) {
1012 if crate::apidoc_patches::is_apidoc_debug_enabled() {
1016 let total = self.entries.len();
1017 let rcpv: Vec<String> = self.entries.iter()
1018 .filter(|(k, _)| k.starts_with("RCPV"))
1019 .map(|(k, e)| format!("{}->{}", k, e.return_type.as_deref().unwrap_or("?")))
1020 .collect();
1021 crate::apidoc_patches::cargo_warning(&format!(
1022 "[apidoc-collector] merge_into: {} entries from inline =for apidoc; \
1023 RCPV-named ({}): {:?}",
1024 total, rcpv.len(), rcpv
1025 ));
1026 }
1027 for (name, entry) in self.entries {
1028 dict.insert(name, entry);
1029 }
1030 }
1031
1032 pub fn len(&self) -> usize {
1034 self.entries.len()
1035 }
1036
1037 pub fn is_empty(&self) -> bool {
1039 self.entries.is_empty()
1040 }
1041
1042 pub fn token_type_macros(&self) -> &[String] {
1047 &self.token_type_macros
1048 }
1049}
1050
1051impl Default for ApidocCollector {
1052 fn default() -> Self {
1053 Self::new()
1054 }
1055}
1056
1057impl CommentCallback for ApidocCollector {
1058 fn on_comment(&mut self, comment: &Comment, _file_id: FileId, _is_target: bool) {
1059 for line in comment.text.lines() {
1062 if let Some(entry) = ApidocEntry::parse_apidoc_line(line) {
1063 if entry.has_token_arg() {
1065 self.token_type_macros.push(entry.name.clone());
1066 }
1067 self.entries.insert(entry.name.clone(), entry);
1068 }
1069 }
1070 }
1071
1072 fn into_any(self: Box<Self>) -> Box<dyn Any> {
1073 self
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080
1081 #[test]
1082 fn test_parse_flags() {
1083 let flags = ApidocFlags::parse("Adp");
1084 assert!(flags.api);
1085 assert!(flags.documented);
1086 assert!(flags.perl_prefix);
1087 assert!(!flags.is_macro);
1088 }
1089
1090 #[test]
1091 fn test_parse_flags_macro() {
1092 let flags = ApidocFlags::parse("ARdm");
1093 assert!(flags.api);
1094 assert!(flags.return_required);
1095 assert!(flags.documented);
1096 assert!(flags.is_macro);
1097 }
1098
1099 #[test]
1100 fn test_parse_flags_allocates_implies_r() {
1101 let flags = ApidocFlags::parse("a");
1102 assert!(flags.allocates);
1103 assert!(flags.return_required);
1104 }
1105
1106 #[test]
1107 fn test_parse_arg_simple() {
1108 let arg = ApidocArg::parse("int method").unwrap();
1109 assert_eq!(arg.nullability, Nullability::Unspecified);
1110 assert!(!arg.non_zero);
1111 assert_eq!(arg.ty, "int");
1112 assert_eq!(arg.name, "method");
1113 }
1114
1115 #[test]
1116 fn test_parse_arg_pointer() {
1117 let arg = ApidocArg::parse("SV *sv").unwrap();
1118 assert_eq!(arg.ty, "SV *");
1119 assert_eq!(arg.name, "sv");
1120 }
1121
1122 #[test]
1123 fn test_parse_arg_not_null() {
1124 let arg = ApidocArg::parse("NN SV *sv").unwrap();
1125 assert_eq!(arg.nullability, Nullability::NotNull);
1126 assert_eq!(arg.ty, "SV *");
1127 assert_eq!(arg.name, "sv");
1128 }
1129
1130 #[test]
1131 fn test_parse_arg_nullok() {
1132 let arg = ApidocArg::parse("NULLOK SV *sv").unwrap();
1133 assert_eq!(arg.nullability, Nullability::Nullable);
1134 assert_eq!(arg.ty, "SV *");
1135 assert_eq!(arg.name, "sv");
1136 }
1137
1138 #[test]
1139 fn test_parse_arg_const_pointer() {
1140 let arg = ApidocArg::parse("NN const char * const name").unwrap();
1141 assert_eq!(arg.nullability, Nullability::NotNull);
1142 assert_eq!(arg.ty, "const char * const");
1143 assert_eq!(arg.name, "name");
1144 }
1145
1146 #[test]
1147 fn test_parse_arg_varargs() {
1148 let arg = ApidocArg::parse("...").unwrap();
1149 assert_eq!(arg.ty, "...");
1150 assert_eq!(arg.name, "");
1151 }
1152
1153 #[test]
1154 fn test_parse_line_simple() {
1155 let entry = ApidocEntry::parse_line("Adp |SV * |av_pop |NN AV *av").unwrap();
1156 assert!(entry.flags.api);
1157 assert!(entry.flags.documented);
1158 assert!(entry.flags.perl_prefix);
1159 assert_eq!(entry.return_type, Some("SV *".to_string()));
1160 assert_eq!(entry.name, "av_pop");
1161 assert_eq!(entry.args.len(), 1);
1162 assert_eq!(entry.args[0].ty, "AV *");
1163 assert_eq!(entry.args[0].name, "av");
1164 assert_eq!(entry.args[0].nullability, Nullability::NotNull);
1165 }
1166
1167 #[test]
1168 fn test_parse_line_comment() {
1169 assert!(ApidocEntry::parse_line(": This is a comment").is_none());
1170 assert!(ApidocEntry::parse_line(":").is_none());
1171 assert!(ApidocEntry::parse_line("").is_none());
1172 }
1173
1174 #[test]
1175 fn test_parse_line_macro() {
1176 let entry = ApidocEntry::parse_line("ARdm |SSize_t|av_tindex |NN AV *av").unwrap();
1177 assert!(entry.flags.is_macro);
1178 assert!(entry.flags.return_required);
1179 assert_eq!(entry.name, "av_tindex");
1180 }
1181
1182 #[test]
1183 fn test_parse_line_multiple_args() {
1184 let entry = ApidocEntry::parse_line(
1185 "Adp |SV * |amagic_call |NN SV *left |NN SV *right |int method |int dir"
1186 ).unwrap();
1187 assert_eq!(entry.args.len(), 4);
1188 assert_eq!(entry.args[0].name, "left");
1189 assert_eq!(entry.args[1].name, "right");
1190 assert_eq!(entry.args[2].name, "method");
1191 assert_eq!(entry.args[3].name, "dir");
1192 }
1193
1194 #[test]
1195 fn test_parse_apidoc_line_name_only() {
1196 let entry = ApidocEntry::parse_apidoc_line("=for apidoc av_pop").unwrap();
1197 assert_eq!(entry.name, "av_pop");
1198 assert!(entry.return_type.is_none());
1199 assert!(entry.args.is_empty());
1200 }
1201
1202 #[test]
1203 fn test_parse_apidoc_line_full() {
1204 let entry = ApidocEntry::parse_apidoc_line(
1205 "=for apidoc Am|char*|SvPV|SV* sv|STRLEN len"
1206 ).unwrap();
1207 assert!(entry.flags.api);
1208 assert!(entry.flags.is_macro);
1209 assert_eq!(entry.return_type, Some("char*".to_string()));
1210 assert_eq!(entry.name, "SvPV");
1211 assert_eq!(entry.args.len(), 2);
1212 }
1213
1214 #[test]
1215 fn test_parse_apidoc_item() {
1216 let entry = ApidocEntry::parse_apidoc_line(
1217 "=for apidoc_item |const char*|SvPV_const|SV* sv|STRLEN len"
1218 ).unwrap();
1219 assert_eq!(entry.return_type, Some("const char*".to_string()));
1220 assert_eq!(entry.name, "SvPV_const");
1221 }
1222
1223 #[test]
1224 fn test_embed_fnc_str() {
1225 let content = r#"
1226: This is a comment
1227Adp |SV * |av_pop |NN AV *av
1228ARdm |SSize_t|av_tindex |NN AV *av
1229"#;
1230 let dict = ApidocDict::parse_embed_fnc_str(content);
1231 assert_eq!(dict.len(), 2);
1232 assert!(dict.get("av_pop").is_some());
1233 assert!(dict.get("av_tindex").is_some());
1234 }
1235
1236 #[test]
1237 fn test_embed_fnc_continuation() {
1238 let content = r#"
1239pr |void |abort_execution|NULLOK SV *msg_sv \
1240 |NN const char * const name
1241"#;
1242 let dict = ApidocDict::parse_embed_fnc_str(content);
1243 assert_eq!(dict.len(), 1);
1244 let entry = dict.get("abort_execution").unwrap();
1245 assert_eq!(entry.args.len(), 2);
1246 assert_eq!(entry.args[0].nullability, Nullability::Nullable);
1247 assert_eq!(entry.args[1].nullability, Nullability::NotNull);
1248 }
1249
1250 #[test]
1251 fn test_header_apidoc_str() {
1252 let content = r#"
1253/*
1254=for apidoc Am|char*|SvPV|SV* sv|STRLEN len
1255
1256Returns a pointer to the string value of the SV.
1257
1258=cut
1259*/
1260"#;
1261 let dict = ApidocDict::parse_header_apidoc_str(content);
1262 assert_eq!(dict.len(), 1);
1263 assert!(dict.get("SvPV").is_some());
1264 }
1265
1266 #[test]
1267 fn test_dict_stats() {
1268 let content = r#"
1269Adp |SV * |av_pop |NN AV *av
1270ARdm |SSize_t|av_tindex |NN AV *av
1271ARdip |Size_t |av_count |NN AV *av
1272Cp |void |internal_fn |int x
1273"#;
1274 let dict = ApidocDict::parse_embed_fnc_str(content);
1275 let stats = dict.stats();
1276 assert_eq!(stats.total, 4);
1277 assert_eq!(stats.macro_count, 1);
1278 assert_eq!(stats.inline_count, 1);
1279 assert_eq!(stats.function_count, 2);
1280 assert_eq!(stats.api_count, 3);
1281 }
1282
1283 #[test]
1284 fn test_dict_merge() {
1285 let content1 = "Adp |SV * |av_pop |NN AV *av";
1286 let content2 = "ARdm |SSize_t|av_tindex |NN AV *av";
1287
1288 let mut dict1 = ApidocDict::parse_embed_fnc_str(content1);
1289 let dict2 = ApidocDict::parse_embed_fnc_str(content2);
1290
1291 dict1.merge(dict2);
1292 assert_eq!(dict1.len(), 2);
1293 }
1294
1295 #[test]
1296 fn test_has_token_arg() {
1297 let entry = ApidocEntry::parse_apidoc_line(
1299 "=for apidoc Amu||XopENTRYCUSTOM|const OP *o|token which"
1300 ).unwrap();
1301 assert!(entry.has_token_arg());
1302 assert_eq!(entry.name, "XopENTRYCUSTOM");
1303
1304 let entry2 = ApidocEntry::parse_apidoc_line(
1306 "=for apidoc Am|char*|SvPV|SV* sv|STRLEN len"
1307 ).unwrap();
1308 assert!(!entry2.has_token_arg());
1309 }
1310
1311 #[test]
1312 fn test_has_token_arg_embed_fnc() {
1313 let entry = ApidocEntry::parse_line("Amu | |XopENTRYCUSTOM |const OP *o |token which").unwrap();
1315 assert!(entry.has_token_arg());
1316 assert_eq!(entry.name, "XopENTRYCUSTOM");
1317 }
1318
1319 #[test]
1320 fn test_expand_type_macros() {
1321 use crate::macro_def::MacroDef;
1322 use crate::source::SourceLocation;
1323 use crate::token::{Token, TokenKind};
1324
1325 let content = r#"
1327Adp |Off_t |PerlIO_tell |NN PerlIO *f
1328Adp |Size_t |PerlIO_read |NN PerlIO *f |NN void *buf |Size_t count
1329Adm |STDCHAR * |SvPVX |NN SV *sv
1330"#;
1331 let mut dict = ApidocDict::parse_embed_fnc_str(content);
1332
1333 let mut interner = crate::intern::StringInterner::new();
1335 let mut macro_table = MacroTable::new();
1336 let loc = SourceLocation::default();
1337
1338 let off_t_name = interner.intern("Off_t");
1340 let off_t_body = vec![Token::new(TokenKind::Ident(interner.intern("off_t")), loc.clone())];
1341 macro_table.define(MacroDef::object(off_t_name, off_t_body, loc.clone()), &interner);
1342
1343 let size_t_name = interner.intern("Size_t");
1345 let size_t_body = vec![Token::new(TokenKind::Ident(interner.intern("size_t")), loc.clone())];
1346 macro_table.define(MacroDef::object(size_t_name, size_t_body, loc.clone()), &interner);
1347
1348 let stdchar_name = interner.intern("STDCHAR");
1350 let stdchar_body = vec![Token::new(TokenKind::Ident(interner.intern("char")), loc.clone())];
1351 macro_table.define(MacroDef::object(stdchar_name, stdchar_body, loc), &interner);
1352
1353 dict.expand_type_macros(¯o_table, &interner);
1355
1356 let tell = dict.get("PerlIO_tell").unwrap();
1358 assert_eq!(tell.return_type, Some("off_t".to_string()));
1359
1360 let read = dict.get("PerlIO_read").unwrap();
1362 assert_eq!(read.return_type, Some("size_t".to_string()));
1363
1364 assert_eq!(read.args[2].ty, "size_t");
1366
1367 let svpvx = dict.get("SvPVX").unwrap();
1369 assert_eq!(svpvx.return_type, Some("char *".to_string()));
1370 }
1371
1372 #[test]
1373 fn test_expand_type_string_preserves_non_macros() {
1374 use crate::macro_def::MacroDef;
1375 use crate::source::SourceLocation;
1376 use crate::token::{Token, TokenKind};
1377
1378 let mut interner = crate::intern::StringInterner::new();
1379 let mut macro_table = MacroTable::new();
1380 let loc = SourceLocation::default();
1381
1382 let off_t_name = interner.intern("Off_t");
1384 let off_t_body = vec![Token::new(TokenKind::Ident(interner.intern("off_t")), loc.clone())];
1385 macro_table.define(MacroDef::object(off_t_name, off_t_body, loc), &interner);
1386
1387 let result = super::expand_type_string("SV *", ¯o_table, &interner);
1389 assert_eq!(result, "SV *");
1390
1391 let result = super::expand_type_string("const char *", ¯o_table, &interner);
1393 assert_eq!(result, "const char *");
1394
1395 let result = super::expand_type_string("Off_t *", ¯o_table, &interner);
1397 assert_eq!(result, "off_t *");
1398
1399 let result = super::expand_type_string("const Off_t *", ¯o_table, &interner);
1401 assert_eq!(result, "const off_t *");
1402 }
1403}