alef 0.110.4

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Kotlin source emitter for the AAR module (JNI mode).
//!
//! Produces a pure-Kotlin JNI layout with no bundled Java facade:
//!
//! - `<Module>Bridge.kt` — a Kotlin `object` with `external fun` JNI
//!   declarations and `init { System.loadLibrary("<crate>_jni") }`.
//! - `DefaultClient.kt` — coroutine-friendly client class holding a `Long`
//!   handle when the API has methodful types.
//! - Data classes, enums, and error types are emitted as `.kt` files via the
//!   kotlin backend's pub helpers.
//!
//! `KotlinFfiStyle::Jni` is forced by the parent backend (`lib.rs`) before
//! this module is called, so `config.kotlin_ffi_style()` will always return
//! `KotlinFfiStyle::Jni` here.

mod module_facade;
mod trait_interfaces;

use crate::core::config::Language;

pub use trait_interfaces::format_method_signature;

use std::collections::BTreeSet;
use std::path::Path;

use crate::backends::kotlin::{
    ValueMethodBridge, default_constructible_type_names, emit_enum_pub, emit_error_type_pub, emit_jni_bridge_object,
    emit_jni_client_class, emit_type_pub_with_defaults_sealed_and_constructible,
};
use crate::backends::kotlin_android::naming;
use crate::backends::kotlin_android::naming::kotlin_package;
use crate::backends::kotlin_android::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::jni::bridge_class_name;

/// Type names this emitter drops from the AAR's public surface: `[crates.kotlin_android]
/// exclude_types`, trait-bridge aliases excluded for this language (or whose bridging
/// function is excluded), and generic opaque types.
///
/// Shared with [`super::gen_seed_test`] rather than duplicated, so the scaffolded unit test
/// can never name a type this emitter refuses to emit. ~keep
pub(super) fn effective_excluded_type_names(config: &ResolvedCrateConfig) -> std::collections::HashSet<String> {
    let kotlin_android_excluded_function_names = naming::excluded_function_names(config);
    let mut effective_excluded_types: std::collections::HashSet<String> = config
        .kotlin_android
        .as_ref()
        .map(|c| c.exclude_types.iter().cloned().collect())
        .unwrap_or_default();
    for bridge in config.trait_bridges_for(Language::KotlinAndroid) {
        if bridge.exclude_languages.iter().any(|l| l == "kotlin_android")
            && let Some(alias) = &bridge.type_alias
        {
            effective_excluded_types.insert(alias.clone());
        }
        if let Some(name) = bridge.param_name.as_deref()
            && kotlin_android_excluded_function_names.contains(name)
            && let Some(alias) = &bridge.type_alias
        {
            effective_excluded_types.insert(alias.clone());
        }
    }
    for (name, path) in &config.opaque_types {
        if path.contains('<') {
            effective_excluded_types.insert(name.clone());
        }
    }
    effective_excluded_types
}

