use crate::Diagnostic;
use crate::parser::parse;
pub struct Healed {
pub source: String,
pub notes: Vec<String>,
}
pub fn heal(source: &str) -> Option<Healed> {
let mut current = source.to_string();
let mut notes = Vec::new();
if parse(¤t).is_ok() {
return None;
}
for _pass in 0..6 {
let diagnostics = match parse(¤t) {
Ok(_) => {
return (!notes.is_empty()).then_some(Healed {
source: current,
notes,
});
}
Err(diagnostics) => diagnostics,
};
let mut edits = plan(¤t, &diagnostics, &mut notes);
if edits.is_empty() {
return None;
}
edits.sort_by_key(|edit| std::cmp::Reverse(edit.0));
edits.dedup_by_key(|e| e.0);
for (offset, text) in edits {
if current.is_char_boundary(offset) {
current.insert_str(offset, &text);
}
}
}
None
}
fn plan(source: &str, diagnostics: &[Diagnostic], notes: &mut Vec<String>) -> Vec<(usize, String)> {
let mut edits: Vec<(usize, String)> = Vec::new();
let mut names = 0usize;
let mut fresh = || loop {
names += 1;
let name = format!("fixed_{names}");
if !source.contains(&name) {
return name;
}
};
for d in diagnostics {
let at = line_of(source, d.primary_span.start);
match d.code.as_str() {
"RL1017" => {
let Some(fix) = d.fixes.first() else { continue };
if fix.span.start == fix.span.end && !fix.replacement.is_empty() {
edits.push((fix.span.start, fix.replacement.clone()));
notes.push(format!(
"line {at}: a block must end with `return`; inserted `{}`",
fix.replacement.trim()
));
}
}
"RL1019" => {
let name = fresh();
edits.push((d.primary_span.start, format!("{name} = ")));
notes.push(format!(
"line {at}: statements are bindings; bound the bare expression to `{name}`"
));
}
"RL1014" => {
let name = fresh();
edits.push((d.primary_span.start, format!("{name} = ")));
notes.push(format!(
"line {at}: control structures are expressions; bound the construct to \
`{name}` — write `x = if condition {{ ... }}` directly next time"
));
}
"RL1008" if d.message.contains("expected a newline") => {
edits.push((d.primary_span.start, "\n".to_string()));
notes.push(format!("line {at}: inserted a missing statement separator"));
}
_ => {}
}
}
edits
}
fn line_of(source: &str, offset: usize) -> usize {
source
.get(..offset)
.map(|s| s.matches('\n').count() + 1)
.unwrap_or(1)
}