alef 0.81.0

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//! Feature-gate helpers for generated swift-bridge crates.

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};

/// Check whether the umbrella source crate exposes the given feature name in its
/// on-disk Cargo.toml.
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
}

/// Widen `config`'s configured Swift feature list with any sibling feature the source crate
/// itself pairs with an already-active one.
///
/// A source crate may gate one capability behind either of two (or more) sibling Cargo features
/// (see [`collect_cfg_feature_alternatives`]) rather than nesting one feature inside another's own
/// feature list — for example, a full-dependency feature and a narrower one that swaps in a
/// substitute dependency compatible with cross-compiled targets. When the configured feature list
/// activates one side of such a pairing (directly, or via the `full` umbrella), the other side is
/// included too, but only when the source crate's on-disk `Cargo.toml` actually declares it — so a
/// crate that does not use this pattern for a given feature never has an unknown name injected.
///
/// A companion named in [`SwiftConfig::excluded_default_features`][excl] is never widened in, even
/// when its sibling in the `any(...)` group is active. This return value feeds straight into the
/// core dependency's own `features = [...]` line in the generated `Cargo.toml` (see
/// `cargo::emit_cargo_toml`'s `features` parameter) on a dependency edge that does **not** set
/// `default-features = false` — unlike [`effective_swift_codegen_features`], whose
/// `excluded_default_features` check only trims the wrapper crate's *own* `default = [...]` array.
/// Without this check, a group such as `any(feature = "ocr", feature = "heic")` (a shared gate
/// with no real alternation relationship — both are simply required by one item, not substitutes
/// for each other) would explicitly request `core/heic` the moment `ocr` is configured, activating
/// a pkg-config-only native dependency `excluded_default_features = ["heic"]` was written
/// specifically to keep off cross-compiled targets. Measured against a synthetic fixture workspace
/// (`cargo tree -e features`): a dependency line that lists an opt-in sibling feature explicitly
/// activates that feature's own optional dependency even though `default-features` is left on,
/// because Cargo does not otherwise turn on an opt-in feature nobody asked for — the widening loop
/// is a genuine new activation, not something feature unification would have done anyway. ~keep
///
/// [excl]: crate::core::config::languages::SwiftConfig::excluded_default_features
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()
}

/// Returns `true` when the `cfg` condition is satisfied by `configured_features`.
///
/// Thin wrapper over [`crate::core::ir::cfg_feature_satisfied`] so the Rust
/// bridge crate and the high-level Swift facade share one cfg-matching
/// implementation (keeping their `visible_*` sets in lockstep).
pub(super) fn cfg_satisfied(cfg: Option<&str>, configured_features: &HashSet<&str>) -> bool {
    crate::core::ir::cfg_feature_satisfied(cfg, configured_features)
}

/// Return a copy of `api` whose types', enums' and errors' methods are restricted to those whose
/// `#[cfg(...)]` gate `configured_features` satisfies.
///
/// The sibling of the `visible_types`/`visible_enums`/`visible_functions` filters, one level
/// deeper. Swift cannot express the gate on either side it emits: an `extern "Rust"` declaration
/// inside `#[swift_bridge::bridge]` is a macro input, not a free-standing item that can carry
/// `#[cfg]` per entry (which is why a gated *type* wraps its whole extern block instead), and the
/// Swift facade has no conditional compilation at all. So a gated method has to be dropped, not
/// gated — and dropped from the bridge-crate side and the Swift side by the *same* predicate, or
/// Swift calls a symbol the extern block never declared.
///
/// Applied once at each side's facade rather than threaded into the six method loops
/// (`emit_extern_block_for_type_methods`, `emit_extern_block_for_first_class_dto_methods`,
/// `emit_type_method_shims`, `emit_first_class_dto_method_wrappers`, `emit_client_class`,
/// `emit_error`): the two sides derive `configured_features` independently, so sharing one
/// filtering function is what makes their agreement structural instead of a property six
/// signatures have to keep re-establishing.
///
/// Deliberately narrower than [`crate::core::ir::ApiSurface::with_cfg_filtered_deep`], which also
/// drops gated fields and enum variants: swift already filters fields inline at each site that
/// consumes them (`wrappers::getters`, `extern_block::constructor_fields`), and re-filtering them
/// here would move that decision without being asked to. ~keep
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
        ));
    }

    /// A source crate can gate one capability behind either of two sibling features (see
    /// [`collect_cfg_feature_alternatives`]) instead of nesting one feature inside the other's own
    /// Cargo feature list. When the configured Swift feature list only names one side, the other
    /// side must still be widened in, provided the source crate's own `Cargo.toml` declares it.
    #[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:?}"
        );
    }

    /// The companion side of the widening test above: a feature name that only appears in the
    /// `any(...)` cfg gate, and that the source crate's own `Cargo.toml` never declares, must never
    /// be injected — that would hand Cargo a feature name it does not recognise.
    #[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:?}"
        );
    }

    /// Neither side of an alternative pair is active: no widening should happen even though the
    /// source crate declares both features, since nothing in the configured list triggers it.
    #[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()]);
    }

    /// Regression for the open risk in alef-task #319: widening a companion feature into the
    /// core dependency's `features = [...]` line bypassed `excluded_default_features` entirely
    /// -- that exclusion only trimmed the wrapper crate's own `default = [...]` array
    /// (`effective_swift_codegen_features`), not the list this function feeds straight into
    /// `cargo::emit_cargo_toml`'s dependency line. Measured against a synthetic fixture
    /// workspace (`cargo tree -e features`): the dependency line does not set
    /// `default-features = false`, and the companion feature is opt-in on the source crate (not
    /// part of its own `default`), so nothing else would have turned it on -- the widening was a
    /// genuine new activation of a pkg-config-only native dependency, not something feature
    /// unification would already have done. A name in `excluded_default_features` must never be
    /// widened in, even when its sibling in the same `any(...)` group is active.
    #[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()
        };
        // A three-way `any(...)` gate shared by unrelated capabilities (as in xberg's
        // `extraction/exif.rs`): `ocr` and `ocr-wasm` are genuine alternatives, `heic` merely
        // co-occurs in the same gate and pulls in a pkg-config-only native dependency.
        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:?}"
        );
    }

    /// A crate whose cfg gates carry no `any(...)` alternative at all — every gate is a bare
    /// `feature = "..."` — has nothing for [`collect_cfg_feature_alternatives`] to find, so the
    /// configured feature list must pass through completely unchanged. The source crate declares
    /// (and separately, and validly, gates a different item behind) a second feature,
    /// `engine-portable`, so a mechanism that widened on any cfg-referenced name — not just one
    /// paired with an active feature inside a shared `any(...)` — would inject it here; this
    /// pins that only true `any(...)` alternation, not mere co-occurrence anywhere in the surface,
    /// triggers widening.
    #[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()]);
    }
}