/// Emit all Kotlin source files for the AAR module.
///
/// `kotlin_source_dir` is the resolved Kotlin source destination —
/// `<project_root>/src/main/kotlin/<dotted_package_as_path>/` in the Gradle
/// Android source-set layout.
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig, kotlin_source_dir: &Path) -> Vec<GeneratedFile> {
    let package = kotlin_package(config);
    let mut files = Vec::new();

    let effective_excluded_types = effective_excluded_type_names(config);

    // Enums without a declared `#[default]` variant map to an empty string;
    let enum_defaults: std::collections::HashMap<String, String> = api
        .enums
        .iter()
        .filter(|en| en.serde_tag.is_none() && !en.serde_untagged && en.variants.iter().all(|v| v.fields.is_empty()))
        .map(|en| {
            let default_variant = en
                .variants
                .iter()
                .find(|v| v.is_default)
                .map(|v| v.name.clone())
                .unwrap_or_default();
            (en.name.clone(), default_variant)
        })
        .collect();

    // sealed classes because they are tagged (`#[serde(tag = "...")]`) or
    // untagged (`#[serde(untagged)]`).  Data-class fields whose declared
    let sealed_class_names: std::collections::HashSet<String> = api
        .enums
        .iter()
        .filter(|en| en.serde_tag.is_some() || en.serde_untagged)
        // A declaratively polymorphic sealed class has no custom serializer to pin a field's
        // declared type at, so annotating the field would route it through the abstract base.
        .filter(|en| !crate::backends::kotlin::enum_uses_declarative_polymorphism_pub(en))
        .map(|en| en.name.clone())
        .collect();

    // A Rust `Default` impl alone does not make the emitted data class constructible with no
    // arguments — every constructor parameter must also carry a Kotlin default. See
    // `default_constructible_type_names`.
    let default_constructible_types = default_constructible_type_names(&api.types, &enum_defaults);

    // Data-class instance methods are bridged through the JNI shim crate; the
    // shared predicate keeps this list identical to the `external fun`s the
    // Bridge object declares and the `Java_*` symbols the shim exports.
    let serde_type_names = crate::core::jni::value_bridge_serde_type_names(api);
    let bridge_class = bridge_class_name(&config.name);
    let value_method_bridge = ValueMethodBridge {
        bridge_class: &bridge_class,
        serde_type_names: &serde_type_names,
    };

    let mut bridge_file = emit_jni_bridge_object(api, config);
    bridge_file.path = kotlin_source_dir.join(bridge_file.path.file_name().expect("bridge file must have a filename"));
    files.push(bridge_file);

    let exception_class = format!("{bridge_class}Exception");
    let marker = crate::core::hash::SELF_MARKING_HEADER_LINE;
    let exception_content = format!(
        "{marker}\n\n\
         package {package}\n\n\
         class {exception_class}(message: String?, cause: Throwable?) : RuntimeException(message, cause) {{\n\
         {}\n\
         }}\n",
        "    constructor(message: String?) : this(message, null)"
    );
    files.push(GeneratedFile {
        path: kotlin_source_dir.join(format!("{exception_class}.kt")),
        content: exception_content,
        generated_header: false,
    });

    if let Some(mut client_file) = emit_jni_client_class(api, config, Some(&package)) {
        client_file.path = kotlin_source_dir.join("DefaultClient.kt");
        files.push(client_file);
    }

    for ty in &api.types {
        if ty.is_opaque || ty.is_trait || ty.binding_excluded {
            continue;
        }
        if effective_excluded_types.contains(&ty.name) {
            continue;
        }
        let mut imports: BTreeSet<String> = BTreeSet::new();
        let mut body = String::new();
        let needs_field_filter = ty
            .fields
            .iter()
            .any(|f| effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
        if needs_field_filter {
            let mut filtered = ty.clone();
            filtered
                .fields
                .retain(|f| !effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
            // The data class no longer carries every field of the core struct, so the
            // JSON it serialises cannot be trusted to rebuild the core receiver.
            // Methods are dropped rather than bridged against a lossy payload.
            emit_type_pub_with_defaults_sealed_and_constructible(
                &filtered,
                &mut body,
                &mut imports,
                &enum_defaults,
                &sealed_class_names,
                &default_constructible_types,
                None,
            );
        } else {
            emit_type_pub_with_defaults_sealed_and_constructible(
                ty,
                &mut body,
                &mut imports,
                &enum_defaults,
                &sealed_class_names,
                &default_constructible_types,
                Some(value_method_bridge),
            );
        }
        if body.trim().is_empty() {
            continue;
        }
        let content = assemble_kt_content(&package, &imports, &body);
        files.push(GeneratedFile {
            path: kotlin_source_dir.join(format!("{}.kt", ty.name)),
            content,
            generated_header: false,
        });
    }

    let text_types = &config.untagged_union_text_types;
    for en in &api.enums {
        if en.binding_excluded {
            continue;
        }
        let mut body = String::new();
        emit_enum_pub(en, &mut body, &package, text_types);
        if body.trim().is_empty() {
            continue;
        }
        let content = assemble_kt_content(&package, &BTreeSet::new(), &body);
        files.push(GeneratedFile {
            path: kotlin_source_dir.join(format!("{}.kt", en.name)),
            content,
            generated_header: false,
        });
    }

    for error in &api.errors {
        let mut imports: BTreeSet<String> = BTreeSet::new();
        let mut body = String::new();
        emit_error_type_pub(error, &mut body, &mut imports);
        if body.trim().is_empty() {
            continue;
        }
        let content = assemble_kt_content(&package, &imports, &body);
        files.push(GeneratedFile {
            path: kotlin_source_dir.join(format!("{}.kt", error.name)),
            content,
            generated_header: false,
        });
    }

    trait_interfaces::emit_trait_interfaces(api, config, kotlin_source_dir, &package, &mut files);

    module_facade::emit_module_kt(api, config, kotlin_source_dir, &package, &mut files);

    if !config.components.is_empty() {
        files.push(emit_unsupported_components(kotlin_source_dir, &package));
    }

    files
}

fn emit_unsupported_components(kotlin_source_dir: &Path, package: &str) -> GeneratedFile {
    let message = "Downloadable native components are unsupported by this framework on Android because downloaded executable code conflicts with application packaging, store, and platform security policies. Bundle the required feature profile in the AAR instead."
        .to_string();
    let content = format!(
        "// Generated by alef. Do not edit by hand.\n\n\
         package {package}\n\n\
         /** Explicit unsupported surface for Alef downloadable components on Android. */\n\
         object Components {{\n\
             private const val UNSUPPORTED_MESSAGE = \"{message}\"\n\n\
             fun componentLoad(component: String): Nothing = unsupported(component)\n\n\
             fun componentPrefetch(components: List<String>? = null): Nothing = unsupported(components)\n\n\
             fun componentStatus(component: String): Nothing = unsupported(component)\n\n\
             fun componentCachePath(component: String): Nothing = unsupported(component)\n\n\
             private fun unsupported(@Suppress(\"UNUSED_PARAMETER\") value: Any?): Nothing =\n\
                 throw UnsupportedOperationException(UNSUPPORTED_MESSAGE)\n\
         }}\n"
    );
    GeneratedFile {
        path: kotlin_source_dir.join("Components.kt"),
        content,
        generated_header: false,
    }
}

/// Assemble a complete `.kt` file from package, imports, and body.
pub(super) fn assemble_kt_content(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
    let suppressions = vec![
        "ktlint:standard:trailing-comma-on-call-site",
        "ktlint:standard:trailing-comma-on-declaration-site",
        "ktlint:standard:spacing-between-declarations-with-comments",
        "ktlint:standard:spacing-between-declarations-with-annotations",
        "ktlint:standard:when-entry-bracing",
        "ktlint:standard:blank-line-between-when-conditions",
        "ktlint:standard:blank-line-before-declaration",
        "ktlint:standard:chain-method-continuation",
        "ktlint:standard:annotation",
        "ktlint:standard:max-line-length",
        "ktlint:standard:no-semi",
        "ktlint:standard:statement-wrapping",
        "MaxLineLength",
        "TooManyFunctions",
        "FunctionParameterNaming",
        "LongParameterList",
        "CyclomaticComplexMethod",
        "LongMethod",
        "MagicNumber",
        "NestedBlockDepth",
        "ReturnCount",
        "UnusedParameter",
    ];
    let imports = imports.iter().cloned().collect::<Vec<_>>();
    template_env::render(
        "kt_file.jinja",
        crate::alef_context! {
            package => package,
            imports => imports,
            suppressions => suppressions,
            body => body,
        },
    )
}

#[cfg(test)]
mod component_tests {
    use super::*;

    #[test]
    fn android_component_api_fails_explicitly() {
        let generated = emit_unsupported_components(Path::new("packages/android/src"), "com.example.demo");

        assert!(generated.path.ends_with("Components.kt"));
        for operation in [
            "componentLoad",
            "componentPrefetch",
            "componentStatus",
            "componentCachePath",
        ] {
            assert!(generated.content.contains(operation), "missing {operation}");
        }
        assert!(generated.content.contains("UnsupportedOperationException"));
        assert!(generated.content.contains("unsupported by this framework on Android"));
        assert!(
            generated
                .content
                .contains("packaging, store, and platform security policies")
        );
    }
}