use once_cell::sync::Lazy;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::HashMap;
use std::sync::Mutex;
static COMPILE_TIME_TASK_REGISTRY: Lazy<Mutex<CompileTimeTaskRegistry>> =
Lazy::new(|| Mutex::new(CompileTimeTaskRegistry::new()));
#[derive(Debug, Clone)]
pub struct TaskInfo {
pub id: String,
pub dependencies: Vec<String>,
pub file_path: String,
}
#[derive(Debug)]
pub struct CompileTimeTaskRegistry {
tasks: HashMap<String, TaskInfo>,
dependency_graph: HashMap<String, Vec<String>>,
}
#[allow(dead_code)]
impl CompileTimeTaskRegistry {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
dependency_graph: HashMap::new(),
}
}
pub fn register_task(&mut self, task_info: TaskInfo) -> Result<(), CompileTimeError> {
let task_id = &task_info.id;
if let Some(existing) = self.tasks.get(task_id) {
return Err(CompileTimeError::DuplicateTaskId {
task_id: task_id.clone(),
existing_location: existing.file_path.clone(),
duplicate_location: task_info.file_path.clone(),
});
}
self.dependency_graph
.insert(task_id.clone(), task_info.dependencies.clone());
self.tasks.insert(task_id.clone(), task_info);
Ok(())
}
pub fn validate_dependencies(&self, task_id: &str) -> Result<(), CompileTimeError> {
let is_test_env = std::env::var("CARGO_CRATE_NAME")
.map(|name| name.contains("test") || name == "cloacina")
.unwrap_or(false)
|| std::env::var("CARGO_PKG_NAME")
.map(|name| name.contains("test") || name == "cloacina")
.unwrap_or(false);
if is_test_env && !self.tasks.contains_key(task_id) {
return Ok(());
}
let task = self
.tasks
.get(task_id)
.ok_or_else(|| CompileTimeError::TaskNotFound(task_id.to_string()))?;
for dependency in &task.dependencies {
if !self.tasks.contains_key(dependency) {
if is_test_env {
continue;
}
return Err(CompileTimeError::MissingDependency {
task_id: task_id.to_string(),
dependency: dependency.clone(),
task_location: task.file_path.clone(),
});
}
}
Ok(())
}
#[allow(dead_code)]
pub fn validate_single_dependency(&self, dependency: &str) -> Result<(), CompileTimeError> {
if !self.tasks.contains_key(dependency) {
return Err(CompileTimeError::MissingDependency {
task_id: "unknown".to_string(),
dependency: dependency.to_string(),
task_location: "unknown".to_string(),
});
}
Ok(())
}
pub fn detect_cycles(&self) -> Result<(), CompileTimeError> {
let is_test_env = std::env::var("CARGO_CRATE_NAME")
.map(|name| name.contains("test") || name == "cloacina")
.unwrap_or(false)
|| std::env::var("CARGO_PKG_NAME")
.map(|name| name.contains("test") || name == "cloacina")
.unwrap_or(false);
if is_test_env {
return Ok(());
}
let mut visited = HashMap::new();
let mut rec_stack = HashMap::new();
for task_id in self.tasks.keys() {
if !visited.get(task_id).unwrap_or(&false) {
self.dfs_cycle_detection(task_id, &mut visited, &mut rec_stack, &mut Vec::new())?;
}
}
Ok(())
}
fn dfs_cycle_detection(
&self,
task_id: &str,
visited: &mut HashMap<String, bool>,
rec_stack: &mut HashMap<String, bool>,
path: &mut Vec<String>,
) -> Result<(), CompileTimeError> {
visited.insert(task_id.to_string(), true);
rec_stack.insert(task_id.to_string(), true);
path.push(task_id.to_string());
if let Some(dependencies) = self.dependency_graph.get(task_id) {
for dependency in dependencies {
if !visited.get(dependency).unwrap_or(&false) {
self.dfs_cycle_detection(dependency, visited, rec_stack, path)?;
} else if *rec_stack.get(dependency).unwrap_or(&false) {
let cycle_start = path.iter().position(|x| x == dependency).unwrap_or(0);
let cycle: Vec<String> = path[cycle_start..].to_vec();
return Err(CompileTimeError::CircularDependency {
cycle: cycle.clone(),
task_locations: cycle
.iter()
.filter_map(|id| self.tasks.get(id))
.map(|task| task.file_path.clone())
.collect(),
});
}
}
}
rec_stack.insert(task_id.to_string(), false);
path.pop();
Ok(())
}
pub fn get_all_task_ids(&self) -> Vec<String> {
self.tasks.keys().cloned().collect()
}
#[allow(dead_code)]
pub fn clear(&mut self) {
self.tasks.clear();
self.dependency_graph.clear();
}
#[allow(dead_code)]
pub fn size(&self) -> usize {
self.tasks.len()
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum CompileTimeError {
DuplicateTaskId {
task_id: String,
existing_location: String,
duplicate_location: String,
},
MissingDependency {
task_id: String,
dependency: String,
task_location: String,
},
CircularDependency {
cycle: Vec<String>,
task_locations: Vec<String>,
},
TaskNotFound(String),
}
impl CompileTimeError {
pub fn to_compile_error(&self) -> TokenStream {
match self {
CompileTimeError::DuplicateTaskId {
task_id,
existing_location,
duplicate_location,
} => {
let msg = format!(
"Duplicate task ID '{}'. Already defined at '{}', redefined at '{}'",
task_id, existing_location, duplicate_location
);
quote! { compile_error!(#msg); }
}
CompileTimeError::MissingDependency {
task_id,
dependency,
task_location,
} => {
let registry = get_registry().lock().unwrap();
let available_tasks = registry.get_all_task_ids();
let suggestions = find_similar_task_names(dependency, &available_tasks);
let mut msg = format!(
"Task '{}' depends on undefined task '{}'\n\n",
task_id, dependency
);
if !suggestions.is_empty() {
msg.push_str(&format!(
"Did you mean one of these?\n {}\n\n",
suggestions.join("\n ")
));
}
msg.push_str(&format!(
"Available tasks: [{}]\n\n",
available_tasks.join(", ")
));
if task_location != "unknown" {
msg.push_str(&format!("Task defined at: {}", task_location));
}
quote! { compile_error!(#msg); }
}
CompileTimeError::CircularDependency {
cycle,
task_locations,
} => {
let cycle_str = cycle.join(" -> ");
let locations_str = task_locations.join(", ");
let msg = format!(
"Circular dependency detected: {} (defined at: {})",
cycle_str, locations_str
);
quote! { compile_error!(#msg); }
}
CompileTimeError::TaskNotFound(task_id) => {
let msg = format!("Task '{}' not found in registry", task_id);
quote! { compile_error!(#msg); }
}
}
}
}
pub fn get_registry() -> &'static Lazy<Mutex<CompileTimeTaskRegistry>> {
&COMPILE_TIME_TASK_REGISTRY
}
fn find_similar_task_names(target: &str, available: &[String]) -> Vec<String> {
available
.iter()
.filter_map(|name| {
let distance = levenshtein_distance(target, name);
if distance <= 2 && distance < target.len() / 2 {
Some(name.clone())
} else {
None
}
})
.take(3)
.collect()
}
#[allow(clippy::needless_range_loop)] fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_len = a.len();
let b_len = b.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
for (i, row) in matrix.iter_mut().enumerate().take(a_len + 1) {
row[0] = i;
}
for j in 0..=b_len {
matrix[0][j] = j;
}
for i in 1..=a_len {
for j in 1..=b_len {
let cost = if a.chars().nth(i - 1) == b.chars().nth(j - 1) {
0
} else {
1
};
matrix[i][j] = std::cmp::min(
std::cmp::min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1),
matrix[i - 1][j - 1] + cost,
);
}
}
matrix[a_len][b_len]
}