ruff/src/autofix/mod.rs

211 lines
5.0 KiB
Rust

use std::borrow::Cow;
use std::collections::BTreeSet;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::Location;
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Locator;
pub mod fixer;
pub mod helpers;
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file<'a>(
diagnostics: &'a [Diagnostic],
locator: &'a Locator<'a>,
) -> Option<(Cow<'a, str>, usize)> {
if diagnostics.iter().all(|check| check.fix.is_none()) {
return None;
}
Some(apply_fixes(
diagnostics.iter().filter_map(|check| check.fix.as_ref()),
locator,
))
}
/// Apply a series of fixes.
fn apply_fixes<'a>(
fixes: impl Iterator<Item = &'a Fix>,
locator: &'a Locator<'a>,
) -> (Cow<'a, str>, usize) {
let mut output = RopeBuilder::new();
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
let mut num_fixed: usize = 0;
for fix in fixes.sorted_by_key(|fix| fix.location) {
// If we already applied an identical fix as part of another correction, skip
// any re-application.
if applied.contains(&fix) {
num_fixed += 1;
continue;
}
// Best-effort approach: if this fix overlaps with a fix we've already applied,
// skip it.
if last_pos > fix.location {
continue;
}
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
output.append(&slice);
// Add the patch itself.
output.append(&fix.content);
// Track that the fix was applied.
last_pos = fix.end_location;
applied.insert(fix);
num_fixed += 1;
}
// Add the remaining content.
let slice = locator.slice_source_code_at(&last_pos);
output.append(&slice);
(Cow::from(output.finish()), num_fixed)
}
#[cfg(test)]
mod tests {
use rustpython_parser::ast::Location;
use crate::autofix::apply_fixes;
use crate::fix::Fix;
use crate::source_code::Locator;
#[test]
fn empty_file() {
let fixes = vec![];
let locator = Locator::new(r#""#);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(contents, "");
assert_eq!(fixed, 0);
}
#[test]
fn apply_single_replacement() {
let fixes = vec![Fix {
content: "Bar".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 14),
}];
let locator = Locator::new(
r#"
class A(object):
...
"#
.trim(),
);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(
contents,
r#"
class A(Bar):
...
"#
.trim(),
);
assert_eq!(fixed, 1);
}
#[test]
fn apply_single_removal() {
let fixes = vec![Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
}];
let locator = Locator::new(
r#"
class A(object):
...
"#
.trim(),
);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(
contents,
r#"
class A:
...
"#
.trim()
);
assert_eq!(fixed, 1);
}
#[test]
fn apply_double_removal() {
let fixes = vec![
Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 16),
},
Fix {
content: String::new(),
location: Location::new(1, 16),
end_location: Location::new(1, 23),
},
];
let locator = Locator::new(
r#"
class A(object, object):
...
"#
.trim(),
);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(
contents,
r#"
class A:
...
"#
.trim()
);
assert_eq!(fixed, 2);
}
#[test]
fn ignore_overlapping_fixes() {
let fixes = vec![
Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
},
Fix {
content: "ignored".to_string(),
location: Location::new(1, 9),
end_location: Location::new(1, 11),
},
];
let locator = Locator::new(
r#"
class A(object):
...
"#
.trim(),
);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(
contents,
r#"
class A:
...
"#
.trim(),
);
assert_eq!(fixed, 1);
}
}