use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
const NOT_OURS: &[&str] = &[
"all", "and_then", "any", "as_bytes", "as_ref", "as_str", "assert", "assert_eq", "await", "build",
"bytes", "chain", "clone", "cloned", "close", "collect", "contains", "copied", "count",
"dedup", "default", "ends_with", "entry", "enumerate", "eprintln", "expect", "extend",
"filter_map", "find", "first", "flush", "fold", "format", "from", "get", "insert", "into",
"err", "is_empty", "is_err", "is_none", "is_ok", "is_some", "iter", "ok", "join", "keys", "last", "len", "lines", "lock", "map", "map_err", "max",
"min", "next", "ok_or", "ok_or_else", "or_default", "or_insert", "parse", "position",
"println", "push", "read", "recv", "remove", "replace", "rev", "run", "send", "sleep",
"sort", "sort_by", "spawn", "split", "starts_with", "sum", "take", "to_lowercase",
"to_owned", "to_string", "to_uppercase", "to_vec", "trim", "unwrap", "unwrap_or",
"unwrap_or_default", "unwrap_or_else", "values", "with_capacity", "write", "zip",
];
fn repository_root() -> Option<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..");
root.join("docs").is_dir().then_some(root)
}
fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
if path.is_dir() {
if name != "target" && name != "templates" {
rust_files(&path, out);
}
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
fn defined_functions(root: &Path) -> BTreeSet<String> {
let mut files = Vec::new();
rust_files(&root.join("framework/crates"), &mut files);
let mut names = BTreeSet::new();
for file in files {
let Ok(text) = fs::read_to_string(&file) else { continue };
let mut rest = text.as_str();
while let Some(at) = rest.find("fn ") {
let after = &rest[at + 3..];
let ident: String =
after.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
if !ident.is_empty() {
names.insert(ident);
}
rest = after;
}
}
names
}
fn unescape(code: &str) -> String {
code.replace("<", "<").replace(">", ">").replace(""", "\"").replace("&", "&")
}
fn rust_blocks(page: &str) -> Vec<String> {
let mut blocks = Vec::new();
let mut rest = page;
while let Some(open) = rest.find("<pre><code") {
let after = &rest[open..];
let Some(start) = after.find('>') else { break };
let Some(end) = after.find("</code></pre>") else { break };
let block = unescape(&after[start + 1..end]);
let looks_like_rust = ["let ", "fn ", "use ", "async ", ".await", "App::", "Router", "req."]
.iter()
.any(|marker| block.contains(marker));
if looks_like_rust {
blocks.push(block);
}
rest = &after[end + "</code></pre>".len()..];
}
blocks
}
fn method_calls(code: &str) -> BTreeSet<String> {
let bytes = code.as_bytes();
let mut names = BTreeSet::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'.' {
let mut j = i + 1;
if j < bytes.len() && (bytes[j].is_ascii_lowercase() || bytes[j] == b'_') {
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
let mut k = j;
while k < bytes.len() && bytes[k] == b' ' {
k += 1;
}
if k < bytes.len() && bytes[k] == b'(' {
names.insert(code[i + 1..j].to_string());
}
}
}
i += 1;
}
names
}
#[test]
fn every_method_the_documentation_calls_exists_somewhere_in_the_framework() {
let Some(root) = repository_root() else {
println!("skipped: the repository's docs/ directory is not beside this crate");
return;
};
let defined = defined_functions(&root);
assert!(
defined.len() > 1000,
"found only {} functions in the framework — the scan is broken, and a scan that finds \
nothing would pass every page",
defined.len()
);
let mut unknown: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut blocks_checked = 0;
for page in ["guide.html", "cookbook.html", "index.html"] {
let Ok(html) = fs::read_to_string(root.join("docs").join(page)) else { continue };
for block in rust_blocks(&html) {
blocks_checked += 1;
for name in method_calls(&block) {
if !defined.contains(&name) && !NOT_OURS.contains(&name.as_str()) {
unknown.entry(name).or_default().insert(page.to_string());
}
}
}
}
assert!(blocks_checked > 10, "only {blocks_checked} code blocks were checked; the scan is broken");
assert!(
unknown.is_empty(),
"the documentation calls methods that no function in the framework defines: {unknown:?}. \
Fix the example — or, if it is a standard-library method, add it to NOT_OURS."
);
}