use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use syn::spanned::Spanned;
use crate::{DocTest, SourceFile};
pub(crate) struct FnSite {
pub(crate) site: DocTest,
pub(crate) scope: String,
pub(crate) func: syn::ItemFn,
}
pub(crate) fn functions(
prefix: &str,
file: &SourceFile,
root: &str,
crate_root: &str,
) -> Vec<FnSite> {
let path = file
.path
.strip_prefix(root)
.and_then(|p| p.strip_prefix('/'))
.unwrap_or(&file.path);
let lines: Vec<&str> = file.text.lines().collect();
let mut walk = Walk {
path,
crate_root,
lines: &lines,
out: Vec::new(),
};
walk.items(&file.parsed.items, prefix);
walk.out
}
struct Walk<'a> {
path: &'a str,
crate_root: &'a str,
lines: &'a [&'a str],
out: Vec<FnSite>,
}
impl Walk<'_> {
fn items(&mut self, items: &[syn::Item], module: &str) {
for item in items {
match item {
syn::Item::Fn(f) => self.push(module, None, f, &f.attrs, &f.sig, &f.block),
syn::Item::Mod(m) => {
if let Some((_, inner)) = &m.content {
self.items(inner, &format!("{module}::{}", m.ident));
}
}
syn::Item::Impl(i) => {
let self_type = type_text(&i.self_ty);
for member in &i.items {
if let syn::ImplItem::Fn(f) = member {
self.push(module, Some(&self_type), f, &f.attrs, &f.sig, &f.block);
}
}
}
syn::Item::Trait(t) => {
let self_type = t.ident.to_string();
for member in &t.items {
if let syn::TraitItem::Fn(f) = member
&& let Some(block) = &f.default
{
self.push(module, Some(&self_type), f, &f.attrs, &f.sig, block);
}
}
}
_ => {}
}
}
}
fn push(
&mut self,
module: &str,
self_type: Option<&str>,
whole: &impl Spanned,
attrs: &[syn::Attribute],
sig: &syn::Signature,
block: &syn::Block,
) {
let span = whole.span();
let (line, end) = (span.start().line, span.end().line);
let allow = line
.checked_sub(2)
.and_then(|above| self.lines.get(above))
.is_some_and(|above| above.trim() == "// dejadoc: allow");
let item = match self_type {
Some(ty) => format!("{module}::{ty}::{}", sig.ident),
None => format!("{module}::{}", sig.ident),
};
let code = self
.lines
.get(line.saturating_sub(1)..end.min(self.lines.len()))
.unwrap_or_default()
.join("\n");
self.out.push(FnSite {
site: DocTest {
file: self.path.to_string(),
line: u32::try_from(line).unwrap_or(u32::MAX),
end: u32::try_from(end).ok(),
item,
info: Vec::new(),
code,
allow,
self_type: self_type.map(ToString::to_string),
},
scope: format!("{}\n{module}", self.crate_root),
func: syn::ItemFn {
attrs: attrs.iter().filter(|a| compared(a)).cloned().collect(),
vis: syn::Visibility::Inherited,
modifiers: syn::FnModifiers::default(),
sig: sig.clone(),
block: Box::new(block.clone()),
},
});
}
}
fn compared(attr: &syn::Attribute) -> bool {
![
"cfg",
"cfg_attr",
"inline",
"cold",
"must_use",
"track_caller",
]
.iter()
.any(|name| attr.path().is_ident(name))
}
fn type_text(ty: &syn::Type) -> String {
let printed = quote::ToTokens::to_token_stream(ty).to_string();
let word = |c: Option<char>| c.is_some_and(|c| c.is_alphanumeric() || c == '_');
let mut out = String::with_capacity(printed.len());
let mut chars = printed.chars().peekable();
while let Some(c) = chars.next() {
if c != ' ' || (word(out.chars().last()) && word(chars.peek().copied())) {
out.push(c);
}
}
out
}