alef-backend-java 0.16.65

Java (Panama FFM) backend for alef
Documentation
use alef_codegen::naming::to_java_name;
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::{PrimitiveType, TypeRef};
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase};
use std::collections::HashSet;

/// Names that conflict with methods on `java.lang.Object` and are therefore
/// illegal as record component names or method names in generated Java code.
const JAVA_OBJECT_METHOD_NAMES: &[&str] = &[
    "wait",
    "notify",
    "notifyAll",
    "getClass",
    "hashCode",
    "equals",
    "toString",
    "clone",
    "finalize",
];

/// Returns true if `name` is a tuple/unnamed field index such as `"0"`, `"1"`, `"_0"`, `"_1"`.
/// Serde represents tuple and newtype variant fields with these numeric names. They are not
/// real JSON keys and must not be used as Java identifiers.
/// Escape a string for use inside a Javadoc comment.
/// Replaces `*/` (which would close the comment) and `@` (which starts a tag).
///
/// HTML entities (`<`, `>`, `&`) are also escaped *inside* `{@code …}` blocks.
/// Leaving them raw lets Eclipse-formatter Spotless interpret content like
/// `<pre>` as a block-level HTML element and shatter the line across
/// multiple `* ` rows, which then breaks `alef-verify`'s embedded hash.
pub(crate) fn escape_javadoc_line(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '`' {
            let mut code = String::new();
            for c in chars.by_ref() {
                if c == '`' {
                    break;
                }
                code.push(c);
            }
            result.push_str("{@code ");
            for code_ch in code.chars() {
                match code_ch {
                    '<' => result.push_str("&lt;"),
                    '>' => result.push_str("&gt;"),
                    '&' => result.push_str("&amp;"),
                    other => result.push(other),
                }
            }
            result.push('}');
        } else if ch == '<' {
            result.push_str("&lt;");
        } else if ch == '>' {
            result.push_str("&gt;");
        } else if ch == '&' {
            result.push_str("&amp;");
        } else if ch == '*' && chars.peek() == Some(&'/') {
            chars.next();
            result.push_str("* /");
        } else if ch == '@' {
            result.push_str("{@literal @}");
        } else {
            result.push(ch);
        }
    }
    result
}

pub(crate) fn is_tuple_field_name(name: &str) -> bool {
    let stripped = name.trim_start_matches('_');
    !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit())
}

/// Sanitise a field/parameter name that would conflict with `java.lang.Object`
/// methods.  Conflicting names get a `_` suffix (e.g. `wait` -> `wait_`), which
/// is then converted to camelCase by `to_java_name`.
pub(crate) fn safe_java_field_name(name: &str) -> String {
    let java_name = to_java_name(name);
    if JAVA_OBJECT_METHOD_NAMES.contains(&java_name.as_str()) {
        format!("{}Value", java_name)
    } else {
        java_name
    }
}

pub(crate) fn is_bridge_param_java(
    param: &alef_core::ir::ParamDef,
    bridge_param_names: &HashSet<String>,
    bridge_type_aliases: &HashSet<String>,
) -> bool {
    if bridge_param_names.contains(param.name.as_str()) {
        return true;
    }
    let type_name = match &param.ty {
        TypeRef::Named(n) => Some(n.as_str()),
        TypeRef::Optional(inner) => {
            if let TypeRef::Named(n) = inner.as_ref() {
                Some(n.as_str())
            } else {
                None
            }
        }
        _ => None,
    };
    type_name.is_some_and(|n| bridge_type_aliases.contains(n))
}

/// Generate a named infrastructure exception class that extends `{main_class}Exception`.
///
/// Used for the two fixed FFI infrastructure error codes that are always dispatched
/// from `checkLastError()`:
/// - code 1 → `InvalidInputException` (null pointer / invalid UTF-8 in input args)
/// - code 2 → `ConversionErrorException` (JSON serialisation/deserialisation failure)
pub(crate) fn gen_infrastructure_exception_class(
    package: &str,
    main_class: &str,
    class_name: &str,
    code: i32,
    doc: &str,
) -> String {
    let header = hash::header(CommentStyle::DoubleSlash);
    crate::template_env::render(
        "infrastructure_exception.jinja",
        minijinja::context! {
            header => header,
            package => package,
            class_name => class_name,
            main_class => main_class,
            code => code,
            doc => doc,
        },
    )
}

