alef 0.83.1

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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! Orchestration for [`crate::core::backend::PostBuildStep::VerifyFrbBridgeCoverage`]: reads the
//! FRB facade and the flutter_rust_bridge-generated bridge off disk and fails the build loudly
//! when the bridge is missing a function the facade declares.
//!
//! Split out of `build.rs` (already at this repo's 1,000-line cap) rather than left inline —
//! see alef #135 for the shape this closes: `PostBuildStep::RunCommand`'s missing-tool/
//! `ALEF_SKIP_COMMANDS` fallback treats a skipped `flutter_rust_bridge_codegen` invocation as
//! success, and the `PostProcessFile` steps that follow it in the same post-build sequence
//! patch whatever bridge Dart source is already on disk regardless of whether frb actually
//! regenerated it this run. [`verify`] is the gate `run_post_build` calls between those two:
//! a stale bridge now aborts the post-build sequence for this language instead of receiving
//! alef's patches on top of missing functions.
//!
//! [`verify`] also resolves the enabled-feature set flutter_rust_bridge's own codegen macro
//! expansion would have used -- the Rust crate's own `[features] default = [...]`, read from
//! `facade_file`'s sibling `Cargo.toml` (`facade_file` is always `<rust crate dir>/src/lib.rs`) --
//! and passes it through so [`crate::backends::dart::missing_bridge_functions`] never reports a
//! `#[cfg(...)]`-gated facade function frb correctly never saw, alongside every function it
//! genuinely dropped. It also resolves that same manifest's *declared* `[features]` keys (not
//! just the `default`-enabled subset) so a missing function whose gate names a feature the
//! manifest never declares at all -- most often because alef's own generation wanted to forward
//! that feature there and the write was refused -- is attributed to that cause instead of the
//! generic "rerun flutter_rust_bridge_codegen" guidance, which is wrong for that case: frb never
//! had a chance to see the function regardless of when it last ran (alef #464). See
//! [`crate::backends::dart::frb_rewrite::bridge_coverage`]'s module doc for the declared-vs-active
//! distinction this rests on.

use anyhow::Context as _;
use std::collections::{BTreeSet, HashSet};
use std::path::Path;

