alef 0.80.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Trait-bridge forwarding, visitor-field forwarding, and export-reachability coverage.
//!
//! Split out of `tests.rs` (see `file-modularization` in CLAUDE.md): these tests share a single
//! concern -- whether the wasm backend correctly threads a trait-bridge or visitor handle through
//! a generated `From` impl, and whether it correctly decides a function or bridge operation is
//! reachable/exported for a given `ApiSurface` and config.

use super::{WasmCallability, forward_trait_bridge_builder_fields, function_is_exported, wasm_callability};
use crate::core::config::{BridgeBinding, NewAlefConfig, ResolvedCrateConfig, TraitBridgeConfig};
use crate::core::ir::FunctionDef;

fn make_config() -> ResolvedCrateConfig {
    let cfg: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["wasm"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.wasm]
"#,
    )
    .unwrap();
    cfg.resolve().unwrap().remove(0)
}

#[test]
fn test_visitor_field_substitution_in_post_process() {
    let mut content = "impl From<WasmConversionOptions> for sample_markup_rs::options::ConversionOptions {\n    fn from(val: WasmConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\nimpl From<WasmConversionOptionsUpdate> for sample_markup_rs::options::ConversionOptionsUpdate {\n    fn from(val: WasmConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n".to_string();

    let field_name = "visitor";
    let patterns = &[
        ("            ", "\n            "),
        ("        ", "\n        "),
        ("  ", "\n  "),
    ];
    for (indent, newline_indent) in patterns {
        let old_pattern = format!("{indent}{field_name}: Default::default(),{newline_indent}..Default::default()");
        let new_pattern = format!(
            "{indent}{field_name}: val.{field_name}.map(|v| (*v.inner).clone()),{newline_indent}..Default::default()"
        );
        if content.contains(&old_pattern) {
            content = content.replace(&old_pattern, &new_pattern);
        }
    }

    assert!(
        content.contains("visitor: val.visitor.map(|v| (*v.inner).clone()),"),
        "Visitor field not forwarded in From impl"
    );
    assert!(
        !content.contains("visitor: Default::default(),\n            ..Default::default()"),
        "Unreplaced visitor: Default::default() with 12 spaces still present"
    );
}

#[test]
fn trait_bridge_builder_field_forwards_the_handle() {
    let bridge = TraitBridgeConfig {
        trait_name: "Renderer".to_string(),
        param_name: Some("renderer".to_string()),
        bind_via: BridgeBinding::OptionsField,
        options_type: Some("RenderOptions".to_string()),
        options_field: Some("renderer".to_string()),
        ..Default::default()
    };
    let content = "core_options.renderer(renderer.as_ref().map(|v| &v.inner));".to_string();
    let mut config = make_config();
    config.trait_bridges = vec![bridge];

    let generated = forward_trait_bridge_builder_fields(content, &config);

    assert_eq!(
        generated,
        "core_options.renderer(renderer.map(|v| (*v.inner).clone()));"
    );
    assert!(!generated.contains(".renderer(None)"));
}

#[test]
fn wasm_function_reachability_follows_target_features() {
    let functions = vec![
        FunctionDef {
            name: "download".into(),
            rust_path: "sample::download".into(),
            cfg: Some(r#"feature = "download""#.into()),
            ..FunctionDef::default()
        },
        FunctionDef {
            name: "prefetch".into(),
            rust_path: "sample::prefetch".into(),
            cfg: Some(r#"not(feature = "download")"#.into()),
            ..FunctionDef::default()
        },
    ];
    let config = make_config();

    assert!(!function_is_exported("download", &functions, &config));
    assert!(function_is_exported("prefetch", &functions, &config));
}

fn reachability_functions() -> Vec<FunctionDef> {
    vec![
        FunctionDef {
            name: "download_assets".into(),
            rust_path: "sample::download_assets".into(),
            ..FunctionDef::default()
        },
        FunctionDef {
            name: "gated_download".into(),
            rust_path: "sample::gated_download".into(),
            cfg: Some(r#"feature = "download""#.into()),
            ..FunctionDef::default()
        },
    ]
}

#[test]
fn wasm_callability_accepts_the_javascript_spelling_of_an_exported_function() {
    let functions = reachability_functions();
    let config = make_config();

    assert_eq!(
        wasm_callability("downloadAssets", &functions, &config),
        WasmCallability::Callable,
        "`overrides.wasm.function` names the symbol the way wasm-bindgen exports it"
    );
    assert_eq!(
        wasm_callability("download_assets", &functions, &config),
        WasmCallability::Callable,
        "the Rust spelling must keep working for calls that carry no override"
    );
}

#[test]
fn wasm_callability_accepts_a_bridge_registry_operation_under_either_spelling() {
    let functions = reachability_functions();
    let mut config = make_config();
    config.trait_bridges = vec![TraitBridgeConfig {
        trait_name: "RerankerBackend".into(),
        clear_fn: Some("clear_reranker_backends".into()),
        unregister_fn: Some("unregister_reranker_backend".into()),
        ..Default::default()
    }];

    assert_eq!(
        wasm_callability("clearRerankerBackends", &functions, &config),
        WasmCallability::Callable
    );
    assert_eq!(
        wasm_callability("unregister_reranker_backend", &functions, &config),
        WasmCallability::Callable
    );
    assert!(
        !function_is_exported("clear_reranker_backends", &functions, &config),
        "the codegen predicate must keep answering `false` -- the plain function generator does \
         not emit bridge-managed functions, the trait-bridge generator does"
    );
}

#[test]
fn wasm_callability_tells_an_unknown_name_apart_from_an_unexported_one() {
    let functions = reachability_functions();
    let config = make_config();

    assert_eq!(
        wasm_callability("gatedDownload", &functions, &config),
        WasmCallability::NotExported,
        "a real function the target drops is a capability gap"
    );
    assert_eq!(
        wasm_callability("fetchAssets", &functions, &config),
        WasmCallability::UnknownSymbol,
        "a name nothing answers to is a config error and must not be reported as a capability gap"
    );
    assert_eq!(
        wasm_callability("", &functions, &config),
        WasmCallability::UnknownSymbol,
        "an unresolved name must never be answered with a confident `not exported`"
    );
}

#[test]
fn wasm_callability_honours_an_exclusion_reached_by_the_javascript_spelling() {
    let cfg: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["wasm"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.wasm]
exclude_functions = ["download_assets"]
"#,
    )
    .expect("an exclusion list must deserialize");
    let config = cfg.resolve().expect("config must resolve").remove(0);

    assert_eq!(
        wasm_callability("downloadAssets", &reachability_functions(), &config),
        WasmCallability::NotExported,
        "resolving the JavaScript spelling must not route around `exclude_functions`"
    );
}