use crate::codegen::cfg::{collect_cfg_feature_alternatives, collect_cfg_features};
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use std::collections::{BTreeSet, HashSet};
pub(crate) fn source_crate_has_feature(config: &ResolvedCrateConfig, core_crate_dir: &str, feature: &str) -> bool {
let root = match config.workspace_root.as_deref() {
Some(p) => p.to_path_buf(),
None => match std::env::current_dir() {
Ok(p) => p,
Err(_) => return false,
},
};
let cargo_toml = root.join("crates").join(core_crate_dir).join("Cargo.toml");
let Ok(content) = std::fs::read_to_string(&cargo_toml) else {
return false;
};
let needle_line_start = format!("{feature} =");
let mut in_features = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_features = trimmed == "[features]";
continue;
}
if in_features && trimmed.starts_with(&needle_line_start) {
return true;
}
}
false
}
pub(crate) fn configured_swift_features(
config: &ResolvedCrateConfig,
core_crate_dir: &str,
api: &ApiSurface,
) -> Vec<String> {
let base_features = config.features_for_language(Language::Swift);
let excluded: BTreeSet<&str> = config
.swift
.as_ref()
.map(|c| c.excluded_default_features.iter().map(String::as_str).collect())
.unwrap_or_default();
let mut features: BTreeSet<String> = base_features.iter().cloned().collect();
let full_active = features.contains("full");
for group in collect_cfg_feature_alternatives(api) {
if !full_active && group.is_disjoint(&features) {
continue;
}
for companion in &group {
if excluded.contains(companion.as_str()) {
continue;
}
if !features.contains(companion) && source_crate_has_feature(config, core_crate_dir, companion) {
features.insert(companion.clone());
}
}
}
features.into_iter().collect()
}
pub(crate) fn effective_swift_codegen_features(
api: &ApiSurface,
config: &ResolvedCrateConfig,
core_crate_dir: &str,
) -> Vec<String> {
let mut features: BTreeSet<String> = configured_swift_features(config, core_crate_dir, api)
.into_iter()
.collect();
let excluded: HashSet<&str> = config
.swift
.as_ref()
.map(|c| c.excluded_default_features.iter().map(String::as_str).collect())
.unwrap_or_default();
for feature in collect_cfg_features(api) {
if !excluded.contains(feature.as_str()) {
features.insert(feature);
}
}
features.into_iter().collect()
}
pub(super) fn cfg_satisfied(cfg: Option<&str>, configured_features: &HashSet<&str>) -> bool {
crate::core::ir::cfg_feature_satisfied(cfg, configured_features)
}
pub(crate) fn with_cfg_filtered_methods(api: &ApiSurface, configured_features: &HashSet<&str>) -> ApiSurface {
let mut filtered = api.clone();
for typ in &mut filtered.types {
typ.methods.retain(|method| method.cfg_satisfied(configured_features));
}
for enum_def in &mut filtered.enums {
enum_def
.methods
.retain(|method| method.cfg_satisfied(configured_features));
}
for error in &mut filtered.errors {
error.methods.retain(|method| method.cfg_satisfied(configured_features));
}
filtered
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cfg_satisfied_feature_matching() {
let mut features = HashSet::new();
features.insert("pdf");
features.insert("html");
assert!(cfg_satisfied(Some("feature = \"pdf\""), &features));
assert!(!cfg_satisfied(Some("feature = \"heuristics\""), &features));
assert!(cfg_satisfied(None, &features));
let mut full_features = HashSet::new();
full_features.insert("full");
assert!(cfg_satisfied(Some("feature = \"heuristics\""), &full_features));
}
#[test]
fn test_cfg_satisfied_any_matching() {
let mut features = HashSet::new();
features.insert("ocr");
assert!(cfg_satisfied(
Some("any(feature = \"ocr\", feature = \"paddle-ocr\")"),
&features
));
assert!(!cfg_satisfied(
Some("any(feature = \"heuristics\", feature = \"embeddings\")"),
&features
));
}
#[test]
fn configured_swift_features_widens_an_active_alternative_pair() {
let temp = tempfile::tempdir().expect("create fixture workspace root");
let crate_dir = temp.path().join("crates").join("demo_core");
std::fs::create_dir_all(&crate_dir).expect("create fixture crate dir");
std::fs::write(
crate_dir.join("Cargo.toml"),
"[features]\nengine-native = []\nengine-portable = []\n",
)
.expect("write fixture Cargo.toml");
let config = ResolvedCrateConfig {
name: "demo".to_string(),
features: vec!["engine-native".to_string()],
workspace_root: Some(temp.path().to_path_buf()),
..ResolvedCrateConfig::default()
};
let api = ApiSurface {
crate_name: "demo".to_string(),
types: vec![crate::core::ir::TypeDef {
name: "Engine".to_string(),
rust_path: "demo::Engine".to_string(),
cfg: Some(r#"any(feature = "engine-native", feature = "engine-portable")"#.to_string()),
..Default::default()
}],
..Default::default()
};
let features = configured_swift_features(&config, "demo_core", &api);
assert!(
features.iter().any(|f| f == "engine-portable"),
"expected the source crate's paired feature to be widened in, got: {features:?}"
);
}
#[test]
fn configured_swift_features_does_not_inject_a_feature_the_source_crate_lacks() {
let temp = tempfile::tempdir().expect("create fixture workspace root");
let crate_dir = temp.path().join("crates").join("demo_core");
std::fs::create_dir_all(&crate_dir).expect("create fixture crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), "[features]\nengine-native = []\n")
.expect("write fixture Cargo.toml");
let config = ResolvedCrateConfig {
name: "demo".to_string(),
features: vec!["engine-native".to_string()],
workspace_root: Some(temp.path().to_path_buf()),
..ResolvedCrateConfig::default()
};
let api = ApiSurface {
crate_name: "demo".to_string(),
types: vec![crate::core::ir::TypeDef {
name: "Engine".to_string(),
rust_path: "demo::Engine".to_string(),
cfg: Some(r#"any(feature = "engine-native", feature = "engine-portable")"#.to_string()),
..Default::default()
}],
..Default::default()
};
let features = configured_swift_features(&config, "demo_core", &api);
assert!(
!features.iter().any(|f| f == "engine-portable"),
"must not inject a feature the source crate never declares, got: {features:?}"
);
}
#[test]
fn configured_swift_features_leaves_an_inactive_pair_alone() {
let temp = tempfile::tempdir().expect("create fixture workspace root");
let crate_dir = temp.path().join("crates").join("demo_core");
std::fs::create_dir_all(&crate_dir).expect("create fixture crate dir");
std::fs::write(
crate_dir.join("Cargo.toml"),
"[features]\nengine-native = []\nengine-portable = []\n",
)
.expect("write fixture Cargo.toml");
let config = ResolvedCrateConfig {
name: "demo".to_string(),
features: vec!["unrelated".to_string()],
workspace_root: Some(temp.path().to_path_buf()),
..ResolvedCrateConfig::default()
};
let api = ApiSurface {
crate_name: "demo".to_string(),
types: vec![crate::core::ir::TypeDef {
name: "Engine".to_string(),
rust_path: "demo::Engine".to_string(),
cfg: Some(r#"any(feature = "engine-native", feature = "engine-portable")"#.to_string()),
..Default::default()
}],
..Default::default()
};
let features = configured_swift_features(&config, "demo_core", &api);
assert_eq!(features, vec!["unrelated".to_string()]);
}
#[test]
fn configured_swift_features_never_widens_an_excluded_companion() {
let temp = tempfile::tempdir().expect("create fixture workspace root");
let crate_dir = temp.path().join("crates").join("demo_core");
std::fs::create_dir_all(&crate_dir).expect("create fixture crate dir");
std::fs::write(
crate_dir.join("Cargo.toml"),
"[features]\nocr = []\nocr-wasm = []\nheic = []\n",
)
.expect("write fixture Cargo.toml");
let config = ResolvedCrateConfig {
name: "demo".to_string(),
features: vec!["ocr".to_string()],
workspace_root: Some(temp.path().to_path_buf()),
swift: Some(crate::core::config::languages::SwiftConfig {
excluded_default_features: vec!["heic".to_string()],
..Default::default()
}),
..ResolvedCrateConfig::default()
};
let api = ApiSurface {
crate_name: "demo".to_string(),
types: vec![crate::core::ir::TypeDef {
name: "ExifReader".to_string(),
rust_path: "demo::ExifReader".to_string(),
cfg: Some(r#"any(feature = "ocr", feature = "ocr-wasm", feature = "heic")"#.to_string()),
..Default::default()
}],
..Default::default()
};
let features = configured_swift_features(&config, "demo_core", &api);
assert!(
!features.iter().any(|f| f == "heic"),
"an excluded companion must never be widened into the dependency's features list, got: {features:?}"
);
assert!(
features.iter().any(|f| f == "ocr-wasm"),
"a non-excluded companion in the same group must still be widened in, got: {features:?}"
);
}
#[test]
fn configured_swift_features_unchanged_when_no_cfg_alternatives_exist() {
let temp = tempfile::tempdir().expect("create fixture workspace root");
let crate_dir = temp.path().join("crates").join("demo_core");
std::fs::create_dir_all(&crate_dir).expect("create fixture crate dir");
std::fs::write(
crate_dir.join("Cargo.toml"),
"[features]\nengine-native = []\nengine-portable = []\n",
)
.expect("write fixture Cargo.toml");
let config = ResolvedCrateConfig {
name: "demo".to_string(),
features: vec!["engine-native".to_string()],
workspace_root: Some(temp.path().to_path_buf()),
..ResolvedCrateConfig::default()
};
let api = ApiSurface {
crate_name: "demo".to_string(),
types: vec![
crate::core::ir::TypeDef {
name: "Engine".to_string(),
rust_path: "demo::Engine".to_string(),
cfg: Some(r#"feature = "engine-native""#.to_string()),
..Default::default()
},
crate::core::ir::TypeDef {
name: "PortableWidget".to_string(),
rust_path: "demo::PortableWidget".to_string(),
cfg: Some(r#"feature = "engine-portable""#.to_string()),
..Default::default()
},
],
..Default::default()
};
let features = configured_swift_features(&config, "demo_core", &api);
assert_eq!(features, vec!["engine-native".to_string()]);
}
}