use super::cfg_gates::{cfg_gated_free_functions, free_pub_fn_name};
use super::text_transformations::{contains_function_at_token_boundary, frb_dart_function_name};
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 = frb_dart_function_name(name);
!contains_function_at_token_boundary(bridge_dart_source, &camel)
})
.collect()
}
fn struct_field_pairs(lib_rs_source: &str) -> Vec<(String, String)> {
let mut pairs = Vec::new();
let mut current_struct: Option<String> = None;
let mut depth: i32 = 0;
for line in lib_rs_source.lines() {
if current_struct.is_none() {
if let Some(name) = pub_struct_name(line) {
current_struct = Some(name);
depth = brace_delta(line);
}
continue;
}
depth += brace_delta(line);
if depth <= 0 {
current_struct = None;
continue;
}
if let Some(field) = pub_field_name(line.trim()) {
pairs.push((current_struct.clone().unwrap_or_default(), field));
}
}
pairs
}
fn brace_delta(line: &str) -> i32 {
line.matches('{').count() as i32 - line.matches('}').count() as i32
}
fn pub_struct_name(line: &str) -> Option<String> {
let trimmed = line.trim();
let rest = trimmed.strip_prefix("pub struct ")?;
if !trimmed.ends_with('{') {
return None;
}
let name_end = rest.find(|c: char| c == '{' || c == '<' || c.is_whitespace())?;
let name = &rest[..name_end];
(!name.is_empty()).then(|| name.to_string())
}
fn pub_field_name(trimmed_line: &str) -> Option<String> {
let rest = trimmed_line.strip_prefix("pub ")?;
let colon = rest.find(':')?;
let name = rest[..colon].trim();
let is_identifier = !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
is_identifier.then(|| name.to_string())
}
fn is_top_level_type_header(line: &str) -> bool {
if line.is_empty() || line.starts_with(' ') || line.starts_with('\t') {
return false;
}
[
"class ",
"sealed class ",
"abstract class ",
"base class ",
"final class ",
"mixin ",
"enum ",
]
.iter()
.any(|prefix| line.starts_with(prefix))
}
fn is_class_header_for(line: &str, class_name: &str) -> bool {
if !is_top_level_type_header(line) {
return false;
}
let Some(after_class_kw) = line.split_once("class ").map(|(_, rest)| rest) else {
return false;
};
let name_end = after_class_kw
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(after_class_kw.len());
&after_class_kw[..name_end] == class_name
}
fn dart_class_body(dart_bridge_source: &str, class_name: &str) -> Option<String> {
let lines: Vec<&str> = dart_bridge_source.lines().collect();
let start = lines.iter().position(|line| is_class_header_for(line, class_name))?;
let end = lines[start + 1..]
.iter()
.position(|line| is_top_level_type_header(line))
.map_or(lines.len(), |offset| start + 1 + offset);
Some(lines[start..end].join("\n"))
}
fn contains_identifier_at_token_boundary(haystack: &str, name: &str) -> bool {
if name.is_empty() {
return false;
}
let is_identifier_char = |c: char| c.is_alphanumeric() || c == '_' || c == '$';
let mut search_start = 0;
while let Some(relative_offset) = haystack[search_start..].find(name) {
let absolute_offset = search_start + relative_offset;
let preceding_ok = !matches!(haystack[..absolute_offset].chars().next_back(), Some(c) if is_identifier_char(c));
let following_ok =
!matches!(haystack[absolute_offset + name.len()..].chars().next(), Some(c) if is_identifier_char(c));
if preceding_ok && following_ok {
return true;
}
search_start = absolute_offset + 1;
}
false
}
pub fn missing_bridge_struct_fields(
lib_rs_source: &str,
bridge_dart_source: &str,
exclude_types: &[String],
) -> Vec<String> {
let excluded: HashSet<&str> = exclude_types.iter().map(String::as_str).collect();
struct_field_pairs(lib_rs_source)
.into_iter()
.filter(|(struct_name, _)| !excluded.contains(struct_name.as_str()))
.filter_map(|(struct_name, field_name)| {
let body = dart_class_body(bridge_dart_source, &struct_name)?;
let camel = frb_dart_function_name(&field_name);
(!contains_identifier_at_token_boundary(&body, &camel)).then(|| format!("{struct_name}.{field_name}"))
})
.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:?}"
);
}
const PAGE_RESULT_LIB_RS: &str = "\
#[frb(mirror(CrawlPageResult))]
pub struct CrawlPageResult {
/// The page URL.
pub url: String,
/// Redirect hops taken to reach `final_url` from `url`.
pub redirect_count: i64,
}
";
const PAGE_RESULT_DART_UP_TO_DATE: &str = "\
class CrawlPageResult {
final String url;
/// Redirect hops taken to reach `final_url` from `url`.
final PlatformInt64 redirectCount;
const CrawlPageResult({required this.url, required this.redirectCount});
}
class OtherClass {
final int redirectCountUnrelated;
}
";
#[test]
fn struct_field_pairs_lists_every_named_field_of_every_top_level_struct() {
let lib_rs = "\
pub struct Point {
pub x: f64,
pub y: f64,
}
pub struct Tuple(pub String);
pub struct Unit;
pub enum Shape {
Circle { pub radius: f64 },
}
";
assert_eq!(
struct_field_pairs(lib_rs),
vec![
("Point".to_string(), "x".to_string()),
("Point".to_string(), "y".to_string()),
],
"only the named-field struct's own fields are collected -- tuple/unit structs and \
enum variant payloads are excluded"
);
}
#[test]
fn missing_bridge_struct_fields_reports_a_facade_field_absent_from_the_bridge_class() {
let stale_bridge = "\
class CrawlPageResult {
final String url;
const CrawlPageResult({required this.url});
}
";
let missing = missing_bridge_struct_fields(PAGE_RESULT_LIB_RS, stale_bridge, &[]);
assert_eq!(
missing,
vec!["CrawlPageResult.redirect_count".to_string()],
"a facade field with no matching Dart class property must be reported missing: {missing:?}"
);
}
#[test]
fn missing_bridge_struct_fields_is_empty_when_every_facade_field_is_bridged() {
let missing = missing_bridge_struct_fields(PAGE_RESULT_LIB_RS, PAGE_RESULT_DART_UP_TO_DATE, &[]);
assert!(
missing.is_empty(),
"every facade field is present on the bridge class and must not be reported: {missing:?}"
);
}
#[test]
fn missing_bridge_struct_fields_does_not_cross_class_boundaries() {
let stale_bridge = "\
class CrawlPageResult {
final String url;
const CrawlPageResult({required this.url});
}
class OtherClass {
final int redirectCount;
}
";
let missing = missing_bridge_struct_fields(PAGE_RESULT_LIB_RS, stale_bridge, &[]);
assert_eq!(
missing,
vec!["CrawlPageResult.redirect_count".to_string()],
"a same-named field on an unrelated class must not satisfy CrawlPageResult's own \
coverage: {missing:?}"
);
}
#[test]
fn missing_bridge_struct_fields_ignores_configured_exclusions() {
let stale_bridge = "\
class CrawlPageResult {
final String url;
const CrawlPageResult({required this.url});
}
";
let missing = missing_bridge_struct_fields(PAGE_RESULT_LIB_RS, stale_bridge, &["CrawlPageResult".to_string()]);
assert!(
missing.is_empty(),
"an excluded type's fields must not be reported missing: {missing:?}"
);
}
#[test]
fn dart_class_body_stops_at_the_next_top_level_type_header() {
let body = dart_class_body(PAGE_RESULT_DART_UP_TO_DATE, "CrawlPageResult")
.expect("CrawlPageResult class must be found");
assert!(body.contains("redirectCount"));
assert!(
!body.contains("redirectCountUnrelated"),
"the next class's body must not leak into CrawlPageResult's own: {body}"
);
}
#[test]
fn dart_class_body_is_none_for_an_absent_class() {
assert!(dart_class_body(PAGE_RESULT_DART_UP_TO_DATE, "NeverEmitted").is_none());
}
}