use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Report {
pub backups_created: Vec<PathBuf>,
pub cargo_toml_edits: Vec<PathBuf>,
pub files_touched: Vec<PathBuf>,
pub tests_converted: usize,
pub warnings: Vec<Warning>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Warning {
pub file: PathBuf,
pub line: Option<usize>,
pub message: String,
pub source: Option<Arc<str>>,
pub span: Option<(usize, usize)>,
}
impl Report {
#[inline]
pub const fn add_converted(&mut self, count: usize) {
self.tests_converted = self.tests_converted.saturating_add(count);
}
#[inline]
pub fn backed_up(&mut self, path: PathBuf) {
self.backups_created.push(path);
}
#[inline]
pub fn cargo_edit(&mut self, path: PathBuf) {
self.cargo_toml_edits.push(path);
}
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
backups_created: Vec::new(),
cargo_toml_edits: Vec::new(),
files_touched: Vec::new(),
tests_converted: 0,
warnings: Vec::new(),
}
}
#[inline]
pub fn print_summary<W: Write>(&self, mut out: W) -> io::Result<()> {
writeln!(out)?;
writeln!(out, "== rudzio-migrate summary ==")?;
writeln!(out, "Files touched: {}", self.files_touched.len())?;
writeln!(out, "Tests converted: {}", self.tests_converted)?;
writeln!(
out,
"Cargo.toml edits: {} files",
self.cargo_toml_edits.len()
)?;
writeln!(
out,
"Backups created: {} (*.backup_before_migration_to_rudzio)",
self.backups_created.len()
)?;
writeln!(out, "Warnings: {}", self.warnings.len())?;
if !self.warnings.is_empty() {
writeln!(out)?;
writeln!(out, "Warnings (need manual follow-up):")?;
for warning in &self.warnings {
render_warning(&mut out, warning)?;
}
}
writeln!(out)?;
writeln!(out, "Next steps:")?;
writeln!(
out,
" 1. git diff \u{2014} review every change. This tool is not magic."
)?;
writeln!(
out,
" 2. cargo check --tests \u{2014} if anything does not compile, the diff"
)?;
writeln!(
out,
" is your friend; the conversion is mechanical and localized."
)?;
writeln!(out, " 3. Address the warnings (file:line list above).")?;
writeln!(out, " 4. Once satisfied, delete the backups:")?;
writeln!(
out,
" find . -name '*.backup_before_migration_to_rudzio' -delete"
)?;
writeln!(
out,
" Or add the glob to .gitignore and keep them around during review."
)?;
out.flush()?;
Ok(())
}
#[inline]
pub fn touched(&mut self, path: PathBuf) {
self.files_touched.push(path);
}
#[inline]
pub fn warn<S: Into<String>>(&mut self, file: PathBuf, line: Option<usize>, message: S) {
self.warnings.push(Warning {
file,
line,
message: message.into(),
source: None,
span: None,
});
}
#[inline]
pub fn warn_with_span<S: Into<String>>(
&mut self,
file: PathBuf,
line: usize,
byte_offset: usize,
byte_len: usize,
source: Arc<str>,
message: S,
) {
self.warnings.push(Warning {
file,
line: Some(line),
message: message.into(),
source: Some(source),
span: Some((byte_offset, byte_len)),
});
}
}
#[inline]
pub fn progress<W: Write>(mut out: W, msg: &str) -> io::Result<()> {
writeln!(out, "{msg}")?;
out.flush()
}
fn render_warning<W: Write>(mut out: W, warning: &Warning) -> io::Result<()> {
match warning.line {
Some(line) => writeln!(
out,
" {}:{}: {}",
warning.file.display(),
line,
warning.message,
)?,
None => writeln!(out, " {}: {}", warning.file.display(), warning.message)?,
}
let (Some((offset, raw_len)), Some(source)) = (warning.span, &warning.source) else {
return Ok(());
};
let span_len = raw_len.max(1);
let scan_end = offset.min(source.len());
let line_start = source
.get(..scan_end)
.and_then(|prefix| prefix.rfind('\n'))
.map_or(0, |idx| idx.saturating_add(1));
let tail = source.get(line_start..).unwrap_or("");
let line_end = tail
.find('\n')
.map_or(source.len(), |idx| line_start.saturating_add(idx));
let snippet = source.get(line_start..line_end).unwrap_or("");
let col = offset.saturating_sub(line_start);
writeln!(out, " | {snippet}")?;
let pad = " ".repeat(col);
let caret_count = span_len.min(line_end.saturating_sub(offset).max(1));
let underline = "^".repeat(caret_count);
writeln!(out, " | {pad}{underline}")?;
Ok(())
}