use super::cfg_gates::{cfg_gated_free_functions, free_pub_fn_name};
use super::text_transformations::{contains_function_at_token_boundary, snake_to_camel};
use std::collections::{BTreeSet, HashMap, HashSet};
pub fn free_function_names(lib_rs_source: &str) -> Vec<String> {
lib_rs_source.lines().filter_map(free_pub_fn_name).collect()
}
fn cfg_predicate(gate: &str) -> &str {
gate.strip_prefix("#[cfg(")
.and_then(|rest| rest.strip_suffix(")]"))
.unwrap_or(gate)
}
fn cfg_predicate_feature_names(predicate: &str) -> BTreeSet<String> {
let mut names = BTreeSet::new();
crate::codegen::cfg::collect_cfg_feature_names(predicate, &mut names);
names
}
fn active_free_function_names(
lib_rs_source: &str,
enabled_features: Option<&HashSet<&str>>,
declared_features: Option<&HashSet<&str>>,
) -> Vec<String> {
let gates: HashMap<String, String> = cfg_gated_free_functions(lib_rs_source).into_iter().collect();
free_function_names(lib_rs_source)
.into_iter()
.filter(|name| {
let Some(gate) = gates.get(name) else {
return true;
};
let Some(features) = enabled_features else {
return true;
};
if crate::core::ir::cfg_feature_satisfied(Some(cfg_predicate(gate)), features) {
return true;
}
match declared_features {
Some(declared) => cfg_predicate_feature_names(cfg_predicate(gate))
.iter()
.any(|feature_name| !declared.contains(feature_name.as_str())),
None => true,
}
})
.collect()
}
pub fn undeclared_gate_features(
lib_rs_source: &str,
function_name: &str,
declared_features: &HashSet<&str>,
) -> BTreeSet<String> {
cfg_gated_free_functions(lib_rs_source)
.into_iter()
.find(|(name, _)| name == function_name)
.map(|(_, gate)| {
cfg_predicate_feature_names(cfg_predicate(&gate))
.into_iter()
.filter(|feature_name| !declared_features.contains(feature_name.as_str()))
.collect()
})
.unwrap_or_default()
}
pub fn missing_bridge_functions(
lib_rs_source: &str,
bridge_dart_source: &str,
exclude_functions: &[String],
enabled_features: Option<&HashSet<&str>>,
declared_features: Option<&HashSet<&str>>,
) -> Vec<String> {
let excluded: HashSet<&str> = exclude_functions.iter().map(String::as_str).collect();
active_free_function_names(lib_rs_source, enabled_features, declared_features)
.into_iter()
.filter(|name| !excluded.contains(name.as_str()))
.filter(|name| {
let camel = snake_to_camel(name);
!contains_function_at_token_boundary(bridge_dart_source, &camel)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn free_function_names_lists_every_top_level_pub_fn() {
let lib_rs = "\
use std::sync::Arc;
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
pub async fn record_price(id: String, price_cents: i64) -> Result<(), String> {
Ok(())
}
fn private_helper() {}
";
assert_eq!(
free_function_names(lib_rs),
vec!["count_widgets".to_string(), "record_price".to_string()],
);
}
#[test]
fn missing_bridge_functions_reports_a_facade_function_absent_from_the_bridge() {
let lib_rs = "\
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {
Ok(())
}
";
let bridge_dart = "Future<int> countWidgets({required String collection}) => RustLib.instance.api.crateCountWidgets(collection: collection);\n";
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], None, None);
assert_eq!(
missing,
vec!["record_price".to_string()],
"record_price is declared in the facade but has no matching function in the bridge"
);
}
#[test]
fn missing_bridge_functions_is_empty_when_every_facade_function_is_bridged() {
let lib_rs = "pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n";
let bridge_dart = "Future<int> countWidgets({required String collection}) => RustLib.instance.api.crateCountWidgets(collection: collection);\n";
assert!(missing_bridge_functions(lib_rs, bridge_dart, &[], None, None).is_empty());
}
#[test]
fn missing_bridge_functions_finds_a_function_whose_name_is_wrapped_onto_its_own_line() {
let lib_rs = "\
pub fn create_chunk_classification_definition_from_json(
json: String,
) -> Result<ChunkClassificationDefinition, String> {
todo!()
}
";
let bridge_dart = "\
Future<ChunkClassificationDefinition>
createChunkClassificationDefinitionFromJson({required String json}) =>
RustLib.instance.api.crateCreateChunkClassificationDefinitionFromJson(json: json);
";
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], None, None);
assert_eq!(
missing,
Vec::<String>::new(),
"the function is present and correctly bridged -- only line-wrapped -- and must not \
be reported missing: {missing:?}"
);
}
#[test]
fn missing_bridge_functions_ignores_configured_exclusions() {
let lib_rs = "\
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
pub fn internal_only(id: String) -> Result<(), String> {
Ok(())
}
";
let bridge_dart = "Future<int> countWidgets({required String collection}) => RustLib.instance.api.crateCountWidgets(collection: collection);\n";
let missing = missing_bridge_functions(lib_rs, bridge_dart, &["internal_only".to_string()], None, None);
assert!(
missing.is_empty(),
"excluded function must not be reported missing: {missing:?}"
);
}
#[test]
fn missing_bridge_functions_ignores_a_facade_function_behind_a_declared_but_inactive_cfg_gate() {
let lib_rs = "\
#[cfg(feature = \"premium-tier\")]
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {
Ok(json)
}
";
let bridge_dart = "";
let enabled: HashSet<&str> = HashSet::new();
let declared: HashSet<&str> = ["premium-tier"].into_iter().collect();
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], Some(&enabled), Some(&declared));
assert!(
missing.is_empty(),
"a facade function behind a declared-but-inactive cfg gate must not be reported \
missing: {missing:?}"
);
}
#[test]
fn missing_bridge_functions_still_reports_a_function_behind_an_undeclared_cfg_gate() {
let lib_rs = "\
#[cfg(feature = \"widgets\")]
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
";
let bridge_dart = "";
let enabled: HashSet<&str> = HashSet::new();
let declared: HashSet<&str> = HashSet::new();
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], Some(&enabled), Some(&declared));
assert_eq!(
missing,
vec!["count_widgets".to_string()],
"a facade function behind an undeclared cfg gate must still be reported missing: \
{missing:?}"
);
}
#[test]
fn missing_bridge_functions_still_reports_a_genuinely_missing_function_under_an_active_gate() {
let lib_rs = "\
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
#[cfg(feature = \"premium-tier\")]
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {
Ok(json)
}
";
let bridge_dart = "";
let enabled: HashSet<&str> = ["premium-tier"].into_iter().collect();
let declared: HashSet<&str> = ["premium-tier"].into_iter().collect();
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], Some(&enabled), Some(&declared));
assert_eq!(
missing,
vec![
"count_widgets".to_string(),
"create_premium_backend_options_from_json".to_string(),
],
"an ungated function and a function under an active gate must both still be reported \
missing: {missing:?}"
);
}
#[test]
fn missing_bridge_functions_ignores_an_inactive_gate_behind_an_intervening_frb_attribute() {
let lib_rs = "\
#[cfg(feature = \"premium-tier\")]
#[frb]
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {
Ok(json)
}
";
let bridge_dart = "";
let enabled: HashSet<&str> = HashSet::new();
let declared: HashSet<&str> = ["premium-tier"].into_iter().collect();
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], Some(&enabled), Some(&declared));
assert!(
missing.is_empty(),
"a gate followed by an intervening #[frb] attribute must still be recognized and, \
being inactive but declared, must not be reported missing: {missing:?}"
);
}
#[test]
fn missing_bridge_functions_still_reports_a_genuinely_missing_function_behind_an_intervening_frb_attribute() {
let lib_rs = "\
#[cfg(feature = \"premium-tier\")]
#[frb]
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {
Ok(json)
}
";
let bridge_dart = "";
let enabled: HashSet<&str> = ["premium-tier"].into_iter().collect();
let declared: HashSet<&str> = ["premium-tier"].into_iter().collect();
let missing = missing_bridge_functions(lib_rs, bridge_dart, &[], Some(&enabled), Some(&declared));
assert_eq!(
missing,
vec!["create_premium_backend_options_from_json".to_string()],
"a function under an active gate behind an intervening #[frb] attribute must still \
be reported missing when absent from the bridge: {missing:?}"
);
}
#[test]
fn undeclared_gate_features_is_empty_for_an_ungated_function() {
let lib_rs = "pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n";
let declared: HashSet<&str> = HashSet::new();
assert!(undeclared_gate_features(lib_rs, "count_widgets", &declared).is_empty());
}
#[test]
fn undeclared_gate_features_is_empty_when_the_gate_is_declared() {
let lib_rs = "\
#[cfg(feature = \"premium-tier\")]
pub fn create_premium_backend_options_from_json(json: String) -> Result<String, String> {
Ok(json)
}
";
let declared: HashSet<&str> = ["premium-tier"].into_iter().collect();
assert!(undeclared_gate_features(lib_rs, "create_premium_backend_options_from_json", &declared).is_empty());
}
#[test]
fn undeclared_gate_features_names_a_feature_the_manifest_never_declared() {
let lib_rs = "\
#[cfg(feature = \"widgets\")]
pub fn count_widgets(collection: String) -> Result<i64, String> {
Ok(0)
}
";
let declared: HashSet<&str> = HashSet::new();
let undeclared = undeclared_gate_features(lib_rs, "count_widgets", &declared);
assert_eq!(
undeclared,
BTreeSet::from(["widgets".to_string()]),
"the gate's feature name must be reported as undeclared: {undeclared:?}"
);
}
}