#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use std::fs;
use std::path::{Path, PathBuf};
const ALLOWED: &[&str] = &["fn syntax("];
fn sources() -> Vec<PathBuf> {
let mut out = Vec::new();
let mut pending = vec![Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
while let Some(dir) = pending.pop() {
for entry in fs::read_dir(&dir).unwrap() {
let p = entry.unwrap().path();
if p.is_dir() {
pending.push(p);
} else if p.extension().is_some_and(|e| e == "rs")
&& p.file_name().is_some_and(|n| n != "tests.rs")
{
out.push(p);
}
}
}
out
}
fn syntax_names(text: &str) -> Vec<String> {
let mut names = Vec::new();
let mut rest = text;
while let Some(at) = rest.find("use oneyaml_syntax") {
let stmt = &rest[at..];
let end = stmt.find(';').unwrap_or(stmt.len());
for word in stmt[..end].split(|c: char| !(c.is_alphanumeric() || c == '_')) {
let is_type = word.chars().next().is_some_and(char::is_uppercase);
if is_type {
names.push(word.to_owned());
}
}
rest = &stmt[end..];
}
names
}
fn mentions(sig: &str, name: &str) -> bool {
sig.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.any(|w| w == name)
}
fn public_signatures(text: &str) -> Vec<(usize, String)> {
let lines: Vec<&str> = text.lines().collect();
let mut out = Vec::new();
let mut i = 0;
while i < lines.len() {
let t = lines[i].trim_start();
let public = t.starts_with("pub ") && !t.starts_with("pub(");
let hidden = i > 0 && lines[i - 1].trim() == "#[doc(hidden)]";
if public && !hidden {
let mut sig = String::new();
let mut j = i;
while j < lines.len() {
sig.push_str(lines[j]);
sig.push('\n');
let l = lines[j].trim_end();
if l.ends_with('{') || l.ends_with(';') || l.ends_with(',') || l.ends_with('}') {
break;
}
j += 1;
}
if let Some(open) = sig
.find('(')
.filter(|_| sig.trim_start().starts_with("pub struct"))
{
if !sig[open..].contains("pub ") {
sig.truncate(open);
}
}
out.push((i + 1, sig));
i = j;
}
i += 1;
}
out
}
#[test]
fn stable_api_names_no_syntax_type() {
let files = sources();
assert!(files.len() > 10, "found the library sources");
let mut leaks = Vec::new();
for file in files {
let text = fs::read_to_string(&file).unwrap();
let library = text.split("#[cfg(test)]\nmod ").next().unwrap_or("");
let names = syntax_names(library);
for (line, sig) in public_signatures(library) {
if ALLOWED.iter().any(|a| sig.contains(a)) {
continue;
}
let leak = sig.contains("oneyaml_syntax") || names.iter().any(|n| mentions(&sig, n));
if leak {
leaks.push(format!("{}:{line}: {}", file.display(), sig.trim()));
}
}
}
assert!(
leaks.is_empty(),
"syntax types in the stable API:\n{}",
leaks.join("\n")
);
}
#[test]
fn the_scan_finds_leaks() {
let src = "use oneyaml_syntax::{Encoding, tree::SyntaxTree as Tree};\n\
pub fn a() -> Encoding {\n}\n\
pub fn syntax(&self) -> &Tree {\n}\n\
#[doc(hidden)]\npub fn hidden() -> Tree {\n}\n\
pub use oneyaml_syntax as syntax;\n\
pub struct S {\n pub field: Tree,\n}\n\
pub fn fine() -> u8 {\n}\n\
pub struct Handle(Tree);\n\
pub struct Open(pub Tree);\n";
let names = syntax_names(src);
assert!(names.contains(&"Encoding".to_owned()) && names.contains(&"Tree".to_owned()));
let flagged: Vec<usize> = public_signatures(src)
.into_iter()
.filter(|(_, sig)| !ALLOWED.iter().any(|a| sig.contains(a)))
.filter(|(_, sig)| sig.contains("oneyaml_syntax") || names.iter().any(|n| mentions(sig, n)))
.map(|(l, _)| l)
.collect();
assert_eq!(flagged, vec![2, 9, 11, 16], "{flagged:?}");
}