pub const KOTLIN_HARD_KEYWORDS: &[&str] = &[
"as",
"break",
"class",
"continue",
"do",
"else",
"false",
"for",
"fun",
"if",
"in",
"interface",
"is",
"null",
"object",
"package",
"return",
"super",
"this",
"throw",
"true",
"try",
"typealias",
"typeof",
"val",
"var",
"when",
"while",
];
pub fn is_kotlin_hard_keyword(s: &str) -> bool {
KOTLIN_HARD_KEYWORDS.contains(&s)
}
pub fn is_valid_kotlin_ident(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first.is_alphabetic()) {
return false;
}
if !chars.all(|c| c == '_' || c.is_alphanumeric()) {
return false;
}
!is_kotlin_hard_keyword(s)
}
pub fn mangle_kotlin_ident(s: &str) -> String {
if is_valid_kotlin_ident(s) {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + 1);
for (i, c) in s.chars().enumerate() {
if c == '_' || c.is_alphanumeric() {
if i == 0 && c.is_numeric() {
out.push('_');
}
out.push(c);
} else {
out.push('_');
}
}
if out.is_empty() {
out.push('_');
}
if is_kotlin_hard_keyword(&out) {
out.push('_');
}
out
}
pub fn escape_kotlin_ident(s: &str) -> String {
if is_valid_kotlin_ident(s) {
return s.to_string();
}
let cleaned: String = s
.chars()
.map(|c| {
if matches!(
c,
'`' | '\n' | '\r' | '.' | ';' | '[' | ']' | '/' | '<' | '>' | ':' | '\\'
) {
'_'
} else {
c
}
})
.collect();
if cleaned.is_empty() {
return "`_`".to_string();
}
format!("`{cleaned}`")
}
pub fn is_escaped_kotlin_ident(s: &str) -> bool {
let Some(inner) = s.strip_prefix('`').and_then(|r| r.strip_suffix('`')) else {
return false;
};
!inner.is_empty()
&& !inner.contains(|c| {
matches!(
c,
'`' | '\n' | '\r' | '.' | ';' | '[' | ']' | '/' | '<' | '>' | ':' | '\\'
)
})
}
pub fn is_writable_kotlin_ident(s: &str) -> bool {
is_valid_kotlin_ident(s) || is_escaped_kotlin_ident(s)
}
pub fn is_valid_kotlin_package(s: &str) -> bool {
s.is_empty() || s.split('.').all(is_valid_kotlin_ident)
}
pub fn mangle_kotlin_package(path: &str) -> String {
path.split('.')
.filter(|s| !s.is_empty())
.map(mangle_kotlin_ident)
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hard_keywords_are_sorted_and_unique() {
let mut sorted = KOTLIN_HARD_KEYWORDS.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.as_slice(), KOTLIN_HARD_KEYWORDS);
}
#[test]
fn soft_keywords_are_valid_identifiers() {
for s in ["data", "value", "sealed", "inline", "operator", "companion"] {
assert!(is_valid_kotlin_ident(s), "{s} should be a valid identifier");
assert_eq!(mangle_kotlin_ident(s), s);
}
}
#[test]
fn mangling_always_produces_a_valid_identifier() {
for s in [
"", "2fast", "my-name", "a b", "object", "val", "___", "é", "9", "...", "a.b.c",
] {
let m = mangle_kotlin_ident(s);
assert!(is_valid_kotlin_ident(&m), "{s:?} mangled to invalid {m:?}");
assert_eq!(mangle_kotlin_ident(&m), m, "not idempotent for {s:?}");
}
}
#[test]
fn unicode_letters_are_valid() {
assert!(is_valid_kotlin_ident("café"));
assert!(is_valid_kotlin_ident("Привет"));
assert_eq!(mangle_kotlin_ident("café"), "café");
}
#[test]
fn package_mangling_always_produces_a_valid_package() {
for s in ["", "fun.my-pkg", "io..jni.", "1.2.3", "..."] {
let m = mangle_kotlin_package(s);
assert!(
is_valid_kotlin_package(&m),
"{s:?} mangled to invalid {m:?}"
);
assert_eq!(mangle_kotlin_package(&m), m, "not idempotent for {s:?}");
}
}
#[test]
fn escaping_leaves_legal_names_alone() {
for s in ["myValue", "_x", "café"] {
assert_eq!(escape_kotlin_ident(s), s);
}
}
#[test]
fn escaping_strips_characters_illegal_even_in_backticks() {
assert_eq!(escape_kotlin_ident("a`b"), "`a_b`");
assert_eq!(escape_kotlin_ident("a\nb"), "`a_b`");
assert_eq!(escape_kotlin_ident("a/b"), "`a_b`");
assert_eq!(escape_kotlin_ident(""), "`_`");
}
}