use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{ItemFn, ItemImpl};
#[allow(unused_imports)]
use crate::extendr;
use crate::extendr_options::ExtendrOptions;
use crate::wrappers;
fn transform_method_doc_roxygen(method_name: &str, doc: &str) -> (String, Vec<String>) {
let mut description = Vec::new();
let mut other_groups: Vec<(String, Vec<String>)> = Vec::new();
let mut current_other: Option<(String, Vec<String>)> = None;
let mut params: Vec<(String, Vec<String>)> = Vec::new();
let mut current_param: Option<(String, Vec<String>)> = None;
let mut state = "description";
for line in doc.lines() {
let trimmed = line.trim();
if trimmed.starts_with("@param") {
if let Some((name, lines)) = current_param.take() {
params.push((name, lines));
}
if let Some((tag, content)) = current_other.take() {
other_groups.push((tag, content));
}
let mut parts = trimmed.splitn(3, ' ');
parts.next();
let param_name = parts.next().unwrap_or("").to_string();
let param_desc = parts.next().unwrap_or("").to_string();
current_param = Some((param_name, vec![param_desc]));
state = "param";
continue;
} else if trimmed.starts_with('@') {
if let Some((name, lines)) = current_param.take() {
params.push((name, lines));
}
let mut parts = trimmed.splitn(2, ' ');
let tag_with_at = parts.next().unwrap_or("");
let tag = tag_with_at.trim_start_matches('@').to_string();
if let Some((curr_tag, _)) = ¤t_other {
if *curr_tag != tag {
let (t, c) = current_other.take().unwrap();
other_groups.push((t, c));
current_other = Some((tag.clone(), Vec::new()));
}
} else {
current_other = Some((tag.clone(), Vec::new()));
}
if let Some(inline) = parts.next() {
let inline = inline.trim();
if !inline.is_empty() {
if let Some((_, ref mut vec)) = current_other {
vec.push(inline.to_string());
}
}
}
state = "other";
continue;
}
match state {
"description" => description.push(trimmed.to_string()),
"other" => {
if let Some((_, ref mut vec)) = current_other {
vec.push(trimmed.to_string());
}
}
"param" => {
if let Some((_, ref mut lines)) = current_param {
lines.push(trimmed.to_string());
}
}
_ => description.push(trimmed.to_string()),
}
}
if let Some((name, lines)) = current_param.take() {
params.push((name, lines));
}
if let Some((tag, content)) = current_other.take() {
other_groups.push((tag, content));
}
let mut output = String::new();
output.push_str(&format!("\\subsection{{Method `{}`}}{{\n", method_name));
if !description.is_empty() {
output.push_str(&description.join("\n"));
output.push('\n');
}
if !params.is_empty() {
output.push_str(" \\subsection{Arguments}{\n\\describe{\n");
for (pname, plines) in params {
let param_text = plines.join(" ");
output.push_str(&format!("\\item{{`{}`}}{{{}}}\n", pname, param_text));
}
output.push_str("}}\n");
}
let mut examples: Vec<String> = Vec::new();
for (tag, contents) in other_groups {
match tag.as_str() {
"examples" => {
examples.push(format!(
"## ---- Method `{}` ---- ##\n{}",
method_name,
contents.join("\n")
));
}
"usage" => {
output.push_str(&format!(
" \\subsection{{{}}}{{\n \\preformatted{{\n{}\n}}\n}}\n",
tag,
contents.join("\n")
));
}
other => {
output.push_str(&format!(
" \\subsection{{{}}}{{\n{}\n}}\n",
other,
contents.join("\n")
));
}
}
}
output.push_str("}\n");
(output, examples)
}
pub(crate) fn extendr_impl(
mut item_impl: ItemImpl,
opts: &ExtendrOptions,
) -> syn::Result<TokenStream> {
if item_impl.defaultness.is_some() {
return Err(syn::Error::new_spanned(
item_impl,
"default not allowed in #[extendr] impl",
));
}
if item_impl.unsafety.is_some() {
return Err(syn::Error::new_spanned(
item_impl,
"unsafe not allowed in #[extendr] impl",
));
}
if item_impl.generics.const_params().count() != 0 {
return Err(syn::Error::new_spanned(
item_impl,
"const params not allowed in #[extendr] impl",
));
}
if item_impl.generics.type_params().count() != 0 {
return Err(syn::Error::new_spanned(
item_impl,
"type params not allowed in #[extendr] impl",
));
}
if item_impl.generics.where_clause.is_some() {
return Err(syn::Error::new_spanned(
item_impl,
"where clause not allowed in #[extendr] impl",
));
}
let self_ty = item_impl.self_ty.as_ref();
let self_ty_name = wrappers::type_name(self_ty);
let prefix = format!("{}__", self_ty_name);
let mut method_meta_names = Vec::new();
let impl_doc = wrappers::get_doc_string(&item_impl.attrs);
let struct_doc = wrappers::get_struct_doc(&self_ty_name);
wrappers::register_struct_doc(&self_ty_name, "");
let doc_string = if struct_doc.is_empty() {
impl_doc
} else {
format!("{}\n{}", struct_doc.trim_end(), impl_doc)
};
let mut method_docs: Vec<(String, String)> = Vec::new();
let mut all_examples: Vec<String> = Vec::new();
for impl_item in &item_impl.items {
if let syn::ImplItem::Fn(method) = impl_item {
let mdoc = wrappers::get_doc_string(&method.attrs);
if !mdoc.is_empty() {
let (sect, examples) =
transform_method_doc_roxygen(&method.sig.ident.to_string(), &mdoc);
method_docs.push((method.sig.ident.to_string(), sect));
for ex in examples {
all_examples.push(ex);
all_examples.push(String::new());
}
}
}
}
let methods_section = if !method_docs.is_empty() {
let mut sec = String::from("\n @section Methods:");
for (_name, doc) in &method_docs {
sec.push('\n');
sec.push_str(doc);
}
sec
} else {
String::new()
};
let examples_section = if !all_examples.is_empty() {
let mut ex = String::from("\n @examples\n");
for line in &all_examples {
ex.push_str(line);
ex.push('\n');
}
ex
} else {
String::new()
};
let full_doc = format!("{}{}{}", doc_string, methods_section, examples_section);
let mut wrappers: Vec<ItemFn> = Vec::new();
for impl_item in &mut item_impl.items {
if let syn::ImplItem::Fn(ref mut method) = impl_item {
method_meta_names.push(format_ident!(
"{}{}__{}",
wrappers::META_PREFIX,
self_ty_name,
method.sig.ident
));
wrappers::make_function_wrappers(
opts,
&mut wrappers,
prefix.as_str(),
&method.attrs,
&mut method.sig,
Some(self_ty),
)?;
}
}
let meta_name = format_ident!("{}{}", wrappers::META_PREFIX, self_ty_name);
let expanded = TokenStream::from(quote! {
#item_impl
#( #wrappers )*
#[allow(non_snake_case)]
fn #meta_name(impls: &mut Vec<extendr_api::metadata::Impl>) {
let mut methods = Vec::new();
#( #method_meta_names(&mut methods); )*
impls.push(extendr_api::metadata::Impl {
doc: #full_doc,
name: #self_ty_name,
methods,
});
}
});
Ok(expanded)
}