use anyhow::Context as _;
use std::collections::{BTreeSet, HashSet};
use std::path::Path;
pub(super) fn verify(facade_file: &Path, bridge_file: &Path, exclude_functions: &[String]) -> anyhow::Result<()> {
if !facade_file.exists() || !bridge_file.exists() {
tracing::debug!(
"VerifyFrbBridgeCoverage: facade or bridge not found ({} / {})",
facade_file.display(),
bridge_file.display()
);
return Ok(());
}
let facade_source = std::fs::read_to_string(facade_file)
.with_context(|| format!("failed to read FRB facade {}", facade_file.display()))?;
let bridge_source = std::fs::read_to_string(bridge_file)
.with_context(|| format!("failed to read FRB bridge {}", bridge_file.display()))?;
let manifest_path = facade_file
.parent()
.and_then(Path::parent)
.map(|dir| dir.join("Cargo.toml"));
let enabled_features = manifest_path
.as_deref()
.and_then(crate::codegen::cfg::read_default_enabled_cargo_features);
let declared_features = manifest_path
.as_deref()
.and_then(crate::codegen::cfg::read_declared_cargo_features);
let enabled_features_refs: Option<HashSet<&str>> = enabled_features
.as_ref()
.map(|set| set.iter().map(String::as_str).collect());
let declared_features_refs: Option<HashSet<&str>> = declared_features
.as_ref()
.map(|set| set.iter().map(String::as_str).collect());
let missing = crate::backends::dart::missing_bridge_functions(
&facade_source,
&bridge_source,
exclude_functions,
enabled_features_refs.as_ref(),
declared_features_refs.as_ref(),
);
if missing.is_empty() {
return Ok(());
}
let mut undeclared_functions: Vec<&str> = Vec::new();
let mut undeclared_features: BTreeSet<String> = BTreeSet::new();
if let Some(declared) = declared_features_refs.as_ref() {
for name in &missing {
let gate_features = crate::backends::dart::undeclared_gate_features(&facade_source, name, declared);
if !gate_features.is_empty() {
undeclared_functions.push(name.as_str());
undeclared_features.extend(gate_features);
}
}
}
let count = missing.len();
let suffix = if count == 1 { "" } else { "s" };
let bridge_display = bridge_file.display();
let facade_display = facade_file.display();
let names = missing.join(", ");
let guidance = if undeclared_functions.is_empty() {
"Each is reachable per this crate's manifest (ungated, or its `#[cfg(feature = ...)]` is \
in the manifest's default features), so the gap has one of several possible causes: \
flutter_rust_bridge_codegen did not (re)generate this bridge against the current facade; \
the manifest's default feature set does not actually match what the codegen run used; or \
the function's gate depends on a non-feature predicate (target_os, ...) this check cannot \
evaluate and treats as reachable. Determine which applies, then either rerun \
flutter_rust_bridge_codegen or update the committed bridge source -- alef's post-build \
patches must not be applied to a stale bridge."
.to_string()
} else {
let undeclared_function_names = undeclared_functions.join(", ");
let undeclared_feature_names = undeclared_features.into_iter().collect::<Vec<_>>().join(", ");
let manifest_display = manifest_path
.as_deref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "<unresolved manifest>".to_string());
format!(
"{} of these ({undeclared_function_names}) are gated on a Cargo feature this crate's \
manifest ({manifest_display}) does not declare at all: {undeclared_feature_names}. \
flutter_rust_bridge's codegen macro can only bridge a function through a feature its \
manifest actually declares, so this cannot be fixed by rerunning \
flutter_rust_bridge_codegen -- the manifest itself never gained the `[features]` \
entry, most commonly because a write alef's own generation wanted to make there was \
refused by the ownership guard. Check the refusal report above for this manifest and, \
if it was refused, run `alef adopt <path>`; otherwise the manifest needs the feature \
added to its `[features]` table by hand.",
undeclared_functions.len(),
)
};
anyhow::bail!(
"flutter_rust_bridge bridge {bridge_display} is missing {count} function{suffix} from \
{facade_display}: {names}. {guidance}"
);
}
#[cfg(test)]
mod tests {
use super::*;
const FACADE_ONE_FUNCTION: &str =
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n";
const BRIDGE_COVERING_IT: &str = "Future<int> countWidgets({required String collection}) => RustLib.instance.api.crateCountWidgets(collection: collection);\n";
#[test]
fn verify_passes_when_bridge_covers_every_facade_function() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(&facade, FACADE_ONE_FUNCTION).unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
assert!(verify(&facade, &bridge, &[]).is_ok());
}
#[test]
fn verify_fails_when_the_bridge_is_stale_relative_to_the_facade() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
let error = verify(&facade, &bridge, &[]).expect_err("stale bridge must fail the check");
let message = format!("{error:#}");
assert!(
message.contains("record_price"),
"error must name the missing function: {message}"
);
}
#[test]
fn verify_ignores_configured_exclusions() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn internal_only(id: String) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
assert!(verify(&facade, &bridge, &["internal_only".to_string()]).is_ok());
}
#[test]
fn verify_is_a_no_op_when_the_bridge_does_not_exist_yet() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(&facade, FACADE_ONE_FUNCTION).unwrap();
assert!(verify(&facade, &bridge, &[]).is_ok());
}
#[test]
fn verify_ignores_a_facade_function_behind_a_declared_but_inactive_cfg_gate() {
let dir = tempfile::tempdir().expect("temp dir");
let rust_dir = dir.path().join("rust");
std::fs::create_dir_all(rust_dir.join("src")).unwrap();
let facade = rust_dir.join("src/lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"#[cfg(feature = \"premium-tier\")]\n\
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {\n Ok(json)\n}\n",
)
.unwrap();
std::fs::write(
rust_dir.join("Cargo.toml"),
"[package]\nname = \"sample\"\nversion = \"0.1.0\"\n\n\
[features]\ndefault = []\npremium-tier = [\"sample-core/premium-tier\"]\n",
)
.unwrap();
std::fs::write(&bridge, "").unwrap();
assert!(
verify(&facade, &bridge, &[]).is_ok(),
"a facade function behind a feature the manifest does not enable by default must not \
fail the coverage check"
);
}
#[test]
fn verify_still_fails_when_a_facade_function_under_an_active_gate_is_missing() {
let dir = tempfile::tempdir().expect("temp dir");
let rust_dir = dir.path().join("rust");
std::fs::create_dir_all(rust_dir.join("src")).unwrap();
let facade = rust_dir.join("src/lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"#[cfg(feature = \"premium-tier\")]\n\
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {\n Ok(json)\n}\n",
)
.unwrap();
std::fs::write(
rust_dir.join("Cargo.toml"),
"[package]\nname = \"sample\"\nversion = \"0.1.0\"\n\n\
[features]\ndefault = [\"premium-tier\"]\npremium-tier = [\"sample-core/premium-tier\"]\n",
)
.unwrap();
std::fs::write(&bridge, "").unwrap();
let error = verify(&facade, &bridge, &[]).expect_err("a missing function under an active gate must still fail");
let message = format!("{error:#}");
assert!(
message.contains("create_premium_backend_options_from_json"),
"error must name the missing function: {message}"
);
}
#[test]
fn verify_fails_when_a_facade_function_is_gated_on_a_feature_the_manifest_never_declared() {
let dir = tempfile::tempdir().expect("temp dir");
let rust_dir = dir.path().join("rust");
std::fs::create_dir_all(rust_dir.join("src")).unwrap();
let facade = rust_dir.join("src/lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"#[cfg(feature = \"widgets\")]\n\
pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n",
)
.unwrap();
std::fs::write(
rust_dir.join("Cargo.toml"),
"[package]\nname = \"sample\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(&bridge, "").unwrap();
let error = verify(&facade, &bridge, &[])
.expect_err("a function gated on an undeclared feature must still fail the check");
let message = format!("{error:#}");
assert!(
message.contains("count_widgets") && message.contains("missing 1 function"),
"error must name the missing function and count: {message}"
);
assert!(
message.contains("widgets") && message.contains("does not declare"),
"error must name the undeclared feature: {message}"
);
assert!(
message.contains("alef adopt"),
"error must point at the actual remedy for a refused manifest write: {message}"
);
}
#[test]
fn run_post_build_aborts_before_patching_a_stale_bridge_when_frb_is_skipped() {
use crate::core::backend::{BuildConfig, BuildDependency, PostBuildStep, PostProcessor};
use crate::core::config::{Language, ResolvedCrateConfig};
let dir = tempfile::tempdir().expect("temp dir");
let facade_rel = std::path::PathBuf::from("lib.rs");
let bridge_rel = std::path::PathBuf::from("lib.dart");
std::fs::write(
dir.path().join(&facade_rel),
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
let stale_bridge_with_trailing_whitespace = "Future<int> countWidgets({required String collection}) => \nRustLib.instance.api.crateCountWidgets(collection: collection); \n";
std::fs::write(dir.path().join(&bridge_rel), stale_bridge_with_trailing_whitespace).unwrap();
let build_config = BuildConfig {
tool: "cargo",
crate_suffix: "-dart",
build_dep: BuildDependency::None,
post_build: vec![
PostBuildStep::RunCommand {
cmd: "alef-frb-codegen-intentionally-not-on-path-xyz789",
args: vec!["generate"],
},
PostBuildStep::VerifyFrbBridgeCoverage {
facade_path: facade_rel.clone(),
bridge_path: bridge_rel.clone(),
exclude_functions: vec![],
},
PostBuildStep::PostProcessFile {
path: bridge_rel.clone(),
processor: PostProcessor::DartStripTrailingWhitespace,
},
],
};
let result = crate::cli::pipeline::run_post_build(
Language::Dart,
&build_config,
&ResolvedCrateConfig::default(),
dir.path(),
crate::cli::pipeline::StagingProfile::PreferOnDisk,
);
let error = result.expect_err("a stale bridge behind a skipped frb run must fail the build");
assert!(
format!("{error:#}").contains("record_price"),
"error must name the missing function: {error:#}"
);
let bridge_after = std::fs::read_to_string(dir.path().join(&bridge_rel)).unwrap();
assert_eq!(
bridge_after, stale_bridge_with_trailing_whitespace,
"the PostProcessFile step after VerifyFrbBridgeCoverage must never run against the stale bridge"
);
}
}