use std::path::Path;
use crate::config::LintConfig;
use crate::linter::check::check_source;
use crate::linter::diagnostic::{Applicability, Diagnostic, Fix};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Applied {
pub output: String,
pub applied: usize,
}
pub fn apply_fixes(source: &str, diagnostics: &[Diagnostic], include_unsafe: bool) -> Applied {
let mut fixes: Vec<&Fix> = diagnostics
.iter()
.flat_map(|diag| diag.fixes.iter())
.filter(|fix| include_unsafe || fix.applicability == Applicability::Safe)
.collect();
fixes.sort_by_key(|fix| (fix.start, fix.end));
let mut accepted: Vec<&Fix> = Vec::with_capacity(fixes.len());
let mut last_end = 0usize;
for fix in fixes {
if fix.start >= last_end {
last_end = fix.end;
accepted.push(fix);
}
}
let mut output = source.to_string();
for fix in accepted.iter().rev() {
output.replace_range(fix.start..fix.end, &fix.content);
}
Applied {
output,
applied: accepted.len(),
}
}
#[derive(Debug, Clone)]
pub struct FixOutcome {
pub output: String,
pub applied: usize,
pub remaining: Vec<Diagnostic>,
}
const MAX_PASSES: usize = 10;
pub fn fix_source(
path: Option<&Path>,
text: &str,
config: &LintConfig,
include_unsafe: bool,
) -> FixOutcome {
let mut current = text.to_string();
let mut total = 0usize;
for _ in 0..MAX_PASSES {
let report = check_source(path, ¤t, config);
let Applied { output, applied } =
apply_fixes(¤t, &report.diagnostics, include_unsafe);
if applied == 0 {
return FixOutcome {
output: current,
applied: total,
remaining: report.diagnostics,
};
}
total += applied;
current = output;
}
let report = check_source(path, ¤t, config);
FixOutcome {
output: current,
applied: total,
remaining: report.diagnostics,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::diagnostic::Severity;
fn diag(fixes: Vec<Fix>) -> Diagnostic {
Diagnostic {
path: None,
start: 0,
end: 0,
rule: "test".to_string(),
severity: Severity::Warning,
message: String::new(),
fixes,
suppressed: false,
}
}
fn fix(start: usize, end: usize, content: &str, applicability: Applicability) -> Fix {
Fix {
description: String::new(),
content: content.to_string(),
start,
end,
applicability,
}
}
#[test]
fn single_safe_fix_is_applied() {
let src = "if x = 5";
let d = diag(vec![fix(5, 6, "==", Applicability::Safe)]);
let out = apply_fixes(src, &[d], false);
assert_eq!(out.output, "if x == 5");
assert_eq!(out.applied, 1);
}
#[test]
fn unsafe_fix_is_gated() {
let src = "abc";
let d = diag(vec![fix(0, 1, "X", Applicability::Unsafe)]);
let skipped = apply_fixes(src, std::slice::from_ref(&d), false);
assert_eq!(skipped.output, "abc");
assert_eq!(skipped.applied, 0);
let applied = apply_fixes(src, &[d], true);
assert_eq!(applied.output, "Xbc");
assert_eq!(applied.applied, 1);
}
#[test]
fn two_disjoint_fixes_apply_with_correct_offsets() {
let src = "a b c";
let d = diag(vec![
fix(0, 1, "AAAA", Applicability::Safe),
fix(4, 5, "C", Applicability::Safe),
]);
let out = apply_fixes(src, &[d], false);
assert_eq!(out.output, "AAAA b C");
assert_eq!(out.applied, 2);
}
#[test]
fn overlapping_fixes_apply_only_the_first() {
let src = "abcdef";
let d = diag(vec![
fix(0, 3, "X", Applicability::Safe),
fix(2, 5, "Y", Applicability::Safe),
]);
let out = apply_fixes(src, &[d], false);
assert_eq!(out.output, "Xdef");
assert_eq!(out.applied, 1);
}
#[test]
fn fix_source_reaches_a_fixpoint() {
let config = LintConfig {
select: Some(vec!["assignment-in-condition".to_string()]),
..Default::default()
};
let outcome = fix_source(None, "if x = 5\n x\nend\n", &config, false);
assert_eq!(outcome.output, "if x == 5\n x\nend\n");
assert_eq!(outcome.applied, 1);
assert!(outcome.remaining.is_empty());
let again = fix_source(None, &outcome.output, &config, false);
assert_eq!(again.applied, 0);
assert_eq!(again.output, outcome.output);
}
}