use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use quote::ToTokens;
use syn::{
AttrStyle, Expr, ExprLit, ExprMacro, ForeignItem, Ident, Item, Lit, LitStr, Macro, Meta, Token,
TraitItem, Type, UseTree,
parse::{Parse, ParseStream},
parse2,
punctuated::Punctuated,
visit::Visit,
};
use crate::DocTest;
use crate::IncludeRead;
use crate::cfg;
use crate::fence;
struct DocSource {
text: String,
lines: Vec<u32>,
files: Vec<String>,
}
#[must_use]
pub fn extract(
prefix: &str,
file: &str,
parsed: &syn::File,
root: &str,
read_include: &IncludeRead<'_>,
) -> Vec<DocTest> {
let mut walker = Walker {
file,
root,
read_include,
out: Vec::new(),
};
let inner: Vec<syn::Attribute> = parsed
.attrs
.iter()
.filter(|a| matches!(a.style, AttrStyle::Inner { .. }))
.cloned()
.collect();
if !walker.doc(&inner, prefix) {
return walker.out;
}
for item in &parsed.items {
walker.item(item, prefix);
}
walker.out
}
struct Walker<'a> {
file: &'a str,
root: &'a str,
read_include: &'a IncludeRead<'a>,
out: Vec<DocTest>,
}
impl Walker<'_> {
fn doc(&mut self, attrs: &[syn::Attribute], item: &str) -> bool {
if !cfg::allows(attrs) {
return false;
}
for src in doc_sources(attrs, self.file, self.read_include) {
self.out.extend(blocks(&src, item, self.root));
}
true
}
fn item(&mut self, item: &Item, prefix: &str) {
match item {
Item::Mod(m) => {
let sub = format!("{prefix}::{}", m.ident);
if !self.doc(&m.attrs, &sub) {
return;
}
if let Some((_, items)) = &m.content {
for child in items {
self.item(child, &sub);
}
}
}
Item::Impl(i) => {
let sub = format!("{prefix}::{}", type_name(&i.self_ty));
if !self.doc(&i.attrs, &sub) {
return;
}
for assoc in &i.items {
let Some(name) = impl_assoc_name(assoc) else {
continue;
};
let path = format!("{sub}::{name}");
if self.doc(assoc_attrs(assoc), &path)
&& let syn::ImplItem::Fn(f) = assoc
{
self.body(&f.block, &path);
}
}
}
Item::Trait(t) => {
let sub = format!("{prefix}::{}", t.ident);
if !self.doc(&t.attrs, &sub) {
return;
}
for assoc in &t.items {
let Some(name) = trait_assoc_name(assoc) else {
continue;
};
let path = format!("{sub}::{name}");
if self.doc(trait_assoc_attrs(assoc), &path)
&& let TraitItem::Fn(f) = assoc
&& let Some(block) = &f.default
{
self.body(block, &path);
}
}
}
Item::ForeignMod(f) => {
if !self.doc(&f.attrs, prefix) {
return;
}
for foreign in &f.items {
if let Some(name) = foreign_name(foreign) {
self.doc(foreign_attrs(foreign), &format!("{prefix}::{name}"));
}
}
}
Item::Fn(f) => {
let path = format!("{prefix}::{}", f.sig.ident);
if self.doc(&f.attrs, &path) {
self.body(&f.block, &path);
}
}
Item::Struct(s) => {
let path = format!("{prefix}::{}", s.ident);
if self.doc(&s.attrs, &path) {
self.fields(s.fields.iter(), &path);
}
}
Item::Union(u) => {
let path = format!("{prefix}::{}", u.ident);
if self.doc(&u.attrs, &path) {
self.fields(u.fields.named.iter(), &path);
}
}
Item::Enum(e) => {
let path = format!("{prefix}::{}", e.ident);
if !self.doc(&e.attrs, &path) {
return;
}
for variant in &e.variants {
let sub = format!("{path}::{}", variant.ident);
if self.doc(&variant.attrs, &sub) {
self.fields(variant.fields.iter(), &sub);
}
}
}
_ => {
if let Some(name) = item_name(item) {
self.doc(item_attrs(item), &format!("{prefix}::{name}"));
}
}
}
}
fn fields<'f>(&mut self, fields: impl Iterator<Item = &'f syn::Field>, prefix: &str) {
for (index, field) in fields.enumerate() {
let name = field
.ident
.as_ref()
.map_or_else(|| index.to_string(), ToString::to_string);
self.doc(&field.attrs, &format!("{prefix}::{name}"));
}
}
fn body(&mut self, block: &syn::Block, prefix: &str) {
let mut nested = NestedItems {
items: Vec::new(),
hidden: false,
};
nested.visit_block(block);
for item in nested.items {
self.item(item, prefix);
}
}
}
struct NestedItems<'ast> {
items: Vec<&'ast Item>,
hidden: bool,
}
impl NestedItems<'_> {
fn scoped(&mut self, visit: impl FnOnce(&mut Self)) {
let hidden = self.hidden;
visit(self);
self.hidden = hidden;
}
}
impl<'ast> Visit<'ast> for NestedItems<'ast> {
fn visit_item(&mut self, item: &'ast Item) {
if !self.hidden {
self.items.push(item);
}
}
fn visit_attribute(&mut self, attr: &'ast syn::Attribute) {
if !cfg::allows(core::slice::from_ref(attr)) {
self.hidden = true;
}
}
fn visit_expr(&mut self, expr: &'ast Expr) {
self.scoped(|v| syn::visit::visit_expr(v, expr));
}
fn visit_stmt(&mut self, stmt: &'ast syn::Stmt) {
self.scoped(|v| syn::visit::visit_stmt(v, stmt));
}
fn visit_arm(&mut self, arm: &'ast syn::Arm) {
self.scoped(|v| syn::visit::visit_arm(v, arm));
}
fn visit_field_value(&mut self, field: &'ast syn::FieldValue) {
self.scoped(|v| syn::visit::visit_field_value(v, field));
}
}
fn item_attrs(item: &Item) -> &[syn::Attribute] {
match item {
Item::Const(c) => &c.attrs,
Item::ExternCrate(e) => &e.attrs,
Item::Macro(m) => &m.attrs,
Item::Static(s) => &s.attrs,
Item::TraitAlias(t) => &t.attrs,
Item::Type(t) => &t.attrs,
Item::Use(u) => &u.attrs,
_ => &[],
}
}
fn impl_assoc_name(assoc: &syn::ImplItem) -> Option<String> {
match assoc {
syn::ImplItem::Const(c) => Some(c.ident.to_string()),
syn::ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
syn::ImplItem::Type(t) => Some(t.ident.to_string()),
syn::ImplItem::Macro(m) => Some(macro_name(None, &m.mac)),
_ => None,
}
}
fn trait_assoc_name(assoc: &TraitItem) -> Option<String> {
match assoc {
TraitItem::Const(c) => Some(c.ident.to_string()),
TraitItem::Fn(f) => Some(f.sig.ident.to_string()),
TraitItem::Type(t) => Some(t.ident.to_string()),
TraitItem::Macro(m) => Some(macro_name(None, &m.mac)),
_ => None,
}
}
fn foreign_name(foreign: &ForeignItem) -> Option<String> {
match foreign {
ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
ForeignItem::Static(s) => Some(s.ident.to_string()),
ForeignItem::Type(t) => Some(t.ident.to_string()),
ForeignItem::Macro(m) => Some(macro_name(None, &m.mac)),
_ => None,
}
}
fn assoc_attrs(assoc: &syn::ImplItem) -> &[syn::Attribute] {
match assoc {
syn::ImplItem::Const(c) => &c.attrs,
syn::ImplItem::Fn(f) => &f.attrs,
syn::ImplItem::Type(t) => &t.attrs,
syn::ImplItem::Macro(m) => &m.attrs,
_ => &[],
}
}
fn trait_assoc_attrs(assoc: &TraitItem) -> &[syn::Attribute] {
match assoc {
TraitItem::Const(c) => &c.attrs,
TraitItem::Fn(f) => &f.attrs,
TraitItem::Type(t) => &t.attrs,
TraitItem::Macro(m) => &m.attrs,
_ => &[],
}
}
fn foreign_attrs(foreign: &ForeignItem) -> &[syn::Attribute] {
match foreign {
ForeignItem::Fn(f) => &f.attrs,
ForeignItem::Static(s) => &s.attrs,
ForeignItem::Type(t) => &t.attrs,
ForeignItem::Macro(m) => &m.attrs,
_ => &[],
}
}
fn line_of(span: proc_macro2::Span) -> u32 {
let line = span.start().line;
u32::try_from(line).unwrap_or(u32::MAX)
}
fn doc_sources(
attrs: &[syn::Attribute],
file: &str,
read_include: &IncludeRead<'_>,
) -> Vec<DocSource> {
let mut parts: Vec<(String, u32, String)> = Vec::new();
for attr in attrs {
let line = line_of(attr.pound_token.span);
if attr.path().is_ident("doc") {
if let Meta::NameValue(nv) = &attr.meta {
push_doc_expr(&mut parts, &nv.value, line, file, read_include);
}
} else if attr.path().is_ident("cfg_attr")
&& let Meta::List(list) = &attr.meta
&& let Ok(metas) = list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
&& metas.first().is_some_and(cfg_holds)
{
let docs = metas.iter().skip(1).filter_map(|meta| match meta {
Meta::NameValue(nv) if nv.path.is_ident("doc") => Some(nv),
_ => None,
});
for nv in docs {
push_doc_expr(&mut parts, &nv.value, line, file, read_include);
}
}
}
merge_parts(parts)
}
fn cfg_holds(pred: &Meta) -> bool {
let Meta::List(list) = pred else {
return true;
};
if list.path.is_ident("not") {
return !list
.parse_args::<Meta>()
.is_ok_and(|inner| cfg_holds(&inner));
}
let inner = list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated);
if list.path.is_ident("all") {
inner.is_ok_and(|inner| inner.iter().all(cfg_holds))
} else if list.path.is_ident("any") {
inner.is_ok_and(|inner| inner.iter().any(cfg_holds))
} else {
true
}
}
fn push_doc_expr(
parts: &mut Vec<(String, u32, String)>,
value: &Expr,
line: u32,
file: &str,
read_include: &IncludeRead<'_>,
) {
match value {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => parts.push((doc_value(&s.value()), line, file.to_string())),
Expr::Macro(ExprMacro { mac, .. }) if mac.path.is_ident("include_str") => {
if let Ok(s) = parse2::<LitStr>(mac.tokens.clone()) {
push_include(parts, file, read_include, &s.value());
}
}
Expr::Macro(ExprMacro { mac, .. }) if mac.path.is_ident("concat") => {
if let Ok(concat) = parse2::<ConcatParts>(mac.tokens.clone()) {
for part in concat.0 {
match part {
ConcatPart::Lit(s) => {
parts.push((doc_value(&s.value()), line, file.to_string()));
}
ConcatPart::Include(s) => {
push_include(parts, file, read_include, &s.value());
}
}
}
}
}
_ => {}
}
}
fn push_include(
parts: &mut Vec<(String, u32, String)>,
file: &str,
read: &IncludeRead<'_>,
p: &str,
) {
if let Some((path, text)) = read(file, p) {
parts.push((text, 1, path));
}
}
fn merge_parts(parts: Vec<(String, u32, String)>) -> Vec<DocSource> {
let mut out: Vec<DocSource> = Vec::new();
let mut prev: Option<(u32, String)> = None;
for (text, line, file) in parts {
let lines = line_range(&text, line);
let files = vec![file.clone(); lines.len()];
if let Some(last) = out.last_mut() {
if last.text.is_empty() || last.text.ends_with('\n') {
let (pbase, pfile) = prev.unwrap();
let line = if last.lines.is_empty() {
pbase
} else {
*last.lines.last().unwrap() + 1
};
last.lines.push(line);
last.files.push(pfile);
}
last.text.push('\n');
last.text.push_str(&text);
last.lines.extend(lines);
last.files.extend(files);
} else {
out.push(DocSource { text, lines, files });
}
prev = Some((line, file));
}
out
}
fn strip_doc_spaces(text: &str) -> String {
text.split('\n')
.map(|l| l.strip_prefix(' ').unwrap_or(l))
.collect::<Vec<_>>()
.join("\n")
}
fn doc_value(v: &str) -> String {
block_star_strip(&strip_doc_spaces(v))
}
fn unindent(text: &str) -> String {
let min = text
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').count())
.min()
.unwrap_or(0);
if min == 0 {
return text.to_string();
}
text.split('\n')
.map(|l| if l.trim().is_empty() { l } else { &l[min..] })
.collect::<Vec<_>>()
.join("\n")
}
fn block_star_strip(text: &str) -> String {
if !text.contains('\n') {
return text.to_string();
}
let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
let len = lines.len();
let mut changed = false;
if lines.first().is_some_and(|l| l.chars().all(|c| c == '*')) {
lines[0].clear();
changed = true;
}
if len > 1 && !lines[len - 1].is_empty() && lines[len - 1].chars().all(|c| c == '*') {
lines[len - 1].clear();
changed = true;
}
let from = lines
.iter()
.position(|l| !l.trim().is_empty())
.unwrap_or(len);
let to = lines
.iter()
.rposition(|l| !l.trim().is_empty())
.map_or(from, |p| p + 1);
let mut star_col: Option<usize> = None;
for line in &lines[from..to] {
let mut found = false;
for (idx, c) in line.char_indices() {
match c {
'*' => {
match star_col {
None => star_col = Some(idx),
Some(s) if s == idx => {}
_ => return text.to_string(),
}
found = true;
break;
}
' ' | '\t' => {}
_ => return text.to_string(),
}
}
if !found {
return text.to_string();
}
}
let Some(col) = star_col else {
return if changed {
lines.join("\n")
} else {
text.to_string()
};
};
let prefix = lines[from][..col].to_string();
for line in &mut lines {
if let Some(tmp) = line.strip_prefix(&prefix) {
*line = tmp.to_string();
if line == "*" || line.starts_with("* ") || line.starts_with("**") {
line.drain(..1);
}
}
}
lines.join("\n")
}
fn line_range(text: &str, line: u32) -> Vec<u32> {
let n = u32::try_from(text.lines().count()).unwrap_or(u32::MAX);
(line..line.saturating_add(n)).collect()
}
struct ConcatParts(Vec<ConcatPart>);
enum ConcatPart {
Lit(LitStr),
Include(LitStr),
}
enum ConcatArg {
Lit(LitStr),
Include(LitStr),
}
impl Parse for ConcatArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(Lit) {
return Ok(ConcatArg::Lit(input.parse()?));
}
let mac: Macro = input.parse()?;
if !mac.path.is_ident("include_str") {
return Err(syn::Error::new(
mac.bang_token.span,
"unsupported macro in doc concat",
));
}
Ok(ConcatArg::Include(parse2(mac.tokens)?))
}
}
impl Parse for ConcatParts {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args = Punctuated::<ConcatArg, Token![,]>::parse_terminated(input)?;
let parts = args
.into_iter()
.map(|a| match a {
ConcatArg::Lit(l) => ConcatPart::Lit(l),
ConcatArg::Include(l) => ConcatPart::Include(l),
})
.collect();
Ok(ConcatParts(parts))
}
}
fn blocks(src: &DocSource, item: &str, root: &str) -> Vec<DocTest> {
let mut out = Vec::new();
let text = unindent(&src.text);
for f in fence::scan(&text) {
let (counted, info, allow) = classify(&f.info);
if !counted {
continue;
}
let file = src.files[f.line]
.strip_prefix(root)
.and_then(|f| f.strip_prefix('/'))
.unwrap_or(&src.files[f.line]);
out.push(DocTest {
file: file.to_string(),
line: src.lines[f.line],
item: item.to_string(),
info,
code: f.code,
allow,
});
}
out
}
fn classify(info: &str) -> (bool, Vec<String>, bool) {
let Some(tokens) = lang_tokens(info) else {
return (false, Vec::new(), false);
};
let mut seen_rust = false;
let mut seen_other = false;
let mut custom = false;
let mut ignore = false;
let mut allow = false;
let mut out = Vec::new();
for t in tokens {
match t {
"rust" => seen_rust = true,
"custom" => custom = true,
"ignore" => ignore = true,
"should_panic" | "no_run" => {
seen_rust = !seen_other;
out.push(t.to_string());
}
_ if t.starts_with("ignore-") => {
seen_rust = !seen_other;
out.push(t.to_string());
}
"test_harness" | "compile_fail" | "standalone_crate" => {
seen_rust = !seen_other || seen_rust;
out.push(t.to_string());
}
_ if t.starts_with("edition") => out.push(t.to_string()),
"dejadoc" => {
allow = true;
seen_other = true;
}
_ => {
seen_other = true;
out.push(t.to_string());
}
}
}
if custom || (seen_other && !seen_rust) || ignore {
return (false, Vec::new(), false);
}
(true, out, allow)
}
fn lang_tokens(info: &str) -> Option<Vec<&str>> {
let mut out = Vec::new();
let mut start = None;
let mut chars = info.char_indices();
while let Some((i, c)) = chars.next() {
if c == '{' || c == '(' {
if let Some(s) = start.take() {
out.push(&info[s..i]);
}
let close = if c == '{' { '}' } else { ')' };
chars.by_ref().find(|(_, c)| *c == close)?;
} else if c == ',' || c.is_whitespace() {
if let Some(s) = start.take() {
out.push(&info[s..i]);
}
} else if start.is_none() {
start = Some(i);
}
}
if let Some(s) = start {
out.push(&info[s..]);
}
Some(out)
}
fn item_name(item: &Item) -> Option<String> {
Some(match item {
Item::Const(c) => c.ident.to_string(),
Item::Static(s) => s.ident.to_string(),
Item::Type(t) => t.ident.to_string(),
Item::TraitAlias(t) => t.ident.to_string(),
Item::Use(u) => use_name(&u.tree),
Item::ExternCrate(e) => e
.rename
.as_ref()
.map_or_else(|| e.ident.to_string(), |(_, i)| i.to_string()),
Item::Macro(m) => macro_name(m.ident.as_ref(), &m.mac),
_ => return None,
})
}
fn macro_name(ident: Option<&Ident>, mac: &Macro) -> String {
ident.map_or_else(
|| {
mac.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
},
ToString::to_string,
)
}
fn type_name(ty: &Type) -> String {
match ty {
Type::Path(p) => p
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default(),
Type::Group(g) => type_name(&g.elem),
Type::Paren(p) => type_name(&p.elem),
Type::Reference(r) => format!("&{}", type_name(&r.elem)),
other => other.to_token_stream().to_string(),
}
}
fn use_name(tree: &UseTree) -> String {
match tree {
UseTree::Path(p) => use_name(&p.tree),
UseTree::Name(n) => n.ident.to_string(),
UseTree::Rename(r) => r.rename.to_string(),
UseTree::Glob(_) => "*".to_string(),
UseTree::Group(_) => tree.to_token_stream().to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn run(src: &str) -> Vec<DocTest> {
let parsed = syn::parse_file(src).unwrap();
extract(
"mycrate",
"/root/src/lib.rs",
&parsed,
"/root",
&|_f: &str, _p: &str| None,
)
}
fn dt(file: &str, line: u32, item: &str, info: &[&str], code: &str, allow: bool) -> DocTest {
DocTest {
file: file.to_string(),
line,
item: item.into(),
info: info.iter().map(ToString::to_string).collect(),
code: code.into(),
allow,
}
}
#[test]
fn fn_doctest() {
let src = "/// ```rust\n/// fn main() { }\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() { }",
false
)]
);
}
#[test]
fn attributes_recorded() {
let src = "/// ```rust,should_panic\n/// fn main() { panic!(); }\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&["should_panic"],
"fn main() { panic!(); }",
false
)]
);
}
#[test]
fn nested_module_path() {
let src = "pub mod a {\n pub mod b {\n /// ```\n /// fn main() {}\n /// ```\n pub fn c() {}\n }\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::a::b::c",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn file_level_docs() {
let src = "//! ```\n//! fn main() {}\n//! ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate", &[], "fn main() {}", false)]
);
}
#[test]
fn impl_method_path() {
let src = "pub struct S;\nimpl S {\n /// ```rust\n /// fn main() {}\n /// ```\n pub fn m(&self) {}\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::S::m",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn trait_method_path() {
let src =
"pub trait T {\n /// ```\n /// fn main() {}\n /// ```\n fn m(&self);\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
2,
"mycrate::T::m",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn extern_block_item() {
let src = "unsafe extern \"C\" {\n /// ```\n /// fn main() {}\n /// ```\n fn f();\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
2,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn macro_rules_site() {
let src = "/// ```\n/// fn main() {}\n/// ```\nmacro_rules! m { () => {} }\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::m",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn doc_on_use() {
let src = "/// ```\n/// fn main() {}\n/// ```\npub use std::vec;\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::vec",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn doc_on_extern_crate() {
let src = "/// ```\n/// fn main() {}\n/// ```\nextern crate std;\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::std",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn uniformly_indented_fence_unindents() {
let src = "/// ```rust\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn shallow_line_pins_unindent() {
let src = "/// intro\n/// ```\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn blank_lines_do_not_lower_unindent() {
let src =
"/// x\n///\n/// ```\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn block_doc_fence_is_doctest() {
let src =
"/**\n * Use f:\n *\n * ```\n * fn main() { let x = 1; }\n * ```\n */\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
4,
"mycrate::f",
&[],
"fn main() { let x = 1; }",
false
)]
);
}
#[test]
fn block_doc_indented_fence_is_doctest() {
let src = "/**\n * A.\n *\n * ```\n * fn main() {}\n * ```\n */\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
4,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn block_doc_inconsistent_star_column_is_kept() {
let src = "/**\n * A.\n** B.\n */\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn block_doc_blank_inner_line_is_kept() {
let src = "/**\n * A.\n\n * ```\n * fn main() {}\n * ```\n */\npub fn f() {}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 4, "mycrate::f", &[], "", false),
dt("src/lib.rs", 6, "mycrate::f", &[], "", false),
]
);
}
#[test]
fn raw_multiline_doc_stars_are_stripped() {
let src = "#[doc = \" * ```\\n * fn main() {}\\n * ```\"]\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn raw_doc_non_star_last_line_is_kept() {
let src = "#[doc = \" * ```\\n * fn main() {}\\n * last\"]\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() {}\nlast",
false
)]
);
}
#[test]
fn raw_doc_all_star_last_line_is_trimmed() {
let src = "#[doc = \" * ```\\n ***\"]\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate::f", &[], "", false)]
);
}
#[test]
fn single_line_star_fence_is_not_stripped() {
let src = "/// * ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate::f", &[], "", false)]
);
}
#[test]
fn raw_doc_non_star_tail_line_aborts_strip() {
let src = "/// * ```\n/// * x\n/// y\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate::f", &[], "", false)]
);
}
#[test]
fn block_doc_leading_blank_line_still_strips() {
let src = "/**\n *\n * ```\n * fn main() {}\n * ```\n */\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn block_doc_last_line_star_mismatch_aborts_strip() {
let src = "/**\n * ```\n * fn main() {}\n * x\n */\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 2, "mycrate::f", &[], "", false)]
);
}
#[test]
fn block_doc_misaligned_star_fence_is_not_stripped() {
let src = "#[doc = \" * ```\\n * fn main() {}\"]\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate::f", &[], "", false)]
);
}
#[test]
fn whitespace_only_line_is_not_stripped() {
let src = "/// x\n/// \n/// ```\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn indented_code_after_blank_line_is_doctest() {
let src = "/// intro\n///\n/// let x = 1;\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 3, "mycrate::f", &[], "let x = 1;", false)]
);
}
#[test]
fn indented_code_uniformly_indented_is_not_doctest() {
let src = "/// let x = 1;\n/// let y = 2;\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn ignore_is_skipped() {
let src = "/// ```ignore\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn non_rust_is_skipped() {
let src = "/// ```text\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn dejadoc_token_allows() {
let src = "/// ```rust,dejadoc\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate::f", &[], "fn main() {}", true)]
);
}
#[test]
fn dejadoc_alone_is_not_rust() {
let src = "/// ```dejadoc\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn tilde_fence_doctest_extracts() {
let src = "/// ~~~rust\n/// fn main() {}\n/// ~~~\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn min_rustc_recorded() {
let src = "/// ```rust,min rustc 1.45\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&["min", "rustc", "1.45"],
"fn main() {}",
false
)]
);
}
#[test]
fn multiple_fences_one_doc() {
let src = "/// text\n/// ```rust\n/// fn main() {}\n/// ```\n/// more\n/// ```\n/// 1 + 1;\n/// ```\npub fn f() {}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 2, "mycrate::f", &[], "fn main() {}", false),
dt("src/lib.rs", 6, "mycrate::f", &[], "1 + 1;", false),
]
);
}
fn fs_read(src: &std::path::Path) -> impl Fn(&str, &str) -> Option<(String, String)> {
move |_file: &str, p: &str| {
let path = src.join(p);
match std::fs::read_to_string(&path) {
Ok(text) => Some((path.to_string_lossy().into_owned(), text)),
Err(err) => {
std::eprintln!("dejadoc: cannot read doc include {}: {err}", path.display());
None
}
}
}
}
#[test]
fn include_str_doc() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(
src.join("doc.md"),
"See below.\n\n```rust\nlet from = include;\n```\n",
)
.unwrap();
std::fs::write(
src.join("lib.rs"),
"#[doc = include_str!(\"doc.md\")]\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&fs_read(&src),
);
assert_eq!(
dts,
vec![dt(
"src/doc.md",
3,
"mycrate::f",
&[],
"let from = include;",
false
)]
);
}
#[test]
fn concat_include_str_doc() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("doc.md"), "```\nfn main() {}\n```\n").unwrap();
std::fs::write(
src.join("lib.rs"),
"#[doc = concat!(\"prefix \", include_str!(\"doc.md\"))]\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&fs_read(&src),
);
assert_eq!(
dts,
vec![dt(
"src/doc.md",
1,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn include_file_stars_are_kept() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("doc.md"), " * ```\n * fn main() {}\n * ```\n").unwrap();
std::fs::write(
src.join("lib.rs"),
"#[doc = include_str!(\"doc.md\")]\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&fs_read(&src),
);
assert_eq!(
dts,
vec![
dt("src/doc.md", 1, "mycrate::f", &[], "", false),
dt("src/doc.md", 3, "mycrate::f", &[], "", false),
]
);
}
#[test]
fn doc_attribute_literal_form() {
let src = r#"#[doc = "intro\n```rust\nfn main() { }\n```\ntail"]
pub fn escaped() {}
"#;
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
2,
"mycrate::escaped",
&[],
"fn main() { }",
false
)]
);
}
#[test]
fn doc_attribute_raw_string() {
let src = r##"#[doc = r#"raw
intro
```rust
fn main() { }
```"#]
pub fn raw() {}
"##;
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::raw",
&[],
"fn main() { }",
false
)]
);
}
#[test]
fn cfg_attr_doc_opens_the_fence() {
let src = "#[derive(Debug)]\n#[doc(hidden)]\n#[cfg_attr(feature = \"string\", derive(Clone), doc = \"```\", must_use = \"extra\")]\n#[cfg_attr(not(feature = \"string\"), doc = \"```ignore\")]\n/// fn main() { }\n/// ```\npub struct Gated;\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::Gated",
&[],
"fn main() { }",
false
)]
);
}
#[test]
fn cfg_attr_predicate_holds_with_every_cfg_on() {
let src = "#[cfg_attr(all(feature = \"a\", not(feature = \"b\")), doc = \"```\")]\n#[cfg_attr(any(not(doc), feature = \"b\"), doc = \"```\")]\n/// fn main() { }\n/// ```\npub fn gated() {}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
2,
"mycrate::gated",
&[],
"fn main() { }",
false
)]
);
}
#[test]
fn data_item_sites() {
let src = "\
/// ```\n/// fn a() {}\n/// ```\npub const C: i32 = 1;\n\
/// ```\n/// fn b() {}\n/// ```\npub static S: i32 = 1;\n\
/// ```\n/// fn c() {}\n/// ```\npub enum E { X }\n\
/// ```\n/// fn d() {}\n/// ```\npub struct St;\n\
/// ```\n/// fn e() {}\n/// ```\npub union U { f: i32 }\n\
/// ```\n/// fn f() {}\n/// ```\npub type Ty = i32;\n\
/// ```\n/// fn g() {}\n/// ```\npub trait Tr = i32;\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 1, "mycrate::C", &[], "fn a() {}", false),
dt("src/lib.rs", 5, "mycrate::S", &[], "fn b() {}", false),
dt("src/lib.rs", 9, "mycrate::E", &[], "fn c() {}", false),
dt("src/lib.rs", 13, "mycrate::St", &[], "fn d() {}", false),
dt("src/lib.rs", 17, "mycrate::U", &[], "fn e() {}", false),
dt("src/lib.rs", 21, "mycrate::Ty", &[], "fn f() {}", false),
dt("src/lib.rs", 25, "mycrate::Tr", &[], "fn g() {}", false),
]
);
}
#[test]
fn fence_spanning_include() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("doc.md"), "fn main() {}").unwrap();
std::fs::write(
src.join("lib.rs"),
"/// ```\n#[doc = include_str!(\"doc.md\")]\n/// ```\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&fs_read(&src),
);
assert_eq!(
dts,
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn newline_terminated_doc_part_keeps_line_mapping() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(
src.join("lib.rs"),
"#[doc = \"x\\n\"]\n#[doc = \"a\\n```rust\"]\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&|_f: &str, _p: &str| None,
);
assert_eq!(dts, vec![dt("src/lib.rs", 3, "mycrate::f", &[], "", false)]);
}
#[test]
fn empty_first_doc_part_keeps_line_mapping() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(
src.join("lib.rs"),
"///\n/// text\n/// ```rust\npub fn f() {}\n",
)
.unwrap();
let code = std::fs::read_to_string(src.join("lib.rs")).unwrap();
let parsed = syn::parse_file(&code).unwrap();
let dts = extract(
"mycrate",
&src.join("lib.rs").to_string_lossy(),
&parsed,
&dir.path().to_string_lossy(),
&|_f: &str, _p: &str| None,
);
assert_eq!(dts, vec![dt("src/lib.rs", 3, "mycrate::f", &[], "", false)]);
}
#[test]
fn impl_associated_items_carry_doctests() {
let src = "struct S;\nimpl S {\n /// ```\n /// fn main() {}\n /// ```\n const C: i32 = 1;\n /// ```\n /// fn main() { let a = 1; }\n /// ```\n type T = i32;\n}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 3, "mycrate::S::C", &[], "fn main() {}", false),
dt(
"src/lib.rs",
7,
"mycrate::S::T",
&[],
"fn main() { let a = 1; }",
false
)
]
);
}
#[test]
fn trait_associated_items_carry_doctests() {
let src = "trait T {\n /// ```\n /// fn main() {}\n /// ```\n const C: i32;\n /// ```\n /// fn main() { let a = 1; }\n /// ```\n type Out;\n}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 2, "mycrate::T::C", &[], "fn main() {}", false),
dt(
"src/lib.rs",
6,
"mycrate::T::Out",
&[],
"fn main() { let a = 1; }",
false
)
]
);
}
#[test]
fn extern_associated_items_carry_doctests() {
let src = "extern \"C\" {\n /// ```\n /// fn main() {}\n /// ```\n static S: i32;\n /// ```\n /// fn main() { let a = 1; }\n /// ```\n type T;\n}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 2, "mycrate::S", &[], "fn main() {}", false),
dt(
"src/lib.rs",
6,
"mycrate::T",
&[],
"fn main() { let a = 1; }",
false
)
]
);
}
#[test]
fn mod_declaration_carries_its_docs() {
let src = "/// ```\n/// fn main() {}\n/// ```\npub mod m {\n pub fn f() {}\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::m",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn cfg_test_items_are_skipped() {
let src = "\
/// ```\n/// let a = 1;\n/// ```\n#[cfg(test)]\npub fn a() {}\n\
#[cfg(test)]\nmod t {\n /// ```\n /// let b = 2;\n /// ```\n pub fn b() {}\n}\n\
/// ```\n/// let c = 3;\n/// ```\n#[cfg(not(doc))]\npub fn c() {}\n\
/// ```\n/// let d = 4;\n/// ```\n#[cfg(all(not(test), any(doctest, feature = \"x\")))]\npub fn d() {}\n\
pub struct S {\n /// ```\n /// let e = 5;\n /// ```\n #[cfg(test)]\n pub e: u8,\n}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 18, "mycrate::d", &[], "let d = 4;", false)]
);
}
#[test]
fn file_level_cfg_test_skips_the_file() {
let src = "#![cfg(test)]\n/// ```\n/// let a = 1;\n/// ```\npub fn a() {}\n";
assert_eq!(run(src), vec![]);
}
#[test]
fn fields_and_variants_are_walked() {
let src = "\
pub struct S {\n /// ```\n /// let x = 1;\n /// ```\n pub x: u8,\n}\n\
pub struct T(\n /// ```\n /// let t = 2;\n /// ```\n pub u8,\n);\n\
pub enum E {\n /// ```\n /// let v = 3;\n /// ```\n V {\n /// ```\n /// let y = 4;\n /// ```\n y: u8,\n },\n}\n\
pub union U {\n /// ```\n /// let z = 5;\n /// ```\n pub z: u8,\n}\n";
assert_eq!(
run(src),
vec![
dt("src/lib.rs", 2, "mycrate::S::x", &[], "let x = 1;", false),
dt("src/lib.rs", 8, "mycrate::T::0", &[], "let t = 2;", false),
dt("src/lib.rs", 14, "mycrate::E::V", &[], "let v = 3;", false),
dt(
"src/lib.rs",
18,
"mycrate::E::V::y",
&[],
"let y = 4;",
false
),
dt("src/lib.rs", 25, "mycrate::U::z", &[], "let z = 5;", false),
]
);
}
#[test]
fn items_nested_in_bodies_are_walked() {
let src = "\
pub fn outer() {\n /// ```\n /// let a = 1;\n /// ```\n fn inner() {}\n let _ = || {\n /// ```\n /// let b = 2;\n /// ```\n struct Deep;\n };\n}\n\
pub struct S;\nimpl S {\n pub fn m(&self) {\n /// ```\n /// let c = 3;\n /// ```\n fn helper() {}\n }\n}\n\
pub trait Tr {\n fn d(&self) {\n /// ```\n /// let d = 4;\n /// ```\n fn dflt() {}\n }\n}\n";
assert_eq!(
run(src),
vec![
dt(
"src/lib.rs",
2,
"mycrate::outer::inner",
&[],
"let a = 1;",
false
),
dt(
"src/lib.rs",
7,
"mycrate::outer::Deep",
&[],
"let b = 2;",
false
),
dt(
"src/lib.rs",
16,
"mycrate::S::m::helper",
&[],
"let c = 3;",
false
),
dt(
"src/lib.rs",
24,
"mycrate::Tr::d::dflt",
&[],
"let d = 4;",
false
),
]
);
}
#[test]
fn cfg_gated_statements_hide_their_items() {
let src = "\
pub fn outer() {\n #[cfg(test)]\n {\n /// ```\n /// let a = 1;\n /// ```\n fn hidden() {}\n }\n\
#[cfg(test)]\n let _ = { /// ```\n /// let b = 2;\n /// ```\n fn also() {} };\n\
#[cfg(doc)]\n {\n /// ```\n /// let c = 3;\n /// ```\n fn shown() {}\n }\n\
match 0 {\n #[cfg(test)]\n 1 => {\n /// ```\n /// let d = 4;\n /// ```\n fn armed() {}\n }\n\
_ => {\n /// ```\n /// let e = 5;\n /// ```\n fn arm() {}\n }\n }\n\
let _ = P {\n #[cfg(test)]\n a: {\n /// ```\n /// let f = 6;\n /// ```\n fn fa() {}\n },\n\
b: {\n /// ```\n /// let g = 7;\n /// ```\n fn fb() {}\n },\n };\n}\n";
assert_eq!(
run(src),
vec![
dt(
"src/lib.rs",
16,
"mycrate::outer::shown",
&[],
"let c = 3;",
false
),
dt(
"src/lib.rs",
30,
"mycrate::outer::arm",
&[],
"let e = 5;",
false
),
dt(
"src/lib.rs",
45,
"mycrate::outer::fb",
&[],
"let g = 7;",
false
),
]
);
}
#[test]
fn impl_declaration_carries_its_docs() {
let src = "struct S;\n/// ```\n/// fn main() {}\n/// ```\nimpl S {\n pub fn f() {}\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
2,
"mycrate::S",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn reference_self_type_names_its_target() {
let src = "struct S;\nimpl<'a> IntoIterator for &'a mut S {\n /// ```\n /// fn main() {}\n /// ```\n fn into_iter(self) {}\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
3,
"mycrate::&S::into_iter",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn trait_declaration_carries_its_docs() {
let src = "/// ```\n/// fn main() {}\n/// ```\ntrait T {\n fn m(&self);\n}\n";
assert_eq!(
run(src),
vec![dt(
"src/lib.rs",
1,
"mycrate::T",
&[],
"fn main() {}",
false
)]
);
}
#[test]
fn extern_block_carries_its_docs() {
let src = "/// ```\n/// fn main() {}\n/// ```\nextern \"C\" {\n fn f();\n}\n";
assert_eq!(
run(src),
vec![dt("src/lib.rs", 1, "mycrate", &[], "fn main() {}", false)]
);
}
#[test]
fn rust_ignore_fence_is_not_scanned() {
let src = "/// ```rust,ignore\n/// fn main() {}\n/// ```\npub fn f() {}\n";
assert_eq!(run(src), Vec::new());
}
#[test]
fn lang_string_follows_rustdoc() {
let runs: &[(&str, &[&str])] = &[
("no_run,foo", &["no_run", "foo"]),
("ignore-wasm", &["ignore-wasm"]),
("rust (a comment) {.myclass}", &[]),
("rust no_run", &["no_run"]),
("no_run,dejadoc", &["no_run"]),
("compile_fail,E0597", &["compile_fail", "E0597"]),
("edition2021", &["edition2021"]),
("rust (a comment)", &[]),
("rust {.myclass}", &[]),
];
for (info, attrs) in runs {
let src = format!("/// ```{info}\n/// fn main() {{}}\n/// ```\npub fn f() {{}}\n");
assert_eq!(
run(&src),
vec![dt(
"src/lib.rs",
1,
"mycrate::f",
attrs,
"fn main() {}",
info.contains("dejadoc")
)],
"{info}"
);
}
}
#[test]
fn lang_string_skips_follow_rustdoc() {
for info in [
"rust,foo,no_run",
"custom,rust",
"rust ignore",
"dejadoc",
"foo,no_run",
"rust,{unclosed",
"rs",
"E0597",
"shell",
"foo,ignore-wasm",
"rust (unclosed",
] {
let src = format!("/// ```{info}\n/// fn main() {{}}\n/// ```\npub fn f() {{}}\n");
assert_eq!(run(&src), vec![], "{info}");
}
}
}