use super::diagnostic::{Applicability, Fix};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixOutcome {
pub output: String,
pub applied: usize,
pub skipped_conflicts: usize,
}
pub fn apply_fixes(source: &str, fixes: &[Fix], include_unsafe: bool) -> FixOutcome {
let mut eligible: Vec<(usize, usize, &Fix)> = fixes
.iter()
.filter(|f| include_unsafe || f.applicability == Applicability::Safe)
.map(|f| {
let start = f.edits.iter().map(|e| e.start).min().unwrap_or(0);
let end = f.edits.iter().map(|e| e.end).max().unwrap_or(0);
(start, end, f)
})
.collect();
eligible.sort_by_key(|&(start, end, _)| (start, end));
let mut occupied: Vec<(usize, usize)> = Vec::new();
let mut accepted: Vec<&super::diagnostic::Edit> = Vec::new();
let mut applied = 0usize;
let mut skipped_conflicts = 0usize;
for (_, _, fix) in eligible {
let mut edits: Vec<_> = fix.edits.iter().collect();
edits.sort_by_key(|e| (e.start, e.end));
let malformed = edits.is_empty()
|| edits
.iter()
.any(|e| e.start > e.end || e.end > source.len())
|| edits.windows(2).any(|pair| pair[1].start < pair[0].end);
if malformed || edits.iter().any(|e| overlaps(&occupied, e.start, e.end)) {
skipped_conflicts += 1;
continue;
}
for edit in edits {
let at = occupied.partition_point(|&(_, end)| end <= edit.start);
occupied.insert(at, (edit.start, edit.end));
accepted.push(edit);
}
applied += 1;
}
accepted.sort_by_key(|e| (e.start, e.end));
let mut output = source.to_string();
for edit in accepted.iter().rev() {
output.replace_range(edit.start..edit.end, &edit.content);
}
FixOutcome {
output,
applied,
skipped_conflicts,
}
}
fn overlaps(occupied: &[(usize, usize)], start: usize, end: usize) -> bool {
let at = occupied.partition_point(|&(_, e)| e <= start);
occupied.get(at).is_some_and(|&(s, _)| s < end)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::diagnostic::Edit;
fn safe(start: usize, end: usize, content: &str) -> Fix {
Fix::safe(start, end, content, "test")
}
#[test]
fn applies_single_fix() {
let out = apply_fixes("$$x$$", &[safe(0, 2, "\\[")], false);
assert_eq!(out.output, "\\[x$$");
assert_eq!(out.applied, 1);
assert_eq!(out.skipped_conflicts, 0);
}
#[test]
fn applies_multiple_fixes_right_to_left() {
let out = apply_fixes("$$x$$", &[safe(0, 2, "\\["), safe(3, 5, "\\]")], false);
assert_eq!(out.output, "\\[x\\]");
assert_eq!(out.applied, 2);
}
#[test]
fn skips_unsafe_unless_opted_in() {
let fixes = [Fix::unsafe_(0, 6, "", "delete")];
let kept = apply_fixes("abcdef", &fixes, false);
assert_eq!(kept.output, "abcdef");
assert_eq!(kept.applied, 0);
let applied = apply_fixes("abcdef", &fixes, true);
assert_eq!(applied.output, "");
assert_eq!(applied.applied, 1);
}
#[test]
fn drops_overlapping_fixes() {
let out = apply_fixes("abcdef", &[safe(0, 3, "X"), safe(2, 5, "Y")], false);
assert_eq!(out.output, "Xdef");
assert_eq!(out.applied, 1);
assert_eq!(out.skipped_conflicts, 1);
}
#[test]
fn adjacent_fixes_do_not_conflict() {
let out = apply_fixes("abcd", &[safe(0, 2, "X"), safe(2, 4, "Y")], false);
assert_eq!(out.output, "XY");
assert_eq!(out.applied, 2);
assert_eq!(out.skipped_conflicts, 0);
}
#[test]
fn applies_multi_edit_fix() {
let fix = Fix::safe_edits(vec![Edit::new(0, 2, "\\["), Edit::new(3, 5, "\\]")], "swap");
let out = apply_fixes("$$x$$", &[fix], false);
assert_eq!(out.output, "\\[x\\]");
assert_eq!(out.applied, 1);
assert_eq!(out.skipped_conflicts, 0);
}
#[test]
fn multi_edit_fix_is_atomic_on_conflict() {
let single = safe(0, 2, "Z");
let multi = Fix::safe_edits(vec![Edit::new(1, 3, "X"), Edit::new(6, 8, "Y")], "pair");
let out = apply_fixes("abcdefgh", &[multi, single], false);
assert_eq!(out.output, "Zcdefgh");
assert_eq!(out.applied, 1);
assert_eq!(out.skipped_conflicts, 1);
}
#[test]
fn multi_edit_fix_conflicts_detected_out_of_order() {
let pair = Fix::safe_edits(vec![Edit::new(0, 2, "A"), Edit::new(6, 8, "B")], "pair");
let between = safe(3, 4, "-");
let clash = safe(7, 9, "!");
let out = apply_fixes("abcdefghij", &[pair, between, clash], false);
assert_eq!(out.output, "Ac-efBij");
assert_eq!(out.applied, 2);
assert_eq!(out.skipped_conflicts, 1);
}
#[test]
fn drops_internally_overlapping_fix() {
let bad = Fix::safe_edits(
vec![Edit::new(0, 3, "X"), Edit::new(2, 5, "Y")],
"malformed",
);
let out = apply_fixes("abcdef", &[bad], false);
assert_eq!(out.output, "abcdef");
assert_eq!(out.applied, 0);
assert_eq!(out.skipped_conflicts, 1);
}
#[test]
fn drops_out_of_bounds_fix() {
let out = apply_fixes("abc", &[safe(1, 9, "X")], false);
assert_eq!(out.output, "abc");
assert_eq!(out.applied, 0);
assert_eq!(out.skipped_conflicts, 1);
}
}