use crate::error::{Applicability, CompileError};
use crate::span::Span;
pub fn lone_fix(error: &CompileError) -> Option<&crate::error::Suggestion> {
let mut applicable = error
.suggestions
.iter()
.filter(|s| s.applicability == Applicability::MachineApplicable);
match (applicable.next(), applicable.next()) {
(Some(s), None) => Some(s),
_ => None,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Selection {
pub edits: Vec<(Span, String)>,
pub categories: Vec<&'static str>,
}
pub fn edits<'a>(
text: &str,
errors: impl IntoIterator<Item = &'a CompileError>,
) -> Vec<(Span, String)> {
select(text, errors).edits
}
pub fn select<'a>(text: &str, errors: impl IntoIterator<Item = &'a CompileError>) -> Selection {
let mut taken: Vec<(Span, String)> = Vec::new();
let mut categories = Vec::new();
for e in errors {
let Some(s) = lone_fix(e) else {
continue; };
if !s.edits.iter().all(|(span, _)| valid(text, *span)) {
continue;
}
let fresh: Vec<&(Span, String)> = s
.edits
.iter()
.filter(|e| !taken.iter().any(|t| t.0 == e.0 && t.1 == e.1))
.collect();
if fresh
.iter()
.any(|e| taken.iter().any(|t| overlaps(t.0, e.0)))
|| fresh
.iter()
.enumerate()
.any(|(i, a)| fresh[i + 1..].iter().any(|b| overlaps(a.0, b.0)))
{
continue;
}
if !fresh.is_empty() {
categories.push(e.category);
}
taken.extend(fresh.into_iter().cloned());
}
taken.sort_by_key(|(span, _)| (span.start, span.end));
Selection {
edits: taken,
categories,
}
}
fn overlaps(a: Span, b: Span) -> bool {
if a.start == a.end && b.start == b.end {
return a.start == b.start;
}
a.start < b.end && b.start < a.end
}
fn valid(text: &str, span: Span) -> bool {
span.start <= span.end && text.get(span.start..span.end).is_some()
}
pub fn apply(text: &str, edits: &[(Span, String)]) -> String {
let mut out = String::with_capacity(text.len());
let mut last = 0;
for (span, new_text) in edits {
let Some(before) = text.get(last..span.start) else {
continue;
};
if !valid(text, *span) {
continue;
}
out.push_str(before);
out.push_str(new_text);
last = span.end;
}
out.push_str(&text[last..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
type Fix<'a> = (&'a [((usize, usize), &'a str)], Applicability);
fn diag(span: (usize, usize), fixes: &[Fix]) -> CompileError {
let mut error = CompileError::new("bynk.test", Span::new(span.0, span.1), "msg");
for (edits, applicability) in fixes {
error = error.with_suggestion(
"fix",
edits
.iter()
.map(|((s, e), t)| (Span::new(*s, *e), t.to_string()))
.collect(),
*applicability,
);
}
error
}
const TEXT: &str = "0123456789012345678901234567890123456789";
const M: Applicability = Applicability::MachineApplicable;
const P: Applicability = Applicability::HasPlaceholders;
#[test]
fn takes_every_lone_machine_applicable_suggestion_in_position_order() {
let ds = [
diag((10, 12), &[(&[((10, 12), "b")], M)]),
diag((0, 2), &[(&[((0, 2), "a")], M)]),
];
assert_eq!(
edits(TEXT, &ds),
vec![
(Span::new(0, 2), "a".to_string()),
(Span::new(10, 12), "b".to_string())
]
);
}
#[test]
fn skips_placeholders_and_choices() {
let ds = [
diag((0, 2), &[(&[((0, 2), "<T>")], P)]),
diag((5, 6), &[(&[((5, 6), "x")], M), (&[((5, 6), "y")], M)]),
diag((8, 9), &[(&[((8, 9), "z")], M), (&[((8, 9), "<T>")], P)]),
];
assert_eq!(edits(TEXT, &ds), vec![(Span::new(8, 9), "z".to_string())]);
}
#[test]
fn an_overlapping_suggestion_is_skipped_whole() {
let ds = [
diag((0, 4), &[(&[((0, 4), "a")], M)]),
diag((10, 12), &[(&[((10, 12), "b"), ((2, 3), "c")], M)]),
];
assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
}
#[test]
fn an_identical_edit_is_taken_once() {
let ds = [
diag((20, 24), &[(&[((14, 14), ", Used")], M)]),
diag((30, 34), &[(&[((14, 14), ", Used")], M)]),
];
assert_eq!(
edits(TEXT, &ds),
vec![(Span::new(14, 14), ", Used".to_string())]
);
}
#[test]
fn different_insertions_at_one_point_conflict() {
let ds = [
diag((20, 24), &[(&[((14, 14), ", A")], M)]),
diag((30, 34), &[(&[((14, 14), ", B")], M)]),
];
assert_eq!(
edits(TEXT, &ds),
vec![(Span::new(14, 14), ", A".to_string())]
);
}
#[test]
fn touching_edits_do_not_conflict() {
let ds = [
diag((0, 2), &[(&[((0, 2), "a")], M)]),
diag((2, 4), &[(&[((2, 4), "b")], M)]),
diag((4, 4), &[(&[((4, 4), "c")], M)]),
];
assert_eq!(edits(TEXT, &ds).len(), 3);
assert_eq!(apply("0123456", &edits(TEXT, &ds)), "abc456");
}
#[test]
fn an_insertion_inside_a_replacement_conflicts() {
let ds = [
diag((0, 4), &[(&[((0, 4), "a")], M)]),
diag((2, 2), &[(&[((2, 2), "b")], M)]),
];
assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
}
#[test]
fn a_suggestion_with_a_bad_span_is_skipped() {
let ds = [
diag((0, 1), &[(&[((38, 99), "x")], M)]),
diag((0, 1), &[(&[((5, 3), "y")], M)]),
diag((0, 1), &[(&[((0, 1), "z")], M)]),
];
assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 1), "z".to_string())]);
let ds = [diag((0, 1), &[(&[((2, 2), "w")], M)])];
assert!(edits("aé", &ds).is_empty());
}
#[test]
fn apply_skips_a_bad_span_instead_of_panicking() {
let bad = [
(Span::new(0, 1), "A".to_string()),
(Span::new(2, 99), "!".to_string()),
];
assert_eq!(apply("abc", &bad), "Abc");
assert_eq!(apply("aé", &[(Span::new(2, 2), "w".to_string())]), "aé");
}
#[test]
fn nothing_to_fix_is_empty() {
assert!(edits(TEXT, &[] as &[CompileError]).is_empty());
assert!(edits(TEXT, &[diag((0, 1), &[])]).is_empty());
}
#[test]
fn select_names_the_diagnostics_it_took_from() {
let ds = [
diag((0, 2), &[(&[((0, 2), "a")], M)]),
diag((1, 3), &[(&[((1, 3), "b")], M)]),
diag((10, 12), &[(&[((10, 12), "c")], M)]),
];
let s = select(TEXT, &ds);
assert_eq!(s.edits.len(), 2);
assert_eq!(s.categories, vec!["bynk.test", "bynk.test"]);
}
#[test]
fn lone_fix_is_only_a_single_machine_applicable_suggestion() {
assert!(lone_fix(&diag((0, 1), &[(&[((0, 1), "a")], M)])).is_some());
assert!(lone_fix(&diag((0, 1), &[(&[((0, 1), "a")], P)])).is_none());
assert!(
lone_fix(&diag(
(0, 1),
&[(&[((0, 1), "a")], M), (&[((0, 1), "b")], M)]
))
.is_none()
);
assert!(lone_fix(&diag((0, 1), &[])).is_none());
}
}