use crate::codegen::naming::to_java_name;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::{DefaultValue, PrimitiveType, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::HashSet;
const SERDE_DEFAULT_PLACEHOLDER: &str = "/* serde(default) */";
pub(crate) fn is_serde_default_marker(default: Option<&str>) -> bool {
matches!(default, Some(s) if s == SERDE_DEFAULT_PLACEHOLDER || s.starts_with("serde(default = \""))
}
pub(crate) fn java_literal_default(ty: &TypeRef, typed_default: Option<&DefaultValue>) -> Option<String> {
match typed_default? {
DefaultValue::BoolLiteral(true) => Some("true".to_string()),
DefaultValue::IntLiteral(n) if *n != 0 => Some(match ty {
TypeRef::Duration
| TypeRef::Primitive(
PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize,
) => format!("{n}L"),
_ => n.to_string(),
}),
DefaultValue::FloatLiteral(f) if *f != 0.0 => {
let digits = crate::codegen::shared::float_literal_digits(*f)?;
Some(match ty {
TypeRef::Primitive(PrimitiveType::F32) => format!("{digits}f"),
_ => digits,
})
}
_ => None,
}
}
pub(crate) fn boxes_to_carry_literal_default(ty: &TypeRef, typed_default: Option<&DefaultValue>) -> bool {
java_literal_default(ty, typed_default).is_some()
}
pub(crate) fn serde_default_collection_literal(
ty: &TypeRef,
has_serde_default: bool,
serde_skip_serializing_if: bool,
) -> Option<&'static str> {
if !has_serde_default || !serde_skip_serializing_if {
return None;
}
match ty {
TypeRef::Vec(_) => Some("List.of()"),
TypeRef::Map(_, _) => Some("Map.of()"),
_ => None,
}
}
const JAVA_OBJECT_METHOD_NAMES: &[&str] = &[
"wait",
"notify",
"notifyAll",
"getClass",
"hashCode",
"equals",
"toString",
"clone",
"finalize",
];
#[allow(dead_code)]
pub(crate) const JAVA_LANG_AUTO_IMPORTED: &[&str] = &[
"Iterable",
"Object",
"String",
"Throwable",
"Number",
"Boolean",
"Integer",
"Long",
"Double",
"Float",
"Character",
"Byte",
"Short",
"Math",
"Runnable",
"Thread",
"Exception",
"RuntimeException",
"Error",
"Class",
"Comparable",
];
pub(crate) fn escape_javadoc_line(s: &str) -> String {
let sanitized = sanitize_rust_syntax(s);
let mut result = String::with_capacity(sanitized.len());
let mut chars = sanitized.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 ");
let mut code_iter = code.chars().peekable();
while let Some(code_ch) = code_iter.next() {
match code_ch {
'\\' if code_iter.peek() == Some(&'u') => result.push_str("\"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'&' => result.push_str("&"),
'*' if code_iter.peek() == Some(&'/') => {
code_iter.next();
result.push_str("*/");
}
other => result.push(other),
}
}
result.push('}');
} else if ch == '\\' && chars.peek() == Some(&'u') {
result.push_str("\");
} else if ch == '<' {
result.push_str("<");
} else if ch == '>' {
result.push_str(">");
} else if ch == '&' {
result.push_str("&");
} else if ch == '*' && chars.peek() == Some(&'/') {
chars.next();
result.push_str("* /");
} else if ch == '@' {
result.push_str("{@literal @}");
} else {
result.push(ch);
}
}
result
}
fn sanitize_rust_syntax(s: &str) -> String {
crate::codegen::doc_emission::sanitize_rust_idioms(s, crate::codegen::doc_emission::DocTarget::JavaDoc)
}
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())
}
pub(crate) fn render_nullable_type(ftype: &str, is_nullable: bool) -> String {
if !is_nullable {
return ftype.to_string();
}
if let Some(idx) = ftype.rfind('.') {
let (pkg, simple) = ftype.split_at(idx);
let simple = simple.trim_start_matches('.');
format!("{pkg}.@Nullable {simple}")
} else {
format!("@Nullable {ftype}")
}
}
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 safe_java_method_name(name: &str) -> String {
let camel = name.to_lower_camel_case();
match camel.as_str() {
"default" => "defaultInstance".to_string(),
"new" => "create".to_string(),
other if crate::core::keywords::JAVA_KEYWORDS.contains(&other) => format!("{other}_"),
_ => camel,
}
}
pub(crate) fn is_bridge_param_java(
param: &crate::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 ¶m.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))
}
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::backends::java::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::backends::java::template_env::render(
"exception_class.jinja",
minijinja::context! {
header => header,
package => package,
class_name => class_name,
},
)
}
fn transform_rustdoc_for_java(doc: &str, throws_class: &str) -> String {
let sections = crate::codegen::doc_emission::parse_rustdoc_sections(doc);
let rendered = crate::codegen::doc_emission::render_javadoc_sections(§ions, throws_class);
if rendered.trim().is_empty() {
let sanitized = sanitize_rust_syntax(doc);
return sanitized.replace("[`", "").replace("`]", "").trim().to_string();
}
rendered.replace("[`", "").replace("`]", "")
}
pub(crate) fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
emit_javadoc_with_throws(out, doc, indent, "Exception");
}
pub(crate) fn emit_javadoc_with_throws(out: &mut String, doc: &str, indent: &str, throws_class: &str) {
if doc.is_empty() {
return;
}
let transformed = transform_rustdoc_for_java(doc, throws_class);
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::backends::java::template_env::render(
"javadoc_lines.jinja",
minijinja::context! {
indent => indent,
lines => lines,
},
));
out.push_str(indent);
out.push_str(" */\n");
}
pub(crate) const RECORD_LINE_WRAP_THRESHOLD: usize = 100;
pub(crate) fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
crate::codegen::naming::apply_serde_rename_all(name, rename_all)
}
pub(crate) fn format_optional_value(ty: &TypeRef, default: &str) -> String {
if default.contains("Optional.") {
return default.to_string();
}
let inner_ty = match ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
let formatted_value = match inner_ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
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 => {
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 => default.to_string(),
_ => default.to_string(),
},
_ => default.to_string(),
};
format!("Optional.of({})", formatted_value)
}
pub(crate) fn qualify_shadowed_type(java_type: &str, package: &str, variant_names: &HashSet<&str>) -> String {
let mut result = String::with_capacity(java_type.len());
let mut chars = java_type.char_indices().peekable();
let mut prev_was_dot = false;
while let Some((start, ch)) = chars.next() {
if ch.is_ascii_alphabetic() || ch == '_' {
let mut end = start + ch.len_utf8();
while let Some(&(_, next_ch)) = chars.peek() {
if !(next_ch.is_ascii_alphanumeric() || next_ch == '_') {
break;
}
end += next_ch.len_utf8();
chars.next();
}
let ident = &java_type[start..end];
if !prev_was_dot && variant_names.contains(ident) {
result.push_str(package);
result.push('.');
}
result.push_str(ident);
prev_was_dot = false;
} else {
result.push(ch);
prev_was_dot = ch == '.';
}
}
result
}
pub(crate) fn gen_json_util_class(package: &str, main_class: &str) -> String {
let header = crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash);
crate::backends::java::template_env::render(
"json_util.jinja",
minijinja::context! {
header => header,
package => package,
main_class => main_class,
},
)
}
#[cfg(test)]
mod tests {
use super::{escape_javadoc_line, qualify_shadowed_type};
use std::collections::HashSet;
#[test]
fn escapes_unicode_sequences_before_java_lexing() {
let escaped = escape_javadoc_line(r"Rust chars use `\u{1F600}` or \u{00A0}");
assert!(!escaped.contains(r"\u"));
assert!(escaped.contains("\u{1F600}"));
assert!(escaped.contains("\u{00A0}"));
}
#[test]
fn qualify_shadowed_type_qualifies_a_bare_name_matching_a_variant() {
let variants: HashSet<&str> = ["ImageUrl", "Text"].into_iter().collect();
assert_eq!(
qualify_shadowed_type("ImageUrl", "io.xberg.literllm", &variants),
"io.xberg.literllm.ImageUrl"
);
}
#[test]
fn qualify_shadowed_type_leaves_non_colliding_names_untouched() {
let variants: HashSet<&str> = ["ImageUrl", "Text"].into_iter().collect();
assert_eq!(
qualify_shadowed_type("DocumentContent", "io.xberg.literllm", &variants),
"DocumentContent"
);
}
#[test]
fn qualify_shadowed_type_qualifies_inside_generics() {
let variants: HashSet<&str> = ["ImageUrl"].into_iter().collect();
assert_eq!(
qualify_shadowed_type("List<ImageUrl>", "io.xberg.literllm", &variants),
"List<io.xberg.literllm.ImageUrl>"
);
}
#[test]
fn qualify_shadowed_type_does_not_double_qualify_already_qualified_names() {
let variants: HashSet<&str> = ["ImageUrl"].into_iter().collect();
assert_eq!(
qualify_shadowed_type("io.xberg.literllm.ImageUrl", "io.xberg.literllm", &variants),
"io.xberg.literllm.ImageUrl"
);
}
#[test]
fn qualify_shadowed_type_ignores_names_with_no_collision() {
let variants: HashSet<&str> = HashSet::new();
assert_eq!(
qualify_shadowed_type("ImageUrl", "io.xberg.literllm", &variants),
"ImageUrl"
);
}
}