use anyhow::Result;
use crate::cli::{cache, dispatch, pipeline};
use super::super::args::Commands;
use super::super::dispatch::DispatchContext;
use super::super::helpers::*;
use super::super::verify_orphans;
pub(super) fn run(context: &DispatchContext, report_only: bool) -> Result<Option<Commands>> {
let config_path = &context.config_path;
let (_workspace, resolved) = load_config(config_path)?;
let crates_to_process = dispatch::select_crates(&resolved, &context.crate_filter)?;
tracing::info!("Verifying alef-generated files (per-file inputs+content hash)");
let base_dir = std::env::current_dir()?;
let missing_snippet_roots: Vec<String> = crates_to_process
.iter()
.flat_map(|resolved_cfg| missing_snippet_directories(resolved_cfg, &base_dir))
.collect();
let has_missing_snippet_roots = !missing_snippet_roots.is_empty();
let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
let all_inputs_hashes: Vec<String> = crates_to_process
.iter()
.filter_map(|c| cache::sources_hash(&c.sources).ok())
.map(|sh| crate::core::hash::compute_inputs_hash(&sh, &alef_toml_bytes))
.collect();
let (scanned, scan_coverage) = super::super::verify_scan::scan(&base_dir);
let marked_paths: std::collections::HashSet<std::path::PathBuf> =
scanned.iter().map(|(path, _, _)| path.clone()).collect();
let stale = stale_among(&scanned, &all_inputs_hashes);
let mut snippet_coverage_issues = Vec::new();
let mut missing_generated_files: Vec<String> = Vec::new();
let mut missing_gitignored_generated_files: Vec<String> = Vec::new();
let mut frozen_generated_files: Vec<FrozenFile> = Vec::new();
let mut all_managed_paths: std::collections::HashSet<std::path::PathBuf> = std::collections::HashSet::new();
let mut stage_failures: Vec<String> = Vec::new();
let mut create_once_template_drift: Vec<String> = Vec::new();
let mut ephemeral_excluded_count = 0usize;
for resolved_cfg in &crates_to_process {
let languages = resolve_languages(resolved_cfg, None)?;
let api = pipeline::extract(resolved_cfg, config_path, false)?;
let scaffold_files = pipeline::scaffold(&api, resolved_cfg, &languages, config_path)?;
create_once_template_drift.extend(
pipeline::find_create_once_template_drift(&scaffold_files, &base_dir)
.into_iter()
.map(|path| format!("[{}] {}", resolved_cfg.name, path.display())),
);
let found = find_missing_and_frozen_generated_files(&languages, &api, resolved_cfg, config_path, &base_dir)?;
let (missing, missing_excluded) = resolved_cfg.verify.partition_ephemeral(found.missing, &base_dir);
let (missing_gitignored, gitignored_excluded) = resolved_cfg
.verify
.partition_ephemeral(found.missing_gitignored, &base_dir);
ephemeral_excluded_count += missing_excluded + gitignored_excluded;
missing_generated_files.extend(missing);
missing_gitignored_generated_files.extend(missing_gitignored);
frozen_generated_files.extend(found.frozen);
all_managed_paths.extend(found.managed_paths);
stage_failures.extend(
found
.stage_failures
.into_iter()
.map(|failure| format!("[{}] {failure}", resolved_cfg.name)),
);
let Some(e2e_config) = &resolved_cfg.e2e else {
continue;
};
if let Err(error) = crate::e2e::verify_fresh_snippet_coverage(
&base_dir,
resolved_cfg,
e2e_config,
&api.types,
&api.enums,
&api.functions,
) {
snippet_coverage_issues.push(format!("[{}] {error:#}", resolved_cfg.name));
}
}
missing_generated_files.sort();
missing_generated_files.dedup();
missing_gitignored_generated_files.sort();
missing_gitignored_generated_files.dedup();
frozen_generated_files.sort_by(|a, b| a.path.cmp(&b.path));
frozen_generated_files.dedup_by(|a, b| a.path == b.path);
stage_failures.sort();
stage_failures.dedup();
create_once_template_drift.sort();
create_once_template_drift.dedup();
let has_stage_failures = !stage_failures.is_empty();
let has_missing_files = !missing_generated_files.is_empty();
let has_missing_gitignored_files = !missing_gitignored_generated_files.is_empty();
let has_adoptable_frozen_files =
crate::bin_cli::helpers::frozen::has_adoptable_frozen_files(&frozen_generated_files);
log_managed_surface(&all_managed_paths);
let orphan_generated_files = verify_orphans::find_orphaned_generated_files(&base_dir, &all_managed_paths);
let has_orphan_files = !orphan_generated_files.is_empty();
let abi_disagreement = find_stamp_disagreement(&base_dir, crate::core::hash::HANDLE_ABI_STAMP_KEY);
let has_abi_disagreement = abi_disagreement.is_some();
if let Some(disagreement) = &abi_disagreement {
crate::bin_cli::output::line(format_args!(
"ABI generation disagreement detected for `{}`:",
disagreement.key
));
for (path, value) in &disagreement.examples {
crate::bin_cli::output::line(format_args!(" {path} -> {value}"));
}
}
let mut all_frb_generated_drift: Vec<String> = Vec::new();
for resolved_cfg in &crates_to_process {
all_frb_generated_drift.extend(frb_generated_drift(resolved_cfg, &base_dir));
}
let has_frb_generated_drift = !all_frb_generated_drift.is_empty();
if has_frb_generated_drift {
crate::bin_cli::output::line(
"Dart FRB bridge drift detected (frb_generated.rs is not the canonical form \
`CarryFrbCfgGates` would write for the current lib.rs -- run `alef generate` to \
regenerate and reformat it):",
);
for path in &all_frb_generated_drift {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
let mut all_version_mismatches: Vec<String> = Vec::new();
for resolved_cfg in &crates_to_process {
let mismatches = pipeline::verify_versions(resolved_cfg)?;
all_version_mismatches.extend(mismatches);
}
let has_version_issues = !all_version_mismatches.is_empty();
if has_version_issues {
crate::bin_cli::output::line("Version mismatches detected:");
for mismatch in &all_version_mismatches {
crate::bin_cli::output::line(format_args!(" {mismatch}"));
}
}
if !snippet_coverage_issues.is_empty() {
crate::bin_cli::output::line("Snippet coverage issues detected:");
for issue in &snippet_coverage_issues {
crate::bin_cli::output::line(format_args!(" {issue}"));
}
}
if has_missing_snippet_roots {
crate::bin_cli::output::line(
"Configured docs.snippets roots that do not exist (every snippet check that walks \
these passes having examined nothing -- fix the dirs/inline_dirs entry or create \
the directory):",
);
for directory in &missing_snippet_roots {
crate::bin_cli::output::line(format_args!(" {directory}"));
}
}
if !create_once_template_drift.is_empty() {
crate::bin_cli::output::line(
"Create-once scaffold files that predate a template fix (informational -- these are \
user-owned after their first write, so alef never rewrites them; review the current \
template and hand-port the fix if it applies to your copy):",
);
for path in &create_once_template_drift {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
let untracked_records = cache::untracked_required_records(&base_dir);
if !untracked_records.is_empty() {
crate::bin_cli::output::line(
"Required alef records are not tracked by git (alef writes these and depends on them \
being committed):",
);
for record in &untracked_records {
crate::bin_cli::output::line(format_args!(" {record} -- fix with: git add {record}"));
}
}
let unmarked_seeds = crate::bin_cli::helpers::frozen::unmarked_create_once_seeds(&frozen_generated_files);
for path in &unmarked_seeds {
tracing::debug!("unmarked create-once seed (contents not verified): {path}");
}
for line in super::super::verify_coverage::VerifyCoverage::measure(
&all_managed_paths,
&marked_paths,
scan_coverage,
unmarked_seeds.len(),
ephemeral_excluded_count,
)
.report_lines()
{
crate::bin_cli::output::line(line);
}
if stale.is_empty()
&& !has_missing_files
&& !has_missing_gitignored_files
&& !has_adoptable_frozen_files
&& !has_orphan_files
&& !has_abi_disagreement
&& !has_frb_generated_drift
&& !has_version_issues
&& snippet_coverage_issues.is_empty()
&& untracked_records.is_empty()
&& !has_stage_failures
&& !has_missing_snippet_roots
{
crate::bin_cli::output::line("All bindings and versions are up to date.");
} else {
if !stale.is_empty() {
crate::bin_cli::output::line("Stale bindings detected:");
for s in &stale {
crate::bin_cli::output::line(format_args!(" {}", s.path));
if tracing::enabled!(tracing::Level::DEBUG) {
crate::bin_cli::output::line(format_args!(" embedded: {}", s.embedded));
let computed_str = s.computed.join(", ");
crate::bin_cli::output::line(format_args!(" computed: {computed_str}"));
}
}
}
if has_missing_files {
crate::bin_cli::output::line("Missing generated files detected:");
for path in &missing_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
if has_missing_gitignored_files {
crate::bin_cli::output::line(
"Missing generated files that are also gitignored detected (running `alef generate` \
cannot fix these -- the file would be written, then discarded by the matching \
.gitignore rule before it can be committed; narrow the ignore rule for each path \
below, then commit the file):",
);
for path in &missing_gitignored_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
for line in crate::bin_cli::helpers::frozen::report_lines(&frozen_generated_files) {
crate::bin_cli::output::line(line);
}
if has_orphan_files {
crate::bin_cli::output::line(
"Orphaned generated files detected (alef's marker is present but the current run's \
backends would not produce these paths -- a backend may have stopped emitting them, \
they were dropped from generation config, or the file is a create-once seed alef only \
writes when absent; review each and delete by hand if genuinely stale, alef never \
deletes automatically):",
);
for path in &orphan_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
if has_stage_failures {
crate::bin_cli::output::line(
"Generation debt detected while collecting the managed surface (missing/frozen \
files above are still accurate; this is additional, separate debt):",
);
for failure in &stage_failures {
crate::bin_cli::output::line(format_args!(" {failure}"));
}
}
}
super::super::verify_outcome::ensure_success(
!stale.is_empty()
|| has_missing_files
|| has_missing_gitignored_files
|| has_adoptable_frozen_files
|| has_orphan_files
|| has_abi_disagreement
|| has_frb_generated_drift
|| has_stage_failures,
has_version_issues,
snippet_coverage_issues.len(),
report_only,
)?;
super::ensure_required_records_tracked(&untracked_records, report_only)?;
ensure_configured_snippet_directories_exist(&missing_snippet_roots, report_only)?;
Ok(None)
}
fn log_managed_surface(managed_paths: &std::collections::HashSet<std::path::PathBuf>) {
if !tracing::enabled!(tracing::Level::DEBUG) {
return;
}
let mut paths: Vec<&std::path::PathBuf> = managed_paths.iter().collect();
paths.sort();
tracing::debug!(
"managed surface the orphan report is diffed against: {} path(s)",
paths.len()
);
for path in paths {
tracing::debug!(" managed: {}", path.display());
}
}
fn frb_generated_drift(config: &crate::core::config::ResolvedCrateConfig, base_dir: &std::path::Path) -> Vec<String> {
let Some((lib_rs_path, frb_generated_path)) = crate::backends::dart::frb_rust_facade_paths(config) else {
return Vec::new();
};
let lib_rs_path = base_dir.join(lib_rs_path);
let frb_generated_path = base_dir.join(frb_generated_path);
let (Ok(lib_rs), Ok(frb_generated)) = (
std::fs::read_to_string(&lib_rs_path),
std::fs::read_to_string(&frb_generated_path),
) else {
return Vec::new();
};
let canonical = pipeline::canonical_frb_generated(&lib_rs, &frb_generated, &frb_generated_path);
if canonical == frb_generated {
return Vec::new();
}
vec![format!("[{}] {}", config.name, frb_generated_path.display())]
}
fn missing_snippet_directories(
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Vec<String> {
let Some(snippets) = config.docs.as_ref().and_then(|docs| docs.snippets.as_ref()) else {
return Vec::new();
};
snippets
.dirs
.iter()
.chain(&snippets.inline_dirs)
.filter_map(|dir| {
let resolved = base_dir.join(dir);
(!resolved.exists()).then(|| {
format!(
"[{}] {} (resolved to {})",
config.name,
dir.display(),
resolved.display()
)
})
})
.collect()
}
fn ensure_configured_snippet_directories_exist(missing: &[String], report_only: bool) -> Result<()> {
if report_only || missing.is_empty() {
return Ok(());
}
anyhow::bail!(
"configured docs.snippets roots do not exist: {}. Fix the dirs/inline_dirs entries in \
alef.toml or create the directories -- until then every snippet check that walks them \
reports a clean run having examined nothing",
missing.join(", ")
)
}
#[cfg(test)]
mod frb_generated_drift_tests {
use super::frb_generated_drift;
fn dart_config(name: &str) -> crate::core::config::ResolvedCrateConfig {
let toml_text = format!(
"[workspace]\nlanguages = [\"dart\"]\n\n[[crates]]\nname = \"{name}\"\nsources = [\"src/lib.rs\"]\n"
);
let cfg: crate::core::config::new_config::NewAlefConfig =
toml::from_str(&toml_text).expect("test config must parse");
cfg.resolve().expect("test config must resolve").remove(0)
}
fn write_frb_fixture(
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
lib_rs: &str,
frb_generated_rs: &str,
) -> std::path::PathBuf {
let (lib_rs_path, frb_generated_path) =
crate::backends::dart::frb_rust_facade_paths(config).expect("dart FRB style always has facade paths");
let lib_rs_path = base_dir.join(lib_rs_path);
let frb_generated_path = base_dir.join(frb_generated_path);
std::fs::create_dir_all(lib_rs_path.parent().expect("facade path has a parent dir"))
.expect("create facade src dir");
std::fs::write(&lib_rs_path, lib_rs).expect("write facade lib.rs");
std::fs::write(&frb_generated_path, frb_generated_rs).expect("write frb_generated.rs");
frb_generated_path
}
const LIB_RS: &str = "pub fn add(a: i64, b: i64) -> i64 {\n a + b\n}\n";
const RAW_FROM_TOOL: &str = "use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, \
Lockable};\nfn wire__crate__add_impl() {}\n";
#[test]
fn flags_unformatted_frb_generated_as_drift() {
let _cwd_lock = crate::test_support::CWD_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
if !crate::cli::pipeline::is_tool_available("rustfmt") {
return;
}
let config = dart_config("sample-lib");
let base_dir = tempfile::tempdir().expect("tempdir");
write_frb_fixture(&config, base_dir.path(), LIB_RS, RAW_FROM_TOOL);
let drift = frb_generated_drift(&config, base_dir.path());
assert_eq!(
drift.len(),
1,
"unformatted frb_generated.rs must be reported as drift: {drift:?}"
);
}
#[test]
fn is_silent_once_frb_generated_is_canonical() {
let _cwd_lock = crate::test_support::CWD_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
if !crate::cli::pipeline::is_tool_available("rustfmt") {
return;
}
let config = dart_config("sample-lib");
let base_dir = tempfile::tempdir().expect("tempdir");
let frb_generated_path = write_frb_fixture(&config, base_dir.path(), LIB_RS, RAW_FROM_TOOL);
let canonical = crate::cli::pipeline::canonical_frb_generated(LIB_RS, RAW_FROM_TOOL, &frb_generated_path);
std::fs::write(&frb_generated_path, &canonical).expect("write canonical frb_generated.rs");
let drift = frb_generated_drift(&config, base_dir.path());
assert!(
drift.is_empty(),
"already-canonical frb_generated.rs must not be reported as drift: {drift:?}"
);
}
}