alef 0.76.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Coverage for [`core_default_features_active`] and [`enabled_features_for_language`].
//!
//! Split out of the parent test module so both files stay under the `file-modularization` cap.
//!
//! Regression for the E0004 hazard `expand_configured_features` used to leave open: a core crate
//! whose `[features] default = [...]` enables a feature that gates a FOREIGN cfg enum variant, and
//! whose binding `alef.toml` never names that feature explicitly, must still count that feature as
//! active for every language whose Cargo dependency edge to the core crate does not suppress
//! defaults (every language except R, and R only when it opts out explicitly -- see
//! `core_default_features_active`'s doc). Getting this backward in either direction is a real
//! bug: undercounting drops a reachable variant's catch-all and produces a non-exhaustive match
//! (`error[E0004]`) the moment cargo actually turns the feature on; overcounting (treating a
//! feature as active when the binding's Cargo.toml genuinely never turns it on) would resurrect
//! the unreachable-catch-all warning noise the 0.72.0 cfg-reachability work was written to remove.

use crate::codegen::cfg::{core_default_features_active, enabled_features_for_language};
use crate::core::config::{Language, RConfig, ResolvedCrateConfig};
use std::collections::BTreeSet;

/// Write a minimal core crate manifest with the given `[features]` body under
/// `<dir>/crates/my-lib/Cargo.toml`, and return a `ResolvedCrateConfig` pointed at it.
fn config_with_core_features(dir: &std::path::Path, features_body: &str, r: Option<RConfig>) -> ResolvedCrateConfig {
    let core_dir = dir.join("crates").join("my-lib");
    std::fs::create_dir_all(&core_dir).expect("create core crate dir");
    std::fs::write(
        core_dir.join("Cargo.toml"),
        format!("[package]\nname = \"my-lib\"\n\n[features]\n{features_body}"),
    )
    .expect("write core Cargo.toml");

    ResolvedCrateConfig {
        workspace_root: Some(dir.to_path_buf()),
        name: "my-lib".to_string(),
        sources: vec![std::path::PathBuf::from("crates/my-lib/src/lib.rs")],
        r,
        ..Default::default()
    }
}

fn set(names: &[&str]) -> BTreeSet<String> {
    names.iter().map(|n| (*n).to_string()).collect()
}

/// [`core_default_features_active`] must be unconditionally `true` for every language but R: none
/// of them has a base-line (non-per-target-override) knob that can suppress the core crate's own
/// `default-features`. This is the authority [`enabled_features_for_language`] asks before
/// unioning the core crate's declared defaults in, so it must agree with what
/// `scaffold::render_core_dep`/`render_core_dep_with_overrides` actually emit for the base branch.
#[test]
fn core_default_features_active_is_unconditionally_true_outside_r() {
    let config = ResolvedCrateConfig::default();
    for lang in [
        Language::Go,
        Language::Java,
        Language::Kotlin,
        Language::KotlinAndroid,
        Language::Csharp,
        Language::Python,
        Language::Ruby,
        Language::Node,
        Language::Php,
        Language::Elixir,
        Language::Wasm,
        Language::Zig,
        Language::Dart,
        Language::Swift,
        Language::Ffi,
    ] {
        assert!(
            core_default_features_active(&config, lang),
            "{lang:?}'s base core dependency edge never emits `default-features = false`, so its \
             defaults must always read as active"
        );
    }
}

/// R's `default_features` flag is `None`/unset (the common case, and the same default
/// `scaffold_r_cargo` assumes via `unwrap_or(true)`): defaults stay active.
#[test]
fn core_default_features_active_for_r_defaults_to_true_when_unset() {
    let config = ResolvedCrateConfig {
        r: Some(RConfig {
            features: Some(vec!["curated".to_string()]),
            default_features: None,
            ..r_config_defaults()
        }),
        ..Default::default()
    };
    assert!(core_default_features_active(&config, Language::R));
}

/// `[crates.r] default_features = false` with an EMPTY configured feature list must still read as
/// active: `scaffold_r_cargo` keeps the plain (defaults-active) dependency line whenever
/// `features_for_language(Language::R)` is empty, regardless of the flag -- there is nothing to
/// put in defaults' place on the emitted Cargo.toml line, so treating them as suppressed here
/// would disagree with what actually got scaffolded.
#[test]
fn core_default_features_active_for_r_stays_true_with_no_replacement_features() {
    let config = ResolvedCrateConfig {
        r: Some(RConfig {
            features: Some(vec![]),
            default_features: Some(false),
            ..r_config_defaults()
        }),
        ..Default::default()
    };
    assert!(core_default_features_active(&config, Language::R));
}

