use std::any::Any;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::perl_config::{get_perl_version, PerlConfigError};
use serde::{Deserialize, Serialize};
use crate::intern::StringInterner;
use crate::macro_def::{MacroKind, MacroTable};
use crate::preprocessor::CommentCallback;
use crate::source::FileId;
use crate::token::Comment;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Nullability {
NotNull,
Nullable,
#[default]
Unspecified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApidocArg {
pub nullability: Nullability,
pub non_zero: bool,
pub ty: String,
pub name: String,
pub raw: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApidocFlags {
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,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApidocEntry {
pub flags: ApidocFlags,
pub return_type: Option<String>,
pub name: String,
pub args: Vec<ApidocArg>,
pub source_file: Option<String>,
pub line_number: Option<usize>,
#[serde(default)]
pub has_token_pasting: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ApidocDict {
entries: HashMap<String, ApidocEntry>,
}
impl ApidocFlags {
pub fn parse(flags: &str) -> Self {
let mut result = Self {
raw: flags.to_string(),
..Default::default()
};
for ch in flags.chars() {
match ch {
'A' => result.api = true,
'C' => result.core_only = true,
'E' => result.ext_visible = true,
'X' => result.exported = true,
'e' => result.not_exported = true,
'p' => result.perl_prefix = true,
'S' => result.static_fn = true,
's' => result.static_perl = true,
'i' => result.inline = true,
'I' => result.force_inline = true,
'm' => result.is_macro = true,
'M' => result.custom_macro = true,
'T' => result.no_thread_ctx = true,
'd' => result.documented = true,
'h' => result.hide_docs = true,
'U' => result.no_usage = true,
'a' => {
result.allocates = true;
result.return_required = true; }
'P' => {
result.pure = true;
result.return_required = true; }
'R' => result.return_required = true,
'r' => result.no_return = true,
'D' => result.deprecated = true,
'b' => result.compat = true,
'f' => result.format_string = true,
'F' => result.varargs_no_fmt = true,
'n' => result.no_args = true,
'u' => result.unorthodox = true,
'x' => result.experimental = true,
'y' => result.is_typedef = true,
'G' | 'N' | 'O' | 'o' | 'v' | 'W' | ';' | '#' | '?' => {}
_ => {}
}
}
result
}
}
impl ApidocArg {
pub fn parse(arg: &str) -> Option<Self> {
let raw = arg.to_string();
let trimmed = arg.trim();
if trimmed.is_empty() {
return None;
}
let mut nullability = Nullability::Unspecified;
let mut non_zero = false;
let mut remaining = trimmed;
loop {
if remaining.starts_with("NN ") {
nullability = Nullability::NotNull;
remaining = remaining[3..].trim_start();
} else if remaining.starts_with("NULLOK ") {
nullability = Nullability::Nullable;
remaining = remaining[7..].trim_start();
} else if remaining.starts_with("NZ ") {
non_zero = true;
remaining = remaining[3..].trim_start();
} else {
break;
}
}
let (ty, name) = Self::split_type_and_name(remaining);
Some(Self {
nullability,
non_zero,
ty,
name,
raw,
})
}
fn split_type_and_name(s: &str) -> (String, String) {
let s = s.trim();
if s == "..." {
return ("...".to_string(), String::new());
}
if s == "type" || s == "cast" || s == "SP" || s == "block"
|| s == "number" || s == "token" || s.starts_with('"')
{
return (s.to_string(), String::new());
}
let bytes = s.as_bytes();
let mut name_end = bytes.len();
let mut name_start;
while name_end > 0 && bytes[name_end - 1].is_ascii_whitespace() {
name_end -= 1;
}
name_start = name_end;
while name_start > 0 {
let ch = bytes[name_start - 1];
if ch.is_ascii_alphanumeric() || ch == b'_' {
name_start -= 1;
} else {
break;
}
}
if name_start == name_end {
return (s.to_string(), String::new());
}
let name = &s[name_start..name_end];
let ty = s[..name_start].trim_end();
if ty.is_empty() {
return (name.to_string(), String::new());
}
let type_keywords = ["const", "struct", "union", "enum", "unsigned", "signed", "volatile"];
let ty_lower = ty.to_lowercase();
for kw in &type_keywords {
if ty_lower == *kw {
return (s.to_string(), String::new());
}
}
(ty.to_string(), name.to_string())
}
}
impl ApidocEntry {
pub fn parse_line(line: &str) -> Option<Self> {
let trimmed = line.trim();
if trimmed.starts_with(": ") || trimmed == ":" || trimmed.is_empty() {
return None;
}
Self::parse_fields(trimmed)
}
pub fn parse_apidoc_line(line: &str) -> Option<Self> {
let trimmed = line.trim();
let rest = if let Some(rest) = trimmed.strip_prefix("=for apidoc_item") {
rest.trim()
} else if let Some(rest) = trimmed.strip_prefix("=for apidoc") {
rest.trim()
} else {
return None;
};
if rest.is_empty() {
return None;
}
if rest.contains('|') {
Self::parse_fields(rest)
} else {
Some(Self {
flags: ApidocFlags::default(),
return_type: None,
name: rest.to_string(),
args: Vec::new(),
source_file: None,
line_number: None,
has_token_pasting: false,
})
}
}
fn parse_fields(s: &str) -> Option<Self> {
let fields: Vec<&str> = s.split('|').collect();
if fields.len() < 3 {
return None;
}
let flags = ApidocFlags::parse(fields[0].trim());
let return_type = {
let rt = fields[1].trim();
if rt.is_empty() {
None
} else {
Some(rt.to_string())
}
};
let name = fields[2].trim().to_string();
if name.is_empty() {
return None;
}
let args: Vec<ApidocArg> = fields[3..]
.iter()
.filter_map(|arg| ApidocArg::parse(arg))
.collect();
let has_token_pasting = args.iter().any(|arg| {
arg.ty.starts_with('"') && arg.ty.ends_with('"')
});
Some(Self {
flags,
return_type,
name,
args,
source_file: None,
line_number: None,
has_token_pasting,
})
}
pub fn is_public_api(&self) -> bool {
self.flags.api
}
pub fn is_macro(&self) -> bool {
self.flags.is_macro
}
pub fn is_inline(&self) -> bool {
self.flags.inline || self.flags.force_inline
}
pub fn is_type_param_keyword(ty: &str) -> bool {
ty == "type" || ty == "cast"
}
pub fn type_param_indices(&self) -> Vec<usize> {
self.args
.iter()
.enumerate()
.filter(|(_, arg)| Self::is_type_param_keyword(&arg.ty))
.map(|(i, _)| i)
.collect()
}
pub fn returns_type_param(&self) -> bool {
self.return_type
.as_ref()
.map_or(false, |t| Self::is_type_param_keyword(t))
}
pub fn is_generic(&self) -> bool {
self.returns_type_param() || !self.type_param_indices().is_empty()
}
pub fn is_literal_string_keyword(ty: &str) -> bool {
ty.starts_with('"')
}
pub fn has_token_arg(&self) -> bool {
self.args.iter().any(|arg| arg.ty == "token")
}
}
impl ApidocDict {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, name: String, entry: ApidocEntry) {
self.entries.insert(name, entry);
}
pub fn parse_embed_fnc<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let content = fs::read_to_string(path)?;
Ok(Self::parse_embed_fnc_str(&content))
}
pub fn parse_embed_fnc_str(content: &str) -> Self {
let mut dict = Self::new();
let mut continued_line = String::new();
let mut line_number = 0usize;
for line in content.lines() {
line_number += 1;
if line.ends_with('\\') {
continued_line.push_str(line.trim_end_matches('\\'));
continued_line.push(' ');
continue;
}
let full_line = if continued_line.is_empty() {
line.to_string()
} else {
continued_line.push_str(line);
let result = continued_line.clone();
continued_line.clear();
result
};
if let Some(mut entry) = ApidocEntry::parse_line(&full_line) {
entry.line_number = Some(line_number);
dict.entries.insert(entry.name.clone(), entry);
}
}
dict
}
pub fn parse_header_apidoc<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let content = fs::read_to_string(&path)?;
let mut dict = Self::parse_header_apidoc_str(&content);
let path_str = path.as_ref().to_string_lossy().to_string();
for entry in dict.entries.values_mut() {
entry.source_file = Some(path_str.clone());
}
Ok(dict)
}
pub fn parse_header_apidoc_str(content: &str) -> Self {
let mut dict = Self::new();
let mut line_number = 0usize;
for line in content.lines() {
line_number += 1;
if let Some(idx) = line.find("=for apidoc") {
let apidoc_part = &line[idx..];
if let Some(mut entry) = ApidocEntry::parse_apidoc_line(apidoc_part) {
entry.line_number = Some(line_number);
dict.entries.insert(entry.name.clone(), entry);
}
}
}
dict
}
pub fn merge(&mut self, other: Self) {
for (name, entry) in other.entries {
self.entries.entry(name).or_insert(entry);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, name: &str) -> Option<&ApidocEntry> {
self.entries.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut ApidocEntry> {
self.entries.get_mut(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
self.entries.iter()
}
pub fn functions(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
self.entries.iter().filter(|(_, e)| !e.is_macro())
}
pub fn macros(&self) -> impl Iterator<Item = (&String, &ApidocEntry)> {
self.entries.iter().filter(|(_, e)| e.is_macro())
}
pub fn dump_filtered(&self, filter: &str) {
let mut names: Vec<_> = self.entries.keys().collect();
names.sort();
for name in names {
if !filter.is_empty() && !name.contains(filter) {
continue;
}
if let Some(entry) = self.entries.get(name) {
eprintln!("{}:", name);
eprintln!(" flags: {}", entry.flags.raw);
if let Some(ref ret) = entry.return_type {
eprintln!(" return_type: {}", ret);
} else {
eprintln!(" return_type: (none)");
}
eprintln!(" args:");
for (i, arg) in entry.args.iter().enumerate() {
eprintln!(" [{}] {} {} ({:?}{})",
i,
arg.ty,
arg.name,
arg.nullability,
if arg.non_zero { ", NZ" } else { "" }
);
}
if let Some(ref src) = entry.source_file {
eprintln!(" source: {}:{}", src, entry.line_number.unwrap_or(0));
}
eprintln!();
}
}
}
pub fn stats(&self) -> ApidocStats {
let mut stats = ApidocStats::default();
for entry in self.entries.values() {
if entry.is_macro() {
stats.macro_count += 1;
} else if entry.is_inline() {
stats.inline_count += 1;
} else {
stats.function_count += 1;
}
if entry.is_public_api() {
stats.api_count += 1;
}
}
stats.total = self.entries.len();
stats
}
pub fn save_json<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
fs::write(path, json)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn load_json<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let content = fs::read_to_string(path)?;
Self::from_json(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn load_auto<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let path_ref = path.as_ref();
if path_ref.extension().is_some_and(|ext| ext == "json") {
Self::load_json(path_ref)
} else {
Self::parse_embed_fnc(path_ref)
}
}
pub fn find_json_for_version<P: AsRef<Path>>(
apidoc_dir: P,
major: u32,
minor: u32,
) -> Option<std::path::PathBuf> {
let filename = format!("v{}.{}.json", major, minor);
let path = apidoc_dir.as_ref().join(&filename);
if path.exists() {
Some(path)
} else {
None
}
}
pub fn load_for_perl_version<P: AsRef<Path>>(
apidoc_dir: P,
major: u32,
minor: u32,
) -> io::Result<Self> {
let path = Self::find_json_for_version(&apidoc_dir, major, minor).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!(
"{}/v{}.{}.json not found for Perl {}.{}.\n\
Please specify --apidoc explicitly or add the JSON file.",
apidoc_dir.as_ref().display(),
major,
minor,
major,
minor
),
)
})?;
Self::load_json(&path)
}
pub fn expand_type_macros(&mut self, macro_table: &MacroTable, interner: &StringInterner) {
for entry in self.entries.values_mut() {
if let Some(ref mut return_type) = entry.return_type {
*return_type = expand_type_string(return_type, macro_table, interner);
}
for arg in &mut entry.args {
arg.ty = expand_type_string(&arg.ty, macro_table, interner);
}
}
}
}
fn expand_type_string(
type_str: &str,
macro_table: &MacroTable,
interner: &StringInterner,
) -> String {
let mut result = String::new();
let mut chars = type_str.chars().peekable();
while let Some(c) = chars.next() {
if c.is_alphabetic() || c == '_' {
let mut ident = String::from(c);
while let Some(&nc) = chars.peek() {
if nc.is_alphanumeric() || nc == '_' {
ident.push(chars.next().unwrap());
} else {
break;
}
}
if let Some(interned) = interner.lookup(&ident) {
if let Some(macro_def) = macro_table.get(interned) {
if matches!(macro_def.kind, MacroKind::Object) && !macro_def.body.is_empty() {
let expanded: String = macro_def
.body
.iter()
.map(|t| t.kind.format(interner))
.collect::<Vec<_>>()
.join("");
result.push_str(&expanded);
continue;
}
}
}
result.push_str(&ident);
} else {
result.push(c);
}
}
result
}
#[derive(Debug)]
pub enum ApidocResolveError {
DevelopmentVersion { major: u32, minor: u32 },
DirectoryNotFound,
JsonNotFound { path: PathBuf, major: u32, minor: u32 },
VersionError(PerlConfigError),
}
impl std::fmt::Display for ApidocResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApidocResolveError::DevelopmentVersion { major, minor } => {
write!(
f,
"Perl {}.{} is a development version.\n\
Please specify --apidoc explicitly (e.g., --apidoc path/to/embed.fnc)",
major, minor
)
}
ApidocResolveError::DirectoryNotFound => {
write!(
f,
"apidoc directory not found.\n\
Please specify --apidoc explicitly."
)
}
ApidocResolveError::JsonNotFound { path, major, minor } => {
write!(
f,
"{}/v{}.{}.json not found for Perl {}.{}.\n\
Please specify --apidoc explicitly or add the JSON file.",
path.display(),
major, minor, major, minor
)
}
ApidocResolveError::VersionError(e) => {
write!(f, "Failed to get Perl version: {}", e)
}
}
}
}
impl std::error::Error for ApidocResolveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ApidocResolveError::VersionError(e) => Some(e),
_ => None,
}
}
}
impl From<PerlConfigError> for ApidocResolveError {
fn from(e: PerlConfigError) -> Self {
ApidocResolveError::VersionError(e)
}
}
pub fn find_apidoc_dir_from(base_dir: Option<&Path>) -> Option<PathBuf> {
if let Some(base) = base_dir {
let apidoc_dir = base.join("apidoc");
if apidoc_dir.is_dir() {
return Some(apidoc_dir);
}
if base.is_dir() && base.file_name().is_some_and(|n| n == "apidoc") {
return Some(base.to_path_buf());
}
}
if let Some(embedded_dir) = crate::apidoc_data::get_apidoc_dir() {
if embedded_dir.is_dir() {
return Some(embedded_dir);
}
}
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let apidoc_dir = exe_dir.join("apidoc");
if apidoc_dir.is_dir() {
return Some(apidoc_dir);
}
if let Some(parent_dir) = exe_dir.parent() {
let apidoc_dir = parent_dir.join("apidoc");
if apidoc_dir.is_dir() {
return Some(apidoc_dir);
}
if let Some(grandparent_dir) = parent_dir.parent() {
let apidoc_dir = grandparent_dir.join("apidoc");
if apidoc_dir.is_dir() {
return Some(apidoc_dir);
}
}
}
}
}
if let Ok(cwd) = std::env::current_dir() {
let apidoc_dir = cwd.join("apidoc");
if apidoc_dir.is_dir() {
return Some(apidoc_dir);
}
}
None
}
pub fn resolve_apidoc_path(
explicit_path: Option<&Path>,
auto_mode: bool,
apidoc_dir: Option<&Path>,
) -> Result<Option<PathBuf>, ApidocResolveError> {
if let Some(path) = explicit_path {
return Ok(Some(path.to_path_buf()));
}
if !auto_mode {
return Ok(None);
}
let (major, minor) = get_perl_version()?;
if minor % 2 == 1 {
return Err(ApidocResolveError::DevelopmentVersion { major, minor });
}
let resolved_apidoc_dir = find_apidoc_dir_from(apidoc_dir)
.ok_or(ApidocResolveError::DirectoryNotFound)?;
let json_path = ApidocDict::find_json_for_version(&resolved_apidoc_dir, major, minor)
.ok_or_else(|| ApidocResolveError::JsonNotFound {
path: resolved_apidoc_dir,
major,
minor,
})?;
Ok(Some(json_path))
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ApidocStats {
pub total: usize,
pub function_count: usize,
pub macro_count: usize,
pub inline_count: usize,
pub api_count: usize,
}
pub struct ApidocCollector {
entries: HashMap<String, ApidocEntry>,
token_type_macros: Vec<String>,
}
impl ApidocCollector {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
token_type_macros: Vec::new(),
}
}
pub fn merge_into(self, dict: &mut ApidocDict) {
if crate::apidoc_patches::is_apidoc_debug_enabled() {
let total = self.entries.len();
let rcpv: Vec<String> = self.entries.iter()
.filter(|(k, _)| k.starts_with("RCPV"))
.map(|(k, e)| format!("{}->{}", k, e.return_type.as_deref().unwrap_or("?")))
.collect();
crate::apidoc_patches::cargo_warning(&format!(
"[apidoc-collector] merge_into: {} entries from inline =for apidoc; \
RCPV-named ({}): {:?}",
total, rcpv.len(), rcpv
));
}
for (name, entry) in self.entries {
dict.insert(name, entry);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn token_type_macros(&self) -> &[String] {
&self.token_type_macros
}
}
impl Default for ApidocCollector {
fn default() -> Self {
Self::new()
}
}
impl CommentCallback for ApidocCollector {
fn on_comment(&mut self, comment: &Comment, _file_id: FileId, _is_target: bool) {
for line in comment.text.lines() {
if let Some(entry) = ApidocEntry::parse_apidoc_line(line) {
if entry.has_token_arg() {
self.token_type_macros.push(entry.name.clone());
}
self.entries.insert(entry.name.clone(), entry);
}
}
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_flags() {
let flags = ApidocFlags::parse("Adp");
assert!(flags.api);
assert!(flags.documented);
assert!(flags.perl_prefix);
assert!(!flags.is_macro);
}
#[test]
fn test_parse_flags_macro() {
let flags = ApidocFlags::parse("ARdm");
assert!(flags.api);
assert!(flags.return_required);
assert!(flags.documented);
assert!(flags.is_macro);
}
#[test]
fn test_parse_flags_allocates_implies_r() {
let flags = ApidocFlags::parse("a");
assert!(flags.allocates);
assert!(flags.return_required);
}
#[test]
fn test_parse_arg_simple() {
let arg = ApidocArg::parse("int method").unwrap();
assert_eq!(arg.nullability, Nullability::Unspecified);
assert!(!arg.non_zero);
assert_eq!(arg.ty, "int");
assert_eq!(arg.name, "method");
}
#[test]
fn test_parse_arg_pointer() {
let arg = ApidocArg::parse("SV *sv").unwrap();
assert_eq!(arg.ty, "SV *");
assert_eq!(arg.name, "sv");
}
#[test]
fn test_parse_arg_not_null() {
let arg = ApidocArg::parse("NN SV *sv").unwrap();
assert_eq!(arg.nullability, Nullability::NotNull);
assert_eq!(arg.ty, "SV *");
assert_eq!(arg.name, "sv");
}
#[test]
fn test_parse_arg_nullok() {
let arg = ApidocArg::parse("NULLOK SV *sv").unwrap();
assert_eq!(arg.nullability, Nullability::Nullable);
assert_eq!(arg.ty, "SV *");
assert_eq!(arg.name, "sv");
}
#[test]
fn test_parse_arg_const_pointer() {
let arg = ApidocArg::parse("NN const char * const name").unwrap();
assert_eq!(arg.nullability, Nullability::NotNull);
assert_eq!(arg.ty, "const char * const");
assert_eq!(arg.name, "name");
}
#[test]
fn test_parse_arg_varargs() {
let arg = ApidocArg::parse("...").unwrap();
assert_eq!(arg.ty, "...");
assert_eq!(arg.name, "");
}
#[test]
fn test_parse_line_simple() {
let entry = ApidocEntry::parse_line("Adp |SV * |av_pop |NN AV *av").unwrap();
assert!(entry.flags.api);
assert!(entry.flags.documented);
assert!(entry.flags.perl_prefix);
assert_eq!(entry.return_type, Some("SV *".to_string()));
assert_eq!(entry.name, "av_pop");
assert_eq!(entry.args.len(), 1);
assert_eq!(entry.args[0].ty, "AV *");
assert_eq!(entry.args[0].name, "av");
assert_eq!(entry.args[0].nullability, Nullability::NotNull);
}
#[test]
fn test_parse_line_comment() {
assert!(ApidocEntry::parse_line(": This is a comment").is_none());
assert!(ApidocEntry::parse_line(":").is_none());
assert!(ApidocEntry::parse_line("").is_none());
}
#[test]
fn test_parse_line_macro() {
let entry = ApidocEntry::parse_line("ARdm |SSize_t|av_tindex |NN AV *av").unwrap();
assert!(entry.flags.is_macro);
assert!(entry.flags.return_required);
assert_eq!(entry.name, "av_tindex");
}
#[test]
fn test_parse_line_multiple_args() {
let entry = ApidocEntry::parse_line(
"Adp |SV * |amagic_call |NN SV *left |NN SV *right |int method |int dir"
).unwrap();
assert_eq!(entry.args.len(), 4);
assert_eq!(entry.args[0].name, "left");
assert_eq!(entry.args[1].name, "right");
assert_eq!(entry.args[2].name, "method");
assert_eq!(entry.args[3].name, "dir");
}
#[test]
fn test_parse_apidoc_line_name_only() {
let entry = ApidocEntry::parse_apidoc_line("=for apidoc av_pop").unwrap();
assert_eq!(entry.name, "av_pop");
assert!(entry.return_type.is_none());
assert!(entry.args.is_empty());
}
#[test]
fn test_parse_apidoc_line_full() {
let entry = ApidocEntry::parse_apidoc_line(
"=for apidoc Am|char*|SvPV|SV* sv|STRLEN len"
).unwrap();
assert!(entry.flags.api);
assert!(entry.flags.is_macro);
assert_eq!(entry.return_type, Some("char*".to_string()));
assert_eq!(entry.name, "SvPV");
assert_eq!(entry.args.len(), 2);
}
#[test]
fn test_parse_apidoc_item() {
let entry = ApidocEntry::parse_apidoc_line(
"=for apidoc_item |const char*|SvPV_const|SV* sv|STRLEN len"
).unwrap();
assert_eq!(entry.return_type, Some("const char*".to_string()));
assert_eq!(entry.name, "SvPV_const");
}
#[test]
fn test_embed_fnc_str() {
let content = r#"
: This is a comment
Adp |SV * |av_pop |NN AV *av
ARdm |SSize_t|av_tindex |NN AV *av
"#;
let dict = ApidocDict::parse_embed_fnc_str(content);
assert_eq!(dict.len(), 2);
assert!(dict.get("av_pop").is_some());
assert!(dict.get("av_tindex").is_some());
}
#[test]
fn test_embed_fnc_continuation() {
let content = r#"
pr |void |abort_execution|NULLOK SV *msg_sv \
|NN const char * const name
"#;
let dict = ApidocDict::parse_embed_fnc_str(content);
assert_eq!(dict.len(), 1);
let entry = dict.get("abort_execution").unwrap();
assert_eq!(entry.args.len(), 2);
assert_eq!(entry.args[0].nullability, Nullability::Nullable);
assert_eq!(entry.args[1].nullability, Nullability::NotNull);
}
#[test]
fn test_header_apidoc_str() {
let content = r#"
/*
=for apidoc Am|char*|SvPV|SV* sv|STRLEN len
Returns a pointer to the string value of the SV.
=cut
*/
"#;
let dict = ApidocDict::parse_header_apidoc_str(content);
assert_eq!(dict.len(), 1);
assert!(dict.get("SvPV").is_some());
}
#[test]
fn test_dict_stats() {
let content = r#"
Adp |SV * |av_pop |NN AV *av
ARdm |SSize_t|av_tindex |NN AV *av
ARdip |Size_t |av_count |NN AV *av
Cp |void |internal_fn |int x
"#;
let dict = ApidocDict::parse_embed_fnc_str(content);
let stats = dict.stats();
assert_eq!(stats.total, 4);
assert_eq!(stats.macro_count, 1);
assert_eq!(stats.inline_count, 1);
assert_eq!(stats.function_count, 2);
assert_eq!(stats.api_count, 3);
}
#[test]
fn test_dict_merge() {
let content1 = "Adp |SV * |av_pop |NN AV *av";
let content2 = "ARdm |SSize_t|av_tindex |NN AV *av";
let mut dict1 = ApidocDict::parse_embed_fnc_str(content1);
let dict2 = ApidocDict::parse_embed_fnc_str(content2);
dict1.merge(dict2);
assert_eq!(dict1.len(), 2);
}
#[test]
fn test_has_token_arg() {
let entry = ApidocEntry::parse_apidoc_line(
"=for apidoc Amu||XopENTRYCUSTOM|const OP *o|token which"
).unwrap();
assert!(entry.has_token_arg());
assert_eq!(entry.name, "XopENTRYCUSTOM");
let entry2 = ApidocEntry::parse_apidoc_line(
"=for apidoc Am|char*|SvPV|SV* sv|STRLEN len"
).unwrap();
assert!(!entry2.has_token_arg());
}
#[test]
fn test_has_token_arg_embed_fnc() {
let entry = ApidocEntry::parse_line("Amu | |XopENTRYCUSTOM |const OP *o |token which").unwrap();
assert!(entry.has_token_arg());
assert_eq!(entry.name, "XopENTRYCUSTOM");
}
#[test]
fn test_expand_type_macros() {
use crate::macro_def::MacroDef;
use crate::source::SourceLocation;
use crate::token::{Token, TokenKind};
let content = r#"
Adp |Off_t |PerlIO_tell |NN PerlIO *f
Adp |Size_t |PerlIO_read |NN PerlIO *f |NN void *buf |Size_t count
Adm |STDCHAR * |SvPVX |NN SV *sv
"#;
let mut dict = ApidocDict::parse_embed_fnc_str(content);
let mut interner = crate::intern::StringInterner::new();
let mut macro_table = MacroTable::new();
let loc = SourceLocation::default();
let off_t_name = interner.intern("Off_t");
let off_t_body = vec![Token::new(TokenKind::Ident(interner.intern("off_t")), loc.clone())];
macro_table.define(MacroDef::object(off_t_name, off_t_body, loc.clone()), &interner);
let size_t_name = interner.intern("Size_t");
let size_t_body = vec![Token::new(TokenKind::Ident(interner.intern("size_t")), loc.clone())];
macro_table.define(MacroDef::object(size_t_name, size_t_body, loc.clone()), &interner);
let stdchar_name = interner.intern("STDCHAR");
let stdchar_body = vec![Token::new(TokenKind::Ident(interner.intern("char")), loc.clone())];
macro_table.define(MacroDef::object(stdchar_name, stdchar_body, loc), &interner);
dict.expand_type_macros(¯o_table, &interner);
let tell = dict.get("PerlIO_tell").unwrap();
assert_eq!(tell.return_type, Some("off_t".to_string()));
let read = dict.get("PerlIO_read").unwrap();
assert_eq!(read.return_type, Some("size_t".to_string()));
assert_eq!(read.args[2].ty, "size_t");
let svpvx = dict.get("SvPVX").unwrap();
assert_eq!(svpvx.return_type, Some("char *".to_string()));
}
#[test]
fn test_expand_type_string_preserves_non_macros() {
use crate::macro_def::MacroDef;
use crate::source::SourceLocation;
use crate::token::{Token, TokenKind};
let mut interner = crate::intern::StringInterner::new();
let mut macro_table = MacroTable::new();
let loc = SourceLocation::default();
let off_t_name = interner.intern("Off_t");
let off_t_body = vec![Token::new(TokenKind::Ident(interner.intern("off_t")), loc.clone())];
macro_table.define(MacroDef::object(off_t_name, off_t_body, loc), &interner);
let result = super::expand_type_string("SV *", ¯o_table, &interner);
assert_eq!(result, "SV *");
let result = super::expand_type_string("const char *", ¯o_table, &interner);
assert_eq!(result, "const char *");
let result = super::expand_type_string("Off_t *", ¯o_table, &interner);
assert_eq!(result, "off_t *");
let result = super::expand_type_string("const Off_t *", ¯o_table, &interner);
assert_eq!(result, "const off_t *");
}
}