pub(crate) fn gen_exception_class(package: &str, class_name: &str) -> String {
    let header = hash::header(CommentStyle::DoubleSlash);
    crate::template_env::render(
        "exception_class.jinja",
        minijinja::context! {
            header => header,
            package => package,
            class_name => class_name,
        },
    )
}

// ---------------------------------------------------------------------------
// High-level facade class (public API)
// ---------------------------------------------------------------------------

/// Transform Rust intra-doc rustdoc into JavaDoc-compatible prose with
/// JavaDoc tags (`@param`, `@return`, `@throws`).
///
/// Uses the shared section parser from `alef_codegen::doc_emission` so the
/// behaviour is identical across all bindings that translate rustdoc into
/// host-native tag conventions.
///
/// `# Example` blocks are dropped here — they are handled separately by
/// `emit_javadoc`, which would need to wrap code in `<pre>{@code ...}</pre>`.
/// The current Java emitter does not yet emit examples; doing so safely
/// requires a JavaDoc-specific HTML escape that's not done here.
fn transform_rustdoc_for_java(doc: &str) -> String {
    let sections = alef_codegen::doc_emission::parse_rustdoc_sections(doc);
    let rendered = alef_codegen::doc_emission::render_javadoc_sections(&sections, "KreuzbergRsException");
    if rendered.trim().is_empty() {
        // Fallback: when no recognised sections present, emit the raw doc with
        // intra-doc-link cleanup — preserves backward compatibility for prose
        // that has no Markdown headings.
        return doc.replace("[`", "").replace("`]", "").trim().to_string();
    }
    rendered.replace("[`", "").replace("`]", "")
}

pub(crate) fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
    if doc.is_empty() {
        return;
    }
    let transformed = transform_rustdoc_for_java(doc);
    if transformed.is_empty() {
        return;
    }
    out.push_str(indent);
    out.push_str("/**\n");
    let lines: Vec<String> = transformed
        .lines()
        .map(|line| escape_javadoc_line(line).trim_end().to_string())
        .collect();
    out.push_str(&crate::template_env::render(
        "javadoc_lines.jinja",
        minijinja::context! {
            indent => indent,
            lines => lines,
        },
    ));
    out.push_str(indent);
    out.push_str(" */\n");
}

/// Maximum line length before splitting record fields across multiple lines.
/// Checkstyle enforces 120 chars; we split at 100 to leave headroom for indentation.
pub(crate) const RECORD_LINE_WRAP_THRESHOLD: usize = 100;

pub(crate) fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
    match rename_all {
        Some("snake_case") => alef_codegen::naming::pascal_to_snake(name),
        Some("camelCase") => name.to_lower_camel_case(),
        Some("PascalCase") => name.to_pascal_case(),
        Some("SCREAMING_SNAKE_CASE") => alef_codegen::naming::pascal_to_screaming_snake(name),
        Some("kebab-case") => name.to_kebab_case(),
        Some("SCREAMING-KEBAB-CASE") => name.to_kebab_case().to_uppercase(),
        Some("lowercase") => name.to_lowercase(),
        Some("UPPERCASE") => name.to_uppercase(),
        _ => name.to_lowercase(),
    }
}

pub(crate) fn format_optional_value(ty: &TypeRef, default: &str) -> String {
    // Check if the default is already wrapped (e.g., "Optional.of(...)" or "Optional.empty()")
    if default.contains("Optional.") {
        return default.to_string();
    }

    // Unwrap Optional types to get the inner type
    let inner_ty = match ty {
        TypeRef::Optional(inner) => inner.as_ref(),
        other => other,
    };

    // Determine the proper literal suffix based on type
    let formatted_value = match inner_ty {
        TypeRef::Primitive(p) => match p {
            PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
                // Add 'L' suffix for long values if not already present
                if default.ends_with('L') || default.ends_with('l') {
                    default.to_string()
                } else if default.parse::<i64>().is_ok() {
                    format!("{}L", default)
                } else {
                    default.to_string()
                }
            }
            PrimitiveType::F32 => {
                // Add 'f' suffix for float values if not already present
                if default.ends_with('f') || default.ends_with('F') {
                    default.to_string()
                } else if default.parse::<f32>().is_ok() {
                    format!("{}f", default)
                } else {
                    default.to_string()
                }
            }
            PrimitiveType::F64 => {
                // Double defaults can have optional 'd' suffix, but 0.0 is fine
                default.to_string()
            }
            _ => default.to_string(),
        },
        _ => default.to_string(),
    };

    format!("Optional.of({})", formatted_value)
}