/// The one real suppression case: `default_features = false` AND a non-empty configured list --
/// `scaffold_r_cargo` emits `default-features = false, features = [...]` on this exact
/// combination, so defaults must read as inactive.
#[test]
fn core_default_features_active_for_r_is_false_with_default_features_disabled_and_a_replacement_list() {
    let config = ResolvedCrateConfig {
        r: Some(RConfig {
            features: Some(vec!["curated".to_string()]),
            default_features: Some(false),
            ..r_config_defaults()
        }),
        ..Default::default()
    };
    assert!(!core_default_features_active(&config, Language::R));
}

fn r_config_defaults() -> RConfig {
    RConfig {
        package_name: None,
        features: None,
        default_features: None,
        serde_rename_all: None,
        exclude_functions: Vec::new(),
        exclude_types: Vec::new(),
        rename_fields: std::collections::HashMap::new(),
        run_wrapper: None,
        extra_lint_paths: Vec::new(),
        extra_makevars_prelude: Vec::new(),
        extra_pkg_libs: Vec::new(),
    }
}

/// THE E0004 REGRESSION: a foreign cfg gate satisfied only by the core crate's own declared
/// `default = [...]` -- never named in the binding's own configured feature list at all -- must
/// still count as enabled. Before this fix, `expand_configured_features` was handed
/// `features_for_language` alone, so a variant gated on `feature = "extended-mode"` here would be wrongly
/// "proven unreachable" by `enum_conversion_needs_catch_all_for_features`, its catch-all dropped,
/// and the generated match left non-exhaustive the moment cargo (which really does enable `extended-mode`
/// via the core crate's own default) compiled the real variant in.
#[test]
fn enabled_features_for_language_includes_a_core_default_the_binding_never_configured() {
    let dir = tempfile::tempdir().expect("tempdir");
    let config = config_with_core_features(dir.path(), "default = [\"extended-mode\"]\nextended-mode = []\n", None);

    let enabled: BTreeSet<String> = enabled_features_for_language(&config, Language::Go)
        .into_iter()
        .collect();

    assert_eq!(
        enabled,
        set(&["extended-mode"]),
        "a core-declared default feature must be counted as active even though this binding's own \
         `alef.toml` never names it, got: {enabled:?}"
    );
}

/// A core-crate aggregate named only inside `default = [...]` (not as a leaf the binding
/// configured) must still expand to its transitive members, exactly like an aggregate the binding
/// configures directly -- the union with defaults happens before expansion, not instead of it.
#[test]
fn enabled_features_for_language_expands_an_aggregate_reachable_only_through_core_defaults() {
    let dir = tempfile::tempdir().expect("tempdir");
    let config = config_with_core_features(
        dir.path(),
        "default = [\"mobile-target\"]\nmobile-target = [\"tokenizer\"]\ntokenizer = []\n",
        None,
    );

    let enabled: BTreeSet<String> = enabled_features_for_language(&config, Language::Dart)
        .into_iter()
        .collect();

    assert_eq!(
        enabled,
        set(&["mobile-target", "tokenizer"]),
        "the aggregate named in core's own defaults, and its transitive member, must both count \
         as enabled, got: {enabled:?}"
    );
}

/// THE OTHER DIRECTION: a feature genuinely never turned on -- R with `default_features = false`
/// and a configured replacement list that does not include it -- must still be proven unreachable.
/// A fix that unconditionally unions in core defaults regardless of `core_default_features_active`
/// would make this assert fail, reintroducing the unreachable-catch-all warning noise the
/// reachability work this defends was written to remove.
#[test]
fn enabled_features_for_language_does_not_resurrect_a_feature_r_genuinely_suppressed() {
    let dir = tempfile::tempdir().expect("tempdir");
    let config = config_with_core_features(
        dir.path(),
        "default = [\"extended-mode\"]\nextended-mode = []\ncurated = []\n",
        Some(RConfig {
            features: Some(vec!["curated".to_string()]),
            default_features: Some(false),
            ..r_config_defaults()
        }),
    );

    let enabled: BTreeSet<String> = enabled_features_for_language(&config, Language::R)
        .into_iter()
        .collect();

    assert_eq!(
        enabled,
        set(&["curated"]),
        "`extended-mode` is a core default this R binding's Cargo.toml explicitly suppresses -- it must \
         stay absent, got: {enabled:?}"
    );
}