alef 0.71.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! `enum_conversions.rs` used to name every enum variant unconditionally in the generated
//! `From` impls, with no awareness of `EnumVariant::cfg` at all (the module contained zero
//! occurrences of the string `cfg` before this fix). A host-owned variant behind
//! `#[cfg(feature = "x")]` produced an unconditional match arm naming it, which is `E0599` in
//! any build where `x` is off. A variant merged in from a foreign `[[crates.source_crates]]`
//! crate produced the same unconditional arm referencing a feature this generated crate never
//! declares, which is `unexpected cfg condition value`.
//!
//! The fix (`emit_cfg_gated_arm` in `enum_conversions.rs`) asks
//! `codegen::cfg::is_host_owned_rust_path` the same question every other cfg-aware backend
//! (php, ffi, rustler, magnus, napi, wasm) already asks: a host-owned gated variant keeps its
//! arm under a matching `#[cfg(...)]` guard; a foreign-owned one has its arm dropped entirely.

use super::super::ExtendrBackend;
use super::make_config;
use crate::core::backend::Backend;
use crate::core::ir::*;

fn gated_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
    EnumVariant {
        name: name.to_string(),
        cfg: cfg.map(str::to_string),
        ..Default::default()
    }
}

fn returning_function(name: &str, enum_name: &str) -> FunctionDef {
    FunctionDef {
        name: name.to_string(),
        rust_path: format!("test_lib::{name}"),
        return_type: TypeRef::Named(enum_name.to_string()),
        ..Default::default()
    }
}

fn generate_r(api: &ApiSurface) -> String {
    ExtendrBackend
        .generate_bindings(api, &make_config())
        .expect("extendr generation")
        .iter()
        .map(|f| format!("// ==== {} ====\n{}", f.path.display(), f.content))
        .collect::<Vec<_>>()
        .join("\n")
}

/// Host-owned enum (`rust_path` starts with the configured core crate name, `test_lib`): the
/// cfg-gated variant's arm must survive in BOTH conversion directions, each wrapped in the exact
/// `#[cfg(...)]` guard from the source, and the ungated variant's arm must carry no guard at all.
///
/// Fails on pre-fix code: `enum_conversions.rs`'s original `gen_from_binding_to_core` /
/// `gen_from_core_to_binding` built every arm via an unconditional `.map(...)` (never a
/// `.filter_map(...)`) and never read `variant.cfg`, so no `#[cfg(feature = "beta")]` was ever
/// emitted anywhere in the output -- the assertions below on that exact guard text fail.
#[test]
fn host_owned_cfg_gated_variant_keeps_arm_under_matching_cfg_guard() {
    let api = ApiSurface {
        enums: vec![EnumDef {
            name: "Status".to_string(),
            rust_path: "test_lib::Status".to_string(),
            variants: vec![
                gated_variant("Active", None),
                gated_variant("Beta", Some(r#"feature = "beta""#)),
            ],
            ..Default::default()
        }],
        functions: vec![returning_function("get_status", "Status")],
        ..Default::default()
    };

    let out = generate_r(&api);

    // Precondition: the ungated variant's arm must exist with no guard, otherwise the "cfg
    // guard precedes only the gated arm" assertions below could pass vacuously.
    assert!(
        out.contains("Status::Active => Self::Active,"),
        "ungated variant's binding->core arm missing, fixture no longer exercises conversion:\n{out}"
    );
    assert!(
        out.contains("test_lib::Status::Active => Self::Active,"),
        "ungated variant's core->binding arm missing, fixture no longer exercises conversion:\n{out}"
    );

    assert!(
        out.contains("#[cfg(feature = \"beta\")]\n            Status::Beta => Self::Beta,"),
        "host-owned cfg-gated variant must keep its binding->core arm under a matching #[cfg(...)] guard:\n{out}"
    );
    assert!(
        out.contains("#[cfg(feature = \"beta\")]\n            test_lib::Status::Beta => Self::Beta,"),
        "host-owned cfg-gated variant must keep its core->binding arm under a matching #[cfg(...)] guard:\n{out}"
    );
}

/// Foreign-owned enum (`rust_path` does not start with the configured core crate name): the
/// cfg-gated variant's arm must be dropped entirely in BOTH directions -- the generated crate
/// never declares a Cargo feature for a foreign crate's gate, so forwarding it is
/// `unexpected cfg condition value`, and because `cfg(test)`-shaped gates are satisfied under
/// `cargo clippy --all-targets`, an ungated arm naming a variant that may not exist would still
/// compile and fail `E0599`. The ungated sibling variant's arm must still be present.
///
/// Fails on pre-fix code the same way as the host-owned test: the original code never dropped
/// any arm and never guarded any arm, so `External::Bar` (and `foreign_crate::External::Bar`)
/// appear unconditionally in pre-fix output -- the `!out.contains(...)` assertions below fail.
#[test]
fn foreign_owned_cfg_gated_variant_drops_arm_entirely() {
    let api = ApiSurface {
        enums: vec![EnumDef {
            name: "External".to_string(),
            rust_path: "foreign_crate::External".to_string(),
            variants: vec![
                gated_variant("Foo", None),
                gated_variant("Bar", Some(r#"feature = "extra""#)),
            ],
            ..Default::default()
        }],
        functions: vec![returning_function("get_external", "External")],
        ..Default::default()
    };

    let out = generate_r(&api);

    assert!(
        out.contains("External::Foo => Self::Foo,"),
        "ungated variant's binding->core arm missing, fixture no longer exercises conversion:\n{out}"
    );
    assert!(
        out.contains("foreign_crate::External::Foo => Self::Foo,"),
        "ungated variant's core->binding arm missing, fixture no longer exercises conversion:\n{out}"
    );

    assert!(
        !out.contains("External::Bar"),
        "a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n{out}"
    );
    assert!(
        !out.contains(r#"#[cfg(feature = "extra")]"#),
        "a foreign crate's cfg gate must never be forwarded into this generated crate:\n{out}"
    );
}

/// Negative control: an enum with no cfg-gated variant at all must emit no `#[cfg(...)]` guard
/// and no catch-all fallback arm in its conversion impls. Without this, a "fix" that always adds
/// a catch-all (or that drops every gated arm indiscriminately regardless of ownership) would
/// still make the two tests above pass while quietly changing behavior for the common case.
#[test]
fn ungated_enum_emits_no_cfg_guard_and_no_catch_all() {
    let api = ApiSurface {
        enums: vec![EnumDef {
            name: "Plain".to_string(),
            rust_path: "test_lib::Plain".to_string(),
            variants: vec![gated_variant("On", None), gated_variant("Off", None)],
            ..Default::default()
        }],
        functions: vec![returning_function("get_plain", "Plain")],
        ..Default::default()
    };

    let out = generate_r(&api);

    assert!(
        out.contains("Plain::On => Self::On,") && out.contains("Plain::Off => Self::Off,"),
        "both ungated variants' binding->core arms must be present:\n{out}"
    );
    assert!(
        !out.contains("#[cfg("),
        "an ungated enum must not emit any #[cfg(...)] guard:\n{out}"
    );
    assert!(
        !out.contains("_ => Self::default()"),
        "an ungated enum with no data/excluded variants must not emit a catch-all fallback arm:\n{out}"
    );
}