use crate::cli::pipeline::{FormattingOwner, PolyCoverage, formatting_owner};
use crate::core::config::ResolvedCrateConfig;
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
pub(super) struct RealFormatCandidate {
pub(super) full_path: PathBuf,
pub(super) disk_content: String,
pub(super) rendered_content: String,
pub(super) format_context: PathBuf,
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub(crate) struct FormatDriftStats {
pub(crate) compared: usize,
pub(crate) skipped_missing_formatter: usize,
pub(crate) skipped_staging_error: usize,
pub(crate) skipped_formatter_error: usize,
pub(crate) skipped_no_faithful_prediction: usize,
pub(crate) matched_render_exactly: usize,
pub(crate) content_verified_paths: HashSet<PathBuf>,
}
#[derive(Default)]
struct DriftAccumulator {
drifted: Vec<String>,
real_format_candidates: Vec<RealFormatCandidate>,
unpredictable: usize,
matched_render: usize,
content_verified_paths: HashSet<PathBuf>,
}
struct TextDriftContext<'a> {
base_dir: &'a Path,
config: &'a ResolvedCrateConfig,
coverage: &'a PolyCoverage,
poly_available: bool,
}
impl FormatDriftStats {
pub(crate) fn add(&mut self, other: Self) {
let Self {
compared,
skipped_missing_formatter,
skipped_staging_error,
skipped_formatter_error,
skipped_no_faithful_prediction,
matched_render_exactly,
content_verified_paths,
} = other;
self.compared += compared;
self.skipped_missing_formatter += skipped_missing_formatter;
self.skipped_staging_error += skipped_staging_error;
self.skipped_formatter_error += skipped_formatter_error;
self.skipped_no_faithful_prediction += skipped_no_faithful_prediction;
self.matched_render_exactly += matched_render_exactly;
self.content_verified_paths.extend(content_verified_paths);
}
}
fn real_formatter_drift_with(
candidates: Vec<RealFormatCandidate>,
base_dir: &Path,
is_available: &dyn Fn(&str) -> bool,
) -> (Vec<String>, FormatDriftStats) {
real_formatter_drift_with_runner(
candidates,
base_dir,
is_available,
&crate::cli::pipeline::poly_format_strict_including_excluded,
)
}
fn real_formatter_drift_with_runner(
candidates: Vec<RealFormatCandidate>,
_base_dir: &Path,
is_available: &dyn Fn(&str) -> bool,
run_formatter: &dyn Fn(&[PathBuf], &Path) -> anyhow::Result<()>,
) -> (Vec<String>, FormatDriftStats) {
if candidates.is_empty() {
return (Vec::new(), FormatDriftStats::default());
}
if !is_available("poly") {
return (
Vec::new(),
FormatDriftStats {
skipped_missing_formatter: candidates.len(),
..FormatDriftStats::default()
},
);
}
let temp_files: Vec<Option<StagedFormatFile>> = candidates
.iter()
.map(
|candidate| match write_sibling_temp_file(&candidate.full_path, &candidate.rendered_content) {
Ok(temp_file) => Some(temp_file),
Err(error) => {
tracing::debug!(
path = %candidate.full_path.display(),
"could not stage a drift-check temp copy ({error}); skipping this file's \
formatted-output comparison"
);
None
}
},
)
.collect();
let mut paths_by_context: BTreeMap<&Path, Vec<PathBuf>> = BTreeMap::new();
for (candidate, temp_file) in candidates.iter().zip(&temp_files) {
if let Some(temp_file) = temp_file {
paths_by_context
.entry(&candidate.format_context)
.or_default()
.push(temp_file.path().to_path_buf());
}
}
let failed_contexts = run_format_candidates(paths_by_context, run_formatter);
compare_formatted_candidates(&candidates, &temp_files, &failed_contexts)
}
fn run_format_candidates(
paths_by_context: BTreeMap<&Path, Vec<PathBuf>>,
run_formatter: &dyn Fn(&[PathBuf], &Path) -> anyhow::Result<()>,
) -> HashSet<PathBuf> {
let mut failed = HashSet::new();
for (context, paths) in paths_by_context {
if let Err(error) = run_formatter(&paths, context) {
tracing::warn!(
context = %context.display(),
"poly fmt over the drift-check temp copies failed (non-fatal): {error:#}"
);
failed.insert(context.to_path_buf());
}
}
failed
}
fn compare_formatted_candidates(
candidates: &[RealFormatCandidate],
temp_files: &[Option<StagedFormatFile>],
failed_contexts: &HashSet<PathBuf>,
) -> (Vec<String>, FormatDriftStats) {
let mut drifted = Vec::new();
let mut compared = 0usize;
let mut staging_errors = 0usize;
let mut formatter_errors = 0usize;
let mut content_verified_paths = HashSet::new();
for (candidate, temp_file) in candidates.iter().zip(temp_files.iter()) {
let Some(temp_file) = temp_file else {
staging_errors += 1;
continue;
};
if failed_contexts.contains(&candidate.format_context) {
formatter_errors += 1;
continue;
}
let Ok(formatted) = std::fs::read_to_string(temp_file.path()) else {
staging_errors += 1;
continue;
};
compared += 1;
content_verified_paths.insert(candidate.full_path.clone());
if !crate::cli::pipeline::matches_alef_output(&candidate.full_path, &candidate.disk_content, &formatted) {
drifted.push(candidate.full_path.display().to_string());
}
}
(
drifted,
FormatDriftStats {
compared,
skipped_staging_error: staging_errors,
skipped_formatter_error: formatter_errors,
content_verified_paths,
..FormatDriftStats::default()
},
)
}
struct StagedFormatFile {
_directory: tempfile::TempDir,
path: PathBuf,
}
impl StagedFormatFile {
fn path(&self) -> &Path {
&self.path
}
}
fn write_sibling_temp_file(real_path: &Path, content: &str) -> std::io::Result<StagedFormatFile> {
let parent = real_path.parent().unwrap_or_else(|| Path::new("."));
let file_name = real_path
.file_name()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "formatter candidate has no file name"))?;
let directory = tempfile::Builder::new()
.prefix("alef-verify-drift-")
.tempdir_in(parent)?;
let path = directory.path().join(file_name);
std::fs::write(&path, content)?;
Ok(StagedFormatFile {
_directory: directory,
path,
})
}
pub(super) fn drifted_managed_paths(
files: &[crate::core::backend::GeneratedFile],
base_dir: &Path,
config: &ResolvedCrateConfig,
) -> (Vec<String>, FormatDriftStats) {
drifted_managed_paths_with(files, base_dir, config, &crate::cli::pipeline::is_tool_available)
}
fn record_direct_comparison(full_path: &Path, existing: &str, rendered: &str, result: &mut DriftAccumulator) {
result.content_verified_paths.insert(full_path.to_path_buf());
if crate::cli::pipeline::matches_alef_output(full_path, existing, rendered) {
result.matched_render += 1;
} else {
result.drifted.push(full_path.display().to_string());
}
}
fn writer_skips_unchanged_stamped_file(full_path: &Path, existing: &str, rendered: &str) -> bool {
crate::core::hash::content_has_alef_marker(existing)
&& crate::cli::pipeline::matches_alef_output(full_path, existing, rendered)
}
fn is_under_rewritten_output_root(path: &Path, config: &ResolvedCrateConfig, base_dir: &Path) -> bool {
config.e2e.as_ref().is_some_and(|e2e| {
[base_dir.join(&e2e.output), base_dir.join(&e2e.registry.output)]
.iter()
.any(|root| path.starts_with(root))
})
}
fn is_authoritative_drift_candidate(
file: &crate::core::backend::GeneratedFile,
base_dir: &Path,
config: &ResolvedCrateConfig,
declared: &crate::core::config::UserOwnedPaths,
) -> Option<PathBuf> {
let full_path = base_dir.join(&file.path);
if declared.matches(base_dir, &full_path) || !full_path.exists() {
return None;
}
let has_marker = std::fs::read_to_string(&full_path)
.ok()
.is_some_and(|content| crate::core::hash::content_has_alef_marker(&content));
let recorded = !has_marker
&& crate::cli::pipeline::marker_comment_style(&full_path).is_none()
&& crate::cli::cache::is_committed_scaffold_owned_path(base_dir, &full_path);
if !has_marker && !recorded {
return None;
}
if is_create_once_for_drift(file) && !is_under_rewritten_output_root(&full_path, config, base_dir) {
return None;
}
Some(full_path)
}
fn prepared_text_drift_input(
file: &crate::core::backend::GeneratedFile,
full_path: PathBuf,
base_dir: &Path,
) -> anyhow::Result<(PathBuf, String, String)> {
let existing = std::fs::read_to_string(&full_path)
.map_err(|error| anyhow::anyhow!("failed to read owned text output {}: {error}", full_path.display()))?;
let rendered = managed_output_for_drift(file, &existing, base_dir)
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("fresh render omitted owned text output {}", full_path.display()))?;
Ok((full_path, existing, rendered.content))
}
fn compare_binary_drift(file: &crate::core::backend::GeneratedFile, full_path: &Path, result: &mut DriftAccumulator) {
let generated = crate::cli::pipeline::decode_base64_binary(&file.path, &file.content);
let existing = std::fs::read(full_path);
match (generated, existing) {
(Ok(generated), Ok(existing)) => {
result.content_verified_paths.insert(full_path.to_path_buf());
if generated == existing {
result.matched_render += 1;
} else {
result.drifted.push(full_path.display().to_string());
}
}
(Err(error), _) => {
tracing::warn!(
path = %full_path.display(),
"generated binary output could not be decoded during drift verification: {error:#}"
);
result.drifted.push(full_path.display().to_string());
}
(_, Err(error)) => {
tracing::warn!(
path = %full_path.display(),
"owned binary output could not be read during drift verification: {error}"
);
result.drifted.push(full_path.display().to_string());
}
}
}
fn compare_text_drift(
file: &crate::core::backend::GeneratedFile,
full_path: PathBuf,
context: &TextDriftContext<'_>,
result: &mut DriftAccumulator,
) {
let (full_path, existing, rendered) = match prepared_text_drift_input(file, full_path, context.base_dir) {
Ok(prepared) => prepared,
Err(error) => {
tracing::warn!("owned text output could not be compared during drift verification: {error:#}");
result
.drifted
.push(context.base_dir.join(&file.path).display().to_string());
return;
}
};
if writer_skips_unchanged_stamped_file(&full_path, &existing, &rendered) {
result.matched_render += 1;
result.content_verified_paths.insert(full_path);
return;
}
if render_predicts_final_bytes(&full_path, context.poly_available) {
record_direct_comparison(&full_path, &existing, &rendered, result);
return;
}
match formatting_owner(&full_path, context.config, context.base_dir, context.coverage) {
FormattingOwner::None => record_direct_comparison(&full_path, &existing, &rendered, result),
FormattingOwner::Poly => result.real_format_candidates.push(RealFormatCandidate {
full_path,
disk_content: existing,
rendered_content: rendered,
format_context: context
.coverage
.format_context(&context.base_dir.join(&file.path))
.unwrap_or(context.base_dir)
.to_path_buf(),
}),
FormattingOwner::Residual(command) => {
tracing::debug!(
path = %full_path.display(),
"no faithful prediction: alef reformats this path with `{command}`, whose output it does not model"
);
result.unpredictable += 1;
}
FormattingOwner::E2eOverride(command) => {
tracing::debug!(
path = %full_path.display(),
"no faithful prediction: a [crates.e2e.format] override (`{command}`) replaces the poly pass here"
);
result.unpredictable += 1;
}
}
}
fn drifted_managed_paths_with(
files: &[crate::core::backend::GeneratedFile],
base_dir: &Path,
config: &ResolvedCrateConfig,
is_available: &dyn Fn(&str) -> bool,
) -> (Vec<String>, FormatDriftStats) {
let poly_available = is_available("poly");
let coverage = PolyCoverage::probe(&poly_probe_roots(config, base_dir), base_dir);
let declared = crate::cli::pipeline::declared_user_owned(base_dir)
.unwrap_or_else(|_| crate::core::config::UserOwnedPaths::none());
let text_context = TextDriftContext {
base_dir,
config,
coverage: &coverage,
poly_available,
};
let mut result = DriftAccumulator::default();
for file in files {
let Some(full_path) = is_authoritative_drift_candidate(file, base_dir, config, &declared) else {
continue;
};
if crate::cli::pipeline::is_base64_binary_output(&file.path) {
compare_binary_drift(file, &full_path, &mut result);
continue;
}
compare_text_drift(file, full_path, &text_context, &mut result);
}
let (real_drifted, mut stats) = real_formatter_drift_with(result.real_format_candidates, base_dir, is_available);
result.drifted.extend(real_drifted);
stats.content_verified_paths.extend(result.content_verified_paths);
stats.skipped_no_faithful_prediction = result.unpredictable;
stats.matched_render_exactly = result.matched_render;
(result.drifted, stats)
}
fn is_create_once_for_drift(file: &crate::core::backend::GeneratedFile) -> bool {
crate::cli::commands::adopt::is_create_once_seed(file)
|| (!file.generated_header && file.path == Path::new(".gitattributes"))
}
fn managed_output_for_drift(
file: &crate::core::backend::GeneratedFile,
existing: &str,
base_dir: &Path,
) -> Vec<crate::cli::commands::adopt::ManagedOutput> {
let mut prepared = file.clone();
if crate::core::hash::content_has_alef_marker(existing) {
prepared.generated_header = true;
}
if file.generated_header && file.path == Path::new("poly.toml") {
prepared.content = match crate::cli::pipeline::generate::merge_managed_toml_preview(
existing,
&file.content,
base_dir,
&file.path,
) {
Ok(content) => crate::core::hash::strip_hash_line(&content),
Err(error) => {
tracing::warn!(
path = %file.path.display(),
"could not preview managed TOML merge while checking drift: {error:#}"
);
file.content.clone()
}
};
}
crate::cli::commands::adopt::managed_outputs(std::slice::from_ref(&prepared), base_dir)
}
fn poly_probe_roots(config: &ResolvedCrateConfig, base_dir: &Path) -> Vec<PathBuf> {
let mut roots = vec![base_dir.to_path_buf()];
if let Some(e2e) = &config.e2e {
for output in [e2e.output.as_str(), e2e.registry.output.as_str()] {
let output_root = base_dir.join(output);
let Ok(entries) = std::fs::read_dir(output_root) else {
continue;
};
let mut language_roots: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.collect();
language_roots.sort();
for path in language_roots {
if !roots.contains(&path) {
roots.push(path);
}
}
}
}
roots
}
fn render_predicts_final_bytes(path: &Path, poly_available: bool) -> bool {
match path.extension().and_then(|extension| extension.to_str()) {
Some("rs") => !poly_available,
Some("md") => !poly_available,
_ => false,
}
}
pub(crate) fn report_format_drift_coverage(stats: FormatDriftStats) {
let mut notes: Vec<String> = Vec::new();
if stats.skipped_missing_formatter > 0 {
tracing::warn!(
"poly is not installed; {} generated file(s) could not be checked for formatted-output \
drift (install poly to close this gap)",
stats.skipped_missing_formatter
);
notes.push(format!(
"{} SKIPPED because poly is not installed (install poly to close this gap -- see poly-lint-format)",
stats.skipped_missing_formatter
));
}
if stats.skipped_staging_error > 0 {
tracing::warn!(
"{} generated file(s) could not be checked for formatted-output drift: their temp copy \
could not be written (read-only checkout?)",
stats.skipped_staging_error
);
notes.push(format!(
"{} SKIPPED because a temp copy could not be written beside them",
stats.skipped_staging_error
));
}
if stats.skipped_formatter_error > 0 {
notes.push(format!(
"{} SKIPPED because the formatter invocation failed",
stats.skipped_formatter_error
));
}
if stats.skipped_no_faithful_prediction > 0 {
notes.push(format!(
"{} NOT CHECKED because alef reformats them with a tool whose output it does not model \
(a [crates.e2e.format] override, `cargo sort`, or `mix format`) -- no honest byte-level \
prediction exists for these",
stats.skipped_no_faithful_prediction
));
}
let detail = if notes.is_empty() {
"all examined.".to_string()
} else {
format!("{}.", notes.join("; "))
};
crate::bin_cli::output::line(format!(
"Formatted-output drift check: {} file(s) compared via a real `poly fmt` pass, {} matched the \
render with no formatter involved, {detail}",
stats.compared, stats.matched_render_exactly
));
}
#[cfg(test)]
mod tests;