use std::path::{Path, PathBuf};
pub(crate) fn rs_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
walk(dir, &mut out);
out
}
pub(crate) fn rs_files_in(rel: &str) -> Vec<PathBuf> {
rs_files(&Path::new(env!("CARGO_MANIFEST_DIR")).join(rel))
}
pub(crate) fn production_code(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.take_while(|line| !line.trim_start().starts_with("#[cfg(test)]"))
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn calls<'a>(code: &'a str, opener: &str) -> Vec<&'a str> {
let mut out = Vec::new();
let mut from = 0;
while let Some(found) = code[from..].find(opener) {
let start = from + found;
let end = start + call_len(&code[start..]);
out.push(&code[start..end]);
from = end.max(start + 1);
}
out
}
fn call_len(call: &str) -> usize {
let bytes = call.as_bytes();
let mut depth = 0usize;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return i + 1;
}
}
b'"' => i = string_end(bytes, i, None),
b'r' => {
if let Some(hashes) = raw_string_hashes(bytes, i) {
i = string_end(bytes, i + 1 + hashes, Some(hashes));
}
}
b'\'' => {
if bytes.get(i + 1) == Some(&b'\\') {
i = bytes[i + 2..]
.iter()
.position(|&b| b == b'\'')
.map_or(bytes.len(), |n| i + 2 + n);
} else if bytes.get(i + 2) == Some(&b'\'') {
i += 2;
}
}
_ => {}
}
i += 1;
}
bytes.len()
}
fn string_end(bytes: &[u8], open: usize, raw: Option<usize>) -> usize {
let hashes = raw.unwrap_or(0);
let mut i = open + 1;
while i < bytes.len() {
if raw.is_none() && bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'"' && bytes[i + 1..].iter().take(hashes).all(|&b| b == b'#') {
return i + hashes;
}
i += 1;
}
bytes.len()
}
fn raw_string_hashes(bytes: &[u8], at: usize) -> Option<usize> {
if at > 0 && (bytes[at - 1].is_ascii_alphanumeric() || bytes[at - 1] == b'_') {
return None;
}
let hashes = bytes[at + 1..].iter().take_while(|&&b| b == b'#').count();
(bytes.get(at + 1 + hashes) == Some(&b'"')).then_some(hashes)
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).expect("a readable source directory") {
let path = entry.expect("a readable directory entry").path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
#[test]
fn no_changelog_entry_appears_under_two_versions() {
let Some(changelog) = changelog() else { return };
let mut seen: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut version = "<before any version heading>";
for line in changelog.lines() {
if let Some(rest) = line.strip_prefix("## ") {
version = rest.trim();
} else if let Some(entry) = line.strip_prefix("- ") {
let entry = entry.trim();
if entry.len() > 24 {
seen.entry(entry).or_default().push(version);
}
}
}
let duplicated: Vec<_> = seen
.iter()
.filter(|(_, versions)| {
let mut distinct = versions.to_vec();
distinct.sort_unstable();
distinct.dedup();
distinct.len() > 1
})
.map(|(entry, versions)| format!("{versions:?}: {}…", &entry[..40.min(entry.len())]))
.collect();
assert!(
duplicated.is_empty(),
"these entries appear under more than one version — an [Unreleased] \\
note has merged into a released section:\n{}",
duplicated.join("\n")
);
}
#[test]
fn no_changelog_section_repeats_a_category() {
let Some(changelog) = changelog() else { return };
let mut offenders = Vec::new();
let mut version = "<before any version heading>";
let mut categories: Vec<&str> = Vec::new();
let mut flush = |version: &str, categories: &mut Vec<&str>| {
let mut sorted = categories.clone();
sorted.sort_unstable();
let before = sorted.len();
sorted.dedup();
if sorted.len() != before {
offenders.push(version.to_string());
}
categories.clear();
};
for line in changelog.lines() {
if let Some(rest) = line.strip_prefix("## ") {
flush(version, &mut categories);
version = rest.trim();
} else if let Some(rest) = line.strip_prefix("### ") {
categories.push(rest.trim());
}
}
flush(version, &mut categories);
const SHIPPED_THIS_WAY: [&str; 2] = ["[1.8.0] — 2026-08-28", "[0.14.0] — 2026-07-20"];
offenders.retain(|version| !SHIPPED_THIS_WAY.contains(&version.as_str()));
assert!(
offenders.is_empty(),
"these versions carry the same category heading twice, which hides \
whatever is under the second one from the release's version \
decision: {offenders:?}"
);
}
fn changelog() -> Option<String> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("CHANGELOG.md");
std::fs::read_to_string(&path).ok()
}
#[test]
fn calls_ignore_parens_inside_literals() {
let code =
r##"println!("step 1) {}", path.display()); other(); println!(r#"a ) "b" "#, x);"##;
let found = super::calls(code, "println!(");
assert_eq!(found.len(), 2, "{found:?}");
assert!(found[0].ends_with("path.display())"), "{}", found[0]);
assert!(found[1].ends_with("x)"), "{}", found[1]);
}
#[test]
fn calls_handle_char_literals_lifetimes_and_escapes() {
let code = r#"print!("{}", f(')', '"', "\"", &'a str)); tail()"#;
let found = super::calls(code, "print!(");
assert_eq!(found, [r#"print!("{}", f(')', '"', "\"", &'a str))"#]);
}
#[test]
fn calls_visit_each_macro_once_and_survive_an_unclosed_call() {
let code = "eprintln!(\"a\"); println!(\"b\"); println!(\"c\"";
let found = super::calls(code, "println!(");
assert_eq!(
found,
["println!(\"a\")", "println!(\"b\")", "println!(\"c\""]
);
}
}