/// Read `facade_file` and `bridge_file` and return an error naming every facade free function
/// (outside `exclude_functions`, and reachable given the sibling manifest's default features)
/// that has no matching function in the bridge.
///
/// A no-op when either file does not exist yet — mirrors every other `PostBuildStep` handler in
/// `run_post_build`, which treats "nothing to check yet" as fine rather than an error (a
/// project's first-ever generation has not produced a bridge to compare against).
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()))?;

    // `facade_file` is always `<rust crate dir>/src/lib.rs` (see
    // `backends::dart::gen_bindings::frb_rust_facade_paths`), so its manifest is two directories
    // up. A missing/unparseable manifest resolves both `enabled_features` and `declared_features`
    // to `None` together (both read and parse the same file the same way), and
    // `missing_bridge_functions` degrades to the pre-cfg-awareness blanket check rather than
    // exempting every gated function from coverage.
    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(());
    }

    // Attribute each missing name to an undeclared cfg gate when applicable -- see
    // `undeclared_gate_features`'s doc. This is what lets the diagnostic below name the real
    // cause (a manifest missing a `[features]` entry, commonly from a refused write) instead of
    // defaulting every gap to "rerun flutter_rust_bridge_codegen", which cannot fix an undeclared
    // gate: frb's codegen macro can never see a function gated on a feature its manifest does not
    // declare, no matter how many times it reruns. ~keep
    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");
        // The facade gained a second function that the (stale) bridge never picked up --
        // the alef #135 shape: frb was skipped this pass and the bridge on disk predates
        // `record_price`.
        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());
    }

    /// A facade function gated behind a feature the sibling `Cargo.toml` declares but does not
    /// enable by default must not be reported missing, because flutter_rust_bridge's own codegen
    /// macro expansion never saw it either. Uses the real `<rust crate dir>/src/lib.rs` +
    /// `<rust crate dir>/Cargo.toml` layout `verify` resolves the manifest from, not the flat
    /// `dir.path()` layout the other tests in this module use for their cfg-blind fixtures.
    ///
    /// "Declared but off" is the key distinction from
    /// `verify_fails_when_a_facade_function_is_gated_on_a_feature_the_manifest_never_declared`
    /// below: this manifest's `[features]` table names `premium-tier` explicitly, just not in
    /// `default` -- a deliberate, legitimate choice, unlike a feature the table never mentions
    /// at all.
    #[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();
        // The bridge has nothing for this function -- exactly what a correct frb run produces
        // when the gating feature is off.
        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"
        );
    }

    /// Negative control for the test above: a facade function whose gate IS in the manifest's
    /// default features, but that is genuinely absent from the bridge, must still fail the check.
    /// Without this control, a fix that stopped evaluating cfg gates at all (treating every gated
    /// function as inactive) would pass the positive test above while quietly breaking the
    /// coverage check's entire purpose.
    #[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();
        // The bridge is stale: the gate IS active by default, so frb should have bridged this
        // function, and did not.
        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}"
        );
    }

    /// The alef #464 regression: a facade function gated on a feature the sibling
    /// `Cargo.toml` does not declare AT ALL (no `[features]` table whatsoever here -- the exact
    /// shape a pre-marker-convention `packages/dart/rust/Cargo.toml` has) must still fail the
    /// check. Before this fix, `active_free_function_names` could not tell "declared but off"
    /// apart from "never declared" -- both simply read as absent from the enabled set -- so this
    /// case was silently treated the same as the legitimate one in
    /// `verify_ignores_a_facade_function_behind_a_declared_but_inactive_cfg_gate` above, and
    /// `alef all` returned `Ok` even though the facade and its manifest had fallen out of sync
    /// (most often because a forwarding write to that manifest was refused by the ownership
    /// guard). The message must also name the real cause rather than defaulting to "rerun
    /// flutter_rust_bridge_codegen", which cannot fix a gate on an undeclared feature no matter
    /// how many times it runs.
    #[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();
        // No `[features]` table at all -- the manifest never gained a `widgets` entry, exactly
        // as if the ownership guard refused a `collect_cfg_features` write to it.
        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}"
        );
    }

    /// End-to-end proof that `run_post_build` itself refuses to patch a stale bridge: when the
    /// `RunCommand` step for the frb codegen tool is skipped (here because the configured
    /// command name does not exist on `PATH` -- `run_run_command` reports that as `Ok(false)`
    /// deterministically, with no process-global state involved) and the facade has since
    /// gained a function the committed bridge lacks, the whole post-build sequence must error
    /// out before the `PostProcessFile` step that follows ever touches the bridge file.
    ///
    /// Deliberately does not use the real `flutter_rust_bridge_codegen` command name gated by
    /// `ALEF_SKIP_COMMANDS`: that env var is process-global, and a test-local lock around
    /// setting it only serializes against other holders of that *same* lock instance -- it does
    /// nothing against the crate's other test modules that set the same variable under their own
    /// separate lock (see `run_command_tests::env_lock` in `build.rs`), so two lock instances
    /// guarding one process resource race exactly like the unguarded `current_dir()` callers did
    /// for `CWD_LOCK`. A command name that is simply never installed sidesteps the shared
    /// resource entirely. ~keep
    #[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();
        // Stale committed bridge: predates `record_price`. Carries a trailing space so that
        // if the `DartStripTrailingWhitespace` step below were (wrongly) reached, its effect on
        // this file would be observable.
        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 {
                    // Never installed on any PATH -- `run_run_command` reports a missing tool as
                    // `Ok(false)` (skipped) deterministically, standing in for "frb was skipped
                    // this run" without touching real process-global state.
                    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![],
                },
                // Would rewrite `bridge_rel` in place if reached -- proving it is unreached is
                // the point of this test.
                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"
        );
    }
}