use std::collections::HashMap;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use owo_colors::OwoColorize;
use crate::{
FixMode,
finding::{Finding, Fix, FixDisposition},
models::AsDocument,
registry::{FindingRegistry, input::InputKey, input::InputRegistry},
};
#[derive(Debug)]
pub struct FixResult {
pub applied_count: usize,
pub failed_count: usize,
}
pub fn apply_fixes(
fix_mode: FixMode,
results: &FindingRegistry,
registry: &InputRegistry,
) -> Result<FixResult> {
let mut fixes_by_input: HashMap<&InputKey, Vec<(&Fix, &Finding)>> = HashMap::new();
let mut total_fixes = 0;
for finding in results.fixable_findings() {
total_fixes += finding.fixes.len();
for fix in &finding.fixes {
let fix = match (fix_mode, fix.disposition) {
(FixMode::Safe, FixDisposition::Safe) => fix,
(FixMode::UnsafeOnly, FixDisposition::Unsafe) => fix,
(FixMode::All, _) => fix,
_ => continue,
};
fixes_by_input
.entry(fix.key)
.or_default()
.push((fix, finding));
}
}
if fixes_by_input.is_empty() {
if total_fixes > 0 {
anstream::eprintln!(
"No fixes available to apply ({total_fixes} held back by fix mode)."
);
} else {
anstream::eprintln!("No fixes available to apply.");
}
return Ok(FixResult {
applied_count: 0,
failed_count: 0,
});
}
let mut applied_fixes = Vec::new();
let mut failed_fixes = Vec::new();
let mut total_applied = 0;
for (input_key, fixes) in &fixes_by_input {
let InputKey::Local(local) = input_key else {
panic!("can't apply fixes to non-local inputs");
};
let input = registry.get_input(input_key);
let file_path = &local.given_path;
let mut file_applied_fixes = Vec::new();
let mut current_document = input.as_document().clone();
for (fix, finding) in fixes {
match fix.apply(¤t_document) {
Ok(new_document) => {
current_document = new_document;
file_applied_fixes.push((finding.ident, fix, finding));
total_applied += 1;
}
Err(e) => {
failed_fixes.push((
finding.ident,
file_path,
format!("conflict after applying previous fixes: {e}"),
));
}
}
}
if current_document.source() != input.as_document().source() {
let num_fixes = file_applied_fixes.len();
std::fs::write(file_path, current_document.source())
.with_context(|| format!("failed to update {file_path}"))?;
applied_fixes.push((file_path, num_fixes));
}
}
if !applied_fixes.is_empty() || !failed_fixes.is_empty() {
print_summary(&applied_fixes, &failed_fixes);
}
Ok(FixResult {
applied_count: total_applied,
failed_count: failed_fixes.len(),
})
}
fn print_summary(
applied_fixes: &[(&Utf8PathBuf, usize)],
failed_fixes: &[(&str, &Utf8PathBuf, String)],
) {
anstream::eprintln!("\n{}", "Fix Summary".green().bold());
if !applied_fixes.is_empty() {
anstream::eprintln!(
"Successfully applied fixes to {} files:",
applied_fixes.len()
);
for (file_path, num_fixes) in applied_fixes {
anstream::eprintln!(" {}: {} fixes", file_path, num_fixes);
}
}
if !failed_fixes.is_empty() {
anstream::eprintln!("Failed to apply {} fixes:", failed_fixes.len());
for (ident, file_path, error) in failed_fixes {
anstream::eprintln!(" {}: {} ({})", ident, file_path, error);
}
}
}