use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn fixup_loop(project_dir: &Path, cargo_args: &[String]) {
const MAX_ITERATIONS: usize = 10;
for iteration in 0..MAX_ITERATIONS {
let diagnostics = run_cargo_check_json(project_dir, cargo_args);
let e0689: Vec<_> = diagnostics
.iter()
.filter(|d| d.code.as_deref() == Some("E0689"))
.collect();
let e0658: Vec<_> = diagnostics
.iter()
.filter(|d| d.code.as_deref() == Some("E0658"))
.collect();
if e0689.is_empty() && e0658.is_empty() {
if iteration > 0 {
eprintln!("warn-rewrite fixup: clean after {} iteration(s)", iteration);
}
return;
}
eprintln!(
"warn-rewrite fixup iteration {}: {} E0689, {} E0658",
iteration + 1,
e0689.len(),
e0658.len()
);
let mut applied = false;
applied |= fix_e0689(project_dir, &e0689);
applied |= fix_e0658(project_dir, &e0658);
if !applied {
eprintln!("warn-rewrite fixup: no fixes could be applied, stopping");
return;
}
}
eprintln!("warn-rewrite fixup: reached max iterations ({})", MAX_ITERATIONS);
}
#[derive(Debug)]
struct Diagnostic {
code: Option<String>,
message: String,
spans: Vec<DiagSpan>,
children: Vec<Diagnostic>,
}
#[derive(Debug, Clone)]
struct DiagSpan {
file_name: String,
byte_start: u32,
byte_end: u32,
suggested_replacement: Option<String>,
is_primary: bool,
}
fn run_cargo_check_json(project_dir: &Path, extra_args: &[String]) -> Vec<Diagnostic> {
let output = Command::new("cargo")
.arg("check")
.arg("--message-format=json")
.args(extra_args)
.current_dir(project_dir)
.output()
.expect("failed to run cargo check");
let stdout = String::from_utf8_lossy(&output.stdout);
let mut result = Vec::new();
for line in stdout.lines() {
let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if value.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") {
continue;
}
let Some(msg) = value.get("message") else { continue };
if let Some(diag) = parse_diagnostic(msg) {
result.push(diag);
}
}
result
}
fn parse_diagnostic(v: &serde_json::Value) -> Option<Diagnostic> {
let code = v
.get("code")
.and_then(|c| c.get("code"))
.and_then(|c| c.as_str())
.map(str::to_owned);
let message = v
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("")
.to_owned();
let spans = v
.get("spans")
.and_then(|s| s.as_array())
.map(|arr| arr.iter().filter_map(parse_span).collect())
.unwrap_or_default();
let children = v
.get("children")
.and_then(|c| c.as_array())
.map(|arr| arr.iter().filter_map(parse_diagnostic).collect())
.unwrap_or_default();
Some(Diagnostic { code, message, spans, children })
}
fn parse_span(v: &serde_json::Value) -> Option<DiagSpan> {
let file_name = v.get("file_name")?.as_str()?.to_owned();
let byte_start = v.get("byte_start")?.as_u64()? as u32;
let byte_end = v.get("byte_end")?.as_u64()? as u32;
let suggested_replacement = v
.get("suggested_replacement")
.and_then(|s| s.as_str())
.map(str::to_owned);
let is_primary = v.get("is_primary").and_then(|b| b.as_bool()).unwrap_or(false);
Some(DiagSpan { file_name, byte_start, byte_end, suggested_replacement, is_primary })
}
fn fix_e0689(project_dir: &Path, diags: &[&Diagnostic]) -> bool {
let mut insertions: HashMap<PathBuf, Vec<(u32, String)>> = HashMap::new();
for diag in diags {
if let Some((file, byte_end, ty)) = find_type_suggestion(project_dir, diag) {
insertions.entry(file).or_default().push((byte_end, ty));
}
}
if insertions.is_empty() {
return false;
}
let mut applied = false;
for (file, mut points) in insertions {
if !file.exists() {
continue;
}
let src = match std::fs::read(&file) {
Ok(b) => b,
Err(e) => { eprintln!("fixup: read {:?}: {}", file, e); continue; }
};
points.sort_by(|a, b| b.0.cmp(&a.0));
points.dedup_by_key(|p| p.0);
let mut result = src;
for (offset, ty) in points {
let pos = offset as usize;
if pos > result.len() {
eprintln!("fixup: offset {} out of range in {:?}", pos, file);
continue;
}
if result.get(pos..pos + 2) == Some(b": ") {
continue;
}
let insertion = format!(": {}", ty);
let mut new_result = Vec::with_capacity(result.len() + insertion.len());
new_result.extend_from_slice(&result[..pos]);
new_result.extend_from_slice(insertion.as_bytes());
new_result.extend_from_slice(&result[pos..]);
result = new_result;
eprintln!("fixup E0689: {:?} +{} `{}`", file, offset, insertion);
applied = true;
}
if let Err(e) = std::fs::write(&file, &result) {
eprintln!("fixup: write {:?}: {}", file, e);
}
}
applied
}
fn find_type_suggestion(
project_dir: &Path,
diag: &Diagnostic,
) -> Option<(PathBuf, u32, String)> {
for child in &diag.children {
let msg = &child.message;
if !msg.contains("consider giving") || !msg.contains("a type") {
continue;
}
for span in &child.spans {
if let Some(repl) = &span.suggested_replacement {
let ty = repl.trim_start_matches(": ").trim().to_owned();
if !ty.is_empty() && is_valid_type_name(&ty) {
let path = resolve_path(project_dir, &span.file_name);
return Some((path, span.byte_start, ty));
}
}
}
if let Some(ty) = extract_type_from_message(msg) {
for span in &diag.spans {
if span.is_primary {
let _ = (span, &ty); }
}
for span in &child.spans {
let path = resolve_path(project_dir, &span.file_name);
return Some((path, span.byte_end, ty.clone()));
}
}
}
None
}
fn extract_type_from_message(msg: &str) -> Option<String> {
let after_colon = msg.split(": `").nth(1)?;
let ty = after_colon.trim_end_matches('`').trim().to_owned();
if is_valid_type_name(&ty) { Some(ty) } else { None }
}
fn is_valid_type_name(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| c.is_alphanumeric() || matches!(c, '_' | ':' | '<' | '>' | '&' | '[' | ']'))
}
fn resolve_path(project_dir: &Path, file_name: &str) -> PathBuf {
let p = PathBuf::from(file_name);
if p.is_absolute() {
p
} else {
project_dir.join(p)
}
}
fn fix_e0658(project_dir: &Path, diags: &[&Diagnostic]) -> bool {
let mut by_file: HashMap<PathBuf, Vec<(u32, u32, String)>> = HashMap::new();
for diag in diags {
if !diag.message.contains("not yet stable as a const") && !diag.message.contains("use of unstable library feature") {
continue;
}
for span in &diag.spans {
if !span.is_primary { continue; }
let path = resolve_path(project_dir, &span.file_name);
by_file
.entry(path)
.or_default()
.push((span.byte_start, span.byte_end, String::new()));
}
}
if by_file.is_empty() {
return false;
}
let mut applied = false;
for (file, mut spans) in by_file {
if !file.exists() { continue; }
let src_bytes = match std::fs::read(&file) {
Ok(b) => b,
Err(e) => { eprintln!("fixup E0658: read {:?}: {}", file, e); continue; }
};
let src = match std::str::from_utf8(&src_bytes) {
Ok(s) => s,
Err(_) => continue,
};
spans.sort_by(|a, b| b.0.cmp(&a.0));
spans.dedup_by_key(|s| s.0);
let mut result = src_bytes.clone();
for (start, end, _) in spans {
let s = start as usize;
let e = end as usize;
if e > src.len() || s > e { continue; }
let expr = &src[s..e];
let Some(reverted) = revert_from_to_as(expr) else {
eprintln!("fixup E0658: can't revert {:?} in {:?}", expr, file);
continue;
};
let offset_delta = result.len() as i64 - src_bytes.len() as i64;
let rs = (s as i64 + offset_delta) as usize;
let re = (e as i64 + offset_delta) as usize;
if re > result.len() || rs > re { continue; }
let mut new_result = Vec::with_capacity(result.len() - (re - rs) + reverted.len());
new_result.extend_from_slice(&result[..rs]);
new_result.extend_from_slice(reverted.as_bytes());
new_result.extend_from_slice(&result[re..]);
eprintln!("fixup E0658: {:?} +{}..{} `{}` → `{}`", file, s, e, expr, reverted);
result = new_result;
applied = true;
}
if let Err(e) = std::fs::write(&file, &result) {
eprintln!("fixup: write {:?}: {}", file, e);
}
}
applied
}
fn revert_from_to_as(expr: &str) -> Option<String> {
let expr = expr.trim();
if let Some(rest) = expr.strip_suffix(')') {
if let Some(idx) = rest.find("::from(") {
let ty = &rest[..idx];
let inner = &rest[idx + "::from(".len()..];
if !ty.is_empty() && !inner.is_empty() {
return Some(format!("{} as {}", inner, ty));
}
}
if let Some(rest2) = rest.strip_suffix(".unwrap_or_default(") {
if let Some(rest2) = rest2.strip_suffix(')') {
if let Some(idx) = rest2.find("::try_from(") {
let ty = &rest2[..idx];
let inner = &rest2[idx + "::try_from(".len()..];
if !ty.is_empty() && !inner.is_empty() {
return Some(format!("{} as {}", inner, ty));
}
}
}
}
}
None
}