use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Instant;
use crate::graph::CodeGraph;
use crate::parse::parser::ParseOptions;
use crate::parse::parser::Parser;
use crate::semantic::analyzer::{AnalyzeOptions, SemanticAnalyzer};
use crate::types::AcbResult;
#[derive(Debug, Clone)]
pub struct ChangeSet {
pub added: Vec<PathBuf>,
pub modified: Vec<PathBuf>,
pub removed: Vec<PathBuf>,
}
impl ChangeSet {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.modified.is_empty() && self.removed.is_empty()
}
pub fn total(&self) -> usize {
self.added.len() + self.modified.len() + self.removed.len()
}
}
#[derive(Debug, Clone)]
pub struct IncrementalResult {
pub units_added: usize,
pub units_removed: usize,
pub units_modified: usize,
pub duration: std::time::Duration,
}
pub struct IncrementalCompiler {
hashes: HashMap<PathBuf, String>,
}
impl IncrementalCompiler {
pub fn new() -> Self {
Self {
hashes: HashMap::new(),
}
}
pub fn from_graph(graph: &CodeGraph) -> Self {
let mut hashes = HashMap::new();
for unit in graph.units() {
let path = &unit.file_path;
if hashes.contains_key(path) {
continue;
}
if let Ok(content) = std::fs::read(path) {
let hash = blake3::hash(&content).to_hex().to_string();
hashes.insert(path.clone(), hash);
}
}
Self { hashes }
}
pub fn detect_changes(&self, dir: &Path) -> AcbResult<ChangeSet> {
let current_files = collect_source_files(dir)?;
let mut added = Vec::new();
let mut modified = Vec::new();
for path in ¤t_files {
let current_hash = match std::fs::read(path) {
Ok(content) => blake3::hash(&content).to_hex().to_string(),
Err(_) => continue,
};
match self.hashes.get(path) {
Some(stored_hash) => {
if *stored_hash != current_hash {
modified.push(path.clone());
}
}
None => {
added.push(path.clone());
}
}
}
let current_set: std::collections::HashSet<&PathBuf> = current_files.iter().collect();
let removed: Vec<PathBuf> = self
.hashes
.keys()
.filter(|p| !current_set.contains(p))
.cloned()
.collect();
Ok(ChangeSet {
added,
modified,
removed,
})
}
pub fn recompile(
&mut self,
dir: &Path,
changes: &ChangeSet,
) -> AcbResult<(CodeGraph, IncrementalResult)> {
let start = Instant::now();
let changed_file_count = changes.total();
tracing::info!(
"Incremental: {} added, {} modified, {} removed",
changes.added.len(),
changes.modified.len(),
changes.removed.len()
);
let parser = Parser::new();
let parse_result = parser.parse_directory(dir, &ParseOptions::default())?;
let analyzer = SemanticAnalyzer::new();
let graph = analyzer.analyze(parse_result.units, &AnalyzeOptions::default())?;
self.hashes.clear();
for unit in graph.units() {
let path = &unit.file_path;
if self.hashes.contains_key(path) {
continue;
}
if let Ok(content) = std::fs::read(path) {
let hash = blake3::hash(&content).to_hex().to_string();
self.hashes.insert(path.clone(), hash);
}
}
let duration = start.elapsed();
let result = IncrementalResult {
units_added: changes.added.len(),
units_removed: changes.removed.len(),
units_modified: changes.modified.len(),
duration,
};
tracing::info!(
"Incremental recompile: {} changed files in {:.2?}",
changed_file_count,
duration
);
Ok((graph, result))
}
}
impl Default for IncrementalCompiler {
fn default() -> Self {
Self::new()
}
}
fn collect_source_files(dir: &Path) -> AcbResult<Vec<PathBuf>> {
use ignore::WalkBuilder;
let extensions = ["rs", "py", "ts", "tsx", "js", "jsx", "go"];
let mut files = Vec::new();
let walker = WalkBuilder::new(dir).hidden(true).git_ignore(true).build();
for entry in walker {
let entry = entry
.map_err(|e| crate::AcbError::Io(std::io::Error::other(format!("Walk error: {e}"))))?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if extensions.contains(&ext) {
files.push(path.to_path_buf());
}
}
}
}
Ok(files)
}