alef 0.83.3

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Issue #380: `fn tag_record(record: &mut Record)` must not silently drop the mutation.
//!
//! These tests exercise the real `GoBackend::generate_bindings` path end to end -- not just the
//! shared `codegen::mut_writeback` policy module in isolation -- so they prove the backend is
//! actually wired to the policy, not merely that the policy itself is correct.

use alef::backends::go::GoBackend;
use alef::core::backend::Backend;
use alef::core::config::ResolvedCrateConfig;
use alef::core::config::new_config::NewAlefConfig;
use alef::core::ir::*;

fn resolved_one(toml: &str) -> ResolvedCrateConfig {
    let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
    cfg.resolve().unwrap().remove(0)
}

fn make_config() -> ResolvedCrateConfig {
    resolved_one(
        r#"
[workspace]
languages = ["ffi", "go"]

[[crates]]
name = "krz-lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "krz"

[crates.go]
module = "github.com/test/krz-lib"
"#,
    )
}

fn record_type() -> TypeDef {
    TypeDef {
        name: "Record".to_string(),
        rust_path: "krz_lib::Record".to_string(),
        original_rust_path: String::new(),
        fields: vec![FieldDef {
            version: Default::default(),
            name: "score".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U32),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            serde_with: None,
            serde_skip_serializing_if: false,
            serde_skip: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        }],
        methods: vec![],
        is_opaque: false,
        is_clone: true,
        is_copy: false,
        is_trait: false,
        has_default: false,
        has_stripped_cfg_fields: false,
        is_return_type: false,
        serde_rename_all: None,
        has_serde: true,
        serde_container_default: false,
        serde_container_conversion: Default::default(),
        super_traits: vec![],
        doc: "A record that gets mutated in place.".to_string(),
        cfg: None,
        binding_excluded: false,
        binding_exclusion_reason: None,
        is_variant_wrapper: false,
        has_lifetime_params: false,
        has_private_fields: false,
        version: Default::default(),
    }
}

fn mut_param(name: &str, type_name: &str) -> ParamDef {
    ParamDef {
        name: name.to_string(),
        ty: TypeRef::Named(type_name.to_string()),
        optional: false,
        default: None,
        sanitized: false,
        typed_default: None,
        is_ref: true,
        is_mut: true,
        newtype_wrapper: None,
        original_type: None,
        map_is_ahash: false,
        map_key_is_cow: false,
        vec_inner_is_ref: false,
        map_is_btree: false,
        core_wrapper: CoreWrapper::None,
    }
}

fn base_api(functions: Vec<FunctionDef>) -> ApiSurface {
    ApiSurface {
        crate_name: "krz-lib".to_string(),
        version: "0.1.0".to_string(),
        types: vec![record_type()],
        functions,
        enums: vec![],
        errors: vec![],
        excluded_type_paths: Default::default(),
        excluded_trait_names: Default::default(),
        services: vec![],
        handler_contracts: vec![],
        unsupported_public_items: Vec::new(),
    }
}

#[test]
fn generate_bindings_writes_back_the_mutated_record_end_to_end() {
    let api = base_api(vec![FunctionDef {
        name: "tag_record".to_string(),
        params: vec![mut_param("record", "Record")],
        return_type: TypeRef::Unit,
        error_type: None,
        ..Default::default()
    }]);

    let files = GoBackend
        .generate_bindings(&api, &make_config())
        .expect("generation must succeed for the supported single-&mut-DTO-param shape");
    let content = &files
        .iter()
        .find(|f| f.path.ends_with("binding.go"))
        .expect("binding.go must be generated")
        .content;

    assert!(
        content.contains("func TagRecord(record Record) (*Record, error) {"),
        "must return the updated value instead of a bare error, got:\n{content}"
    );
    assert!(
        content.contains("C.krz_tag_record(cRecord)"),
        "must still call the FFI mutator, got:\n{content}"
    );
    assert!(
        content.contains("C.krz_record_to_json(cRecord)"),
        "must read the mutated handle back out, got:\n{content}"
    );
    assert!(
        !content.contains("C.krz_tag_record(cRecord)\n\treturn nil\n}"),
        "must not regress to the lossy call-then-return-nil shape, got:\n{content}"
    );
}

#[test]
fn generate_bindings_rejects_a_mut_dto_param_combined_with_a_non_unit_return() {
    let api = base_api(vec![FunctionDef {
        name: "tag_and_count".to_string(),
        params: vec![mut_param("record", "Record")],
        return_type: TypeRef::Primitive(PrimitiveType::U32),
        error_type: None,
        ..Default::default()
    }]);

    let err = GoBackend
        .generate_bindings(&api, &make_config())
        .expect_err("a &mut DTO param combined with a non-unit return must fail generation");
    let message = err.to_string();
    assert!(
        message.contains("tag_and_count"),
        "the diagnostic must name the offending function, got: {message}"
    );
}