use std::collections::{BTreeSet, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use tracing::info;
const ALEF_HEADER_MARKERS: &[&str] = &[
"auto-generated by alef",
"AUTO-GENERATED by alef",
"Generated by alef",
"DO NOT EDIT",
];
pub fn cleanup_orphaned_files(current_gen_paths: &HashSet<PathBuf>) -> anyhow::Result<usize> {
if current_gen_paths.is_empty() {
return Ok(0);
}
let normalized: HashSet<PathBuf> = current_gen_paths
.iter()
.map(|p| p.canonicalize().unwrap_or_else(|_| p.clone()))
.collect();
let touched_dirs: BTreeSet<PathBuf> = current_gen_paths
.iter()
.filter_map(|p| p.parent().map(|d| d.to_path_buf()))
.collect();
let mut removed_count = 0;
let mut visited_dirs: HashSet<PathBuf> = HashSet::new();
for dir in &touched_dirs {
if !dir.exists() {
continue;
}
let canonical_dir = dir.canonicalize().unwrap_or_else(|_| dir.clone());
if !visited_dirs.insert(canonical_dir.clone()) {
continue;
}
removed_count += cleanup_dir_recursive(&canonical_dir, &normalized, &touched_dirs)?;
}
Ok(removed_count)
}
fn cleanup_dir_recursive(
dir: &Path,
normalized_gen_paths: &HashSet<PathBuf>,
touched_dirs: &BTreeSet<PathBuf>,
) -> anyhow::Result<usize> {
let mut removed_count = 0;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let canonical_sub = path.canonicalize().unwrap_or_else(|_| path.clone());
let descend = touched_dirs
.iter()
.any(|td| td == &canonical_sub || td.starts_with(&canonical_sub));
if descend {
removed_count += cleanup_dir_recursive(&path, normalized_gen_paths, touched_dirs)?;
}
continue;
}
if !has_alef_header(&path)? {
continue;
}
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
if !normalized_gen_paths.contains(&canonical_path) {
info!("Removing stale alef-generated file: {}", path.display());
fs::remove_file(&path)?;
removed_count += 1;
}
}
Ok(removed_count)
}
fn has_alef_header(path: &Path) -> anyhow::Result<bool> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => {
return Ok(false);
}
};
let first_lines = content.lines().take(5).collect::<Vec<_>>().join("\n");
for marker in ALEF_HEADER_MARKERS {
if first_lines.contains(marker) {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::cleanup_orphaned_files;
use std::collections::HashSet;
use std::fs;
#[test]
fn cleanup_removes_orphan_with_generated_by_alef_header() {
let tempdir = tempfile::tempdir().expect("tempdir");
let package_dir = tempdir.path().join("packages/kotlin/src/main/kotlin/dev/demo");
fs::create_dir_all(&package_dir).expect("create package dir");
let current_file = package_dir.join("GraphQLRouteConfig.kt");
let stale_file = package_dir.join("DefaultClient.kt");
fs::write(
¤t_file,
"// Generated by alef. Do not edit by hand.\n\nclass GraphQLRouteConfig\n",
)
.expect("write current file");
fs::write(
&stale_file,
"// Generated by alef. Do not edit by hand.\n\nclass GraphQLRouteConfig\n",
)
.expect("write stale file");
let current_gen_paths = HashSet::from([current_file.clone()]);
let removed = cleanup_orphaned_files(¤t_gen_paths).expect("cleanup");
assert_eq!(removed, 1);
assert!(current_file.exists());
assert!(!stale_file.exists());
}
}