use unicode_general_category::{GeneralCategory, get_general_category};
pub const IDENTIFIER_UNICODE_VERSION: &str = "16.0.0";
fn is_letter_or_letter_number(character: char) -> bool {
matches!(
get_general_category(character),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
| GeneralCategory::LetterNumber
)
}
fn is_java_identifier_ignorable_control(character: char) -> bool {
matches!(character, '\u{0}'..='\u{8}' | '\u{e}'..='\u{1b}' | '\u{7f}'..='\u{9f}')
}
pub fn is_java_identifier_start(character: char) -> bool {
is_letter_or_letter_number(character)
|| matches!(
get_general_category(character),
GeneralCategory::CurrencySymbol | GeneralCategory::ConnectorPunctuation
)
}
pub fn is_java_identifier_part(character: char) -> bool {
is_java_identifier_start(character)
|| matches!(
get_general_category(character),
GeneralCategory::DecimalNumber
| GeneralCategory::NonspacingMark
| GeneralCategory::SpacingMark
| GeneralCategory::Format
)
|| is_java_identifier_ignorable_control(character)
}
fn is_csharp_lexable(character: char) -> bool {
character.len_utf16() == 1
}
pub fn is_csharp_identifier_start(character: char) -> bool {
is_csharp_lexable(character) && (is_letter_or_letter_number(character) || character == '_')
}
pub fn is_csharp_identifier_part(character: char) -> bool {
is_csharp_lexable(character)
&& (is_letter_or_letter_number(character)
|| matches!(
get_general_category(character),
GeneralCategory::DecimalNumber
| GeneralCategory::ConnectorPunctuation
| GeneralCategory::NonspacingMark
| GeneralCategory::SpacingMark
| GeneralCategory::Format
))
}
fn is_letter_excluding_letter_number(character: char) -> bool {
matches!(
get_general_category(character),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
)
}
pub fn is_kotlin_identifier_start(character: char) -> bool {
is_letter_excluding_letter_number(character) || character == '_'
}
pub fn is_kotlin_identifier_part(character: char) -> bool {
is_kotlin_identifier_start(character) || get_general_category(character) == GeneralCategory::DecimalNumber
}
const SWIFT_IDENTIFIER_HEAD_RANGES: &[(u32, u32)] = &[
(0x00A8, 0x00A8),
(0x00AA, 0x00AA),
(0x00AD, 0x00AD),
(0x00AF, 0x00AF),
(0x00B2, 0x00B5),
(0x00B7, 0x00BA),
(0x00BC, 0x00BE),
(0x00C0, 0x00D6),
(0x00D8, 0x00F6),
(0x00F8, 0x00FF),
(0x0100, 0x02FF),
(0x0370, 0x167F),
(0x1681, 0x180D),
(0x180F, 0x1DBF),
(0x1E00, 0x1FFF),
(0x200B, 0x200D),
(0x202A, 0x202E),
(0x203F, 0x2040),
(0x2054, 0x2054),
(0x2060, 0x206F),
(0x2070, 0x20CF),
(0x2100, 0x218F),
(0x2460, 0x24FF),
(0x2776, 0x2793),
(0x2C00, 0x2DFF),
(0x2E80, 0x2FFF),
(0x3004, 0x3007),
(0x3021, 0x302F),
(0x3031, 0x303F),
(0x3040, 0xD7FF),
(0xF900, 0xFD3D),
(0xFD40, 0xFDCF),
(0xFDF0, 0xFE1F),
(0xFE30, 0xFE44),
(0xFE47, 0xFFFD),
(0x10000, 0x1FFFD),
(0x20000, 0x2FFFD),
(0x30000, 0x3FFFD),
(0x40000, 0x4FFFD),
(0x50000, 0x5FFFD),
(0x60000, 0x6FFFD),
(0x70000, 0x7FFFD),
(0x80000, 0x8FFFD),
(0x90000, 0x9FFFD),
(0xA0000, 0xAFFFD),
(0xB0000, 0xBFFFD),
(0xC0000, 0xCFFFD),
(0xD0000, 0xDFFFD),
(0xE0000, 0xEFFFD),
];
const SWIFT_IDENTIFIER_CONTINUATION_ONLY_RANGES: &[(u32, u32)] =
&[(0x0300, 0x036F), (0x1DC0, 0x1DFF), (0x20D0, 0x20FF), (0xFE20, 0xFE2F)];
fn in_ranges(character: char, ranges: &[(u32, u32)]) -> bool {
let scalar = character as u32;
ranges.iter().any(|&(start, end)| scalar >= start && scalar <= end)
}
pub fn is_swift_identifier_start(character: char) -> bool {
character.is_ascii_alphabetic() || character == '_' || in_ranges(character, SWIFT_IDENTIFIER_HEAD_RANGES)
}
pub fn is_swift_identifier_part(character: char) -> bool {
is_swift_identifier_start(character)
|| character.is_ascii_digit()
|| in_ranges(character, SWIFT_IDENTIFIER_CONTINUATION_ONLY_RANGES)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_alphabetic_disagrees_with_both_grammars_on_starts() {
for character in ['$', '€', '\u{20a3}', '\u{203f}'] {
let label = format!("U+{:04X}", character as u32);
assert!(!character.is_alphabetic(), "{label} is not Rust-alphabetic");
assert!(is_java_identifier_start(character), "{label} starts a Java identifier");
}
for character in ['$', '€', '\u{20a3}', '\u{203f}'] {
assert!(!is_csharp_identifier_start(character), "U+{:04X}", character as u32);
}
}
#[test]
fn rust_alphanumeric_disagrees_with_both_grammars_on_continuations() {
for character in ['\u{301}', '\u{200c}', '\u{203f}'] {
let label = format!("U+{:04X}", character as u32);
assert!(!character.is_alphanumeric(), "{label}");
assert!(is_java_identifier_part(character), "java {label}");
assert!(is_csharp_identifier_part(character), "c# {label}");
}
for character in ['\u{b2}', '\u{2160}'] {
assert!(character.is_alphanumeric(), "U+{:04X}", character as u32);
}
assert!(!is_java_identifier_part('\u{b2}'));
assert!(!is_csharp_identifier_part('\u{b2}'));
}
#[test]
fn csharp_is_bmp_only_because_roslyn_lexes_utf16_code_units() {
assert_eq!('\u{10400}'.len_utf16(), 2);
assert!(is_java_identifier_start('\u{10400}'));
assert!(is_java_identifier_part('\u{10400}'));
assert!(!is_csharp_identifier_start('\u{10400}'));
assert!(!is_csharp_identifier_part('\u{10400}'));
assert_eq!('A'.len_utf16(), 1);
assert!(is_csharp_identifier_start('A'));
}
#[test]
fn only_the_literal_underscore_starts_a_csharp_identifier() {
assert!(is_csharp_identifier_start('_'));
assert!(!is_csharp_identifier_start('\u{203f}'));
assert!(is_csharp_identifier_part('\u{203f}'));
assert!(is_java_identifier_start('\u{203f}'));
}
#[test]
fn java_admits_identifier_ignorable_controls_that_coordinates_reject_by_policy() {
for character in ['\u{0}', '\u{8}', '\u{e}', '\u{1b}', '\u{7f}', '\u{9f}'] {
assert!(is_java_identifier_part(character), "U+{:04X}", character as u32);
assert!(character.is_control(), "U+{:04X}", character as u32);
}
for character in ['\u{9}', '\u{a}', '\u{d}', '\u{1c}'] {
assert!(
!is_java_identifier_part(character),
"whitespace U+{:04X}",
character as u32
);
}
}
#[test]
fn separator_categories_are_not_identifier_characters() {
for character in [' ', '\u{a0}', '\u{2002}', '\u{3000}', '\u{2028}', '\u{2029}'] {
let label = format!("U+{:04X}", character as u32);
assert!(!is_java_identifier_part(character), "java {label}");
assert!(!is_csharp_identifier_part(character), "c# {label}");
}
}
#[test]
fn unicode_version_is_pinned_and_stated() {
assert_eq!(IDENTIFIER_UNICODE_VERSION, "16.0.0");
}
#[test]
fn kotlin_rejects_letter_number_unlike_java_and_csharp() {
let roman_numeral_twelve = '\u{216b}';
assert!(is_java_identifier_start(roman_numeral_twelve));
assert!(is_csharp_identifier_start(roman_numeral_twelve));
assert!(!is_kotlin_identifier_start(roman_numeral_twelve));
assert!(!is_kotlin_identifier_part(roman_numeral_twelve));
}
#[test]
fn kotlin_rejects_combining_marks_and_format_characters_as_continuations() {
for character in ['\u{301}', '\u{93e}', '\u{200c}'] {
let label = format!("U+{:04X}", character as u32);
assert!(is_java_identifier_part(character), "java {label}");
assert!(is_csharp_identifier_part(character), "c# {label}");
assert!(!is_kotlin_identifier_part(character), "kotlin {label}");
}
}
#[test]
fn kotlin_accepts_supplementary_plane_letters_unlike_csharp() {
let deseret_capital_long_i = '\u{10400}';
assert!(!is_csharp_identifier_start(deseret_capital_long_i));
assert!(is_kotlin_identifier_start(deseret_capital_long_i));
assert!(is_kotlin_identifier_part(deseret_capital_long_i));
}
#[test]
fn kotlin_accepts_a_digit_as_continuation_but_not_as_start() {
assert!(!is_kotlin_identifier_start('0'));
assert!(is_kotlin_identifier_part('0'));
}
#[test]
fn kotlin_accepts_the_literal_underscore_as_start_and_part() {
assert!(is_kotlin_identifier_start('_'));
assert!(is_kotlin_identifier_part('_'));
}
#[test]
fn swift_accepts_emoji_that_rust_alphabetic_rejects() {
for character in ['\u{1F600}', '\u{1F389}'] {
let label = format!("U+{:04X}", character as u32);
assert!(!character.is_alphabetic(), "{label} is not Rust-alphabetic");
assert!(is_swift_identifier_start(character), "swift {label} start");
assert!(is_swift_identifier_part(character), "swift {label} part");
}
}
#[test]
fn swift_rejects_symbols_outside_every_identifier_head_range() {
for character in ['\u{2603}', '\u{2049}'] {
let label = format!("U+{:04X}", character as u32);
assert!(!is_swift_identifier_start(character), "swift {label} start");
assert!(!is_swift_identifier_part(character), "swift {label} part");
}
}
#[test]
fn swift_accepts_documented_head_range_boundaries() {
assert!(is_swift_identifier_start('\u{00A8}'));
assert!(is_swift_identifier_start('\u{2103}'));
}
#[test]
fn swift_rejects_a_leading_digit_but_accepts_a_continuation_digit() {
assert!(!is_swift_identifier_start('0'));
assert!(is_swift_identifier_part('0'));
}
#[test]
fn swift_accepts_the_literal_underscore_as_start_and_part() {
assert!(is_swift_identifier_start('_'));
assert!(is_swift_identifier_part('_'));
}
#[test]
fn swift_continuation_only_ranges_are_rejected_as_a_start() {
assert!(!is_swift_identifier_start('\u{0301}'));
assert!(is_swift_identifier_part('\u{0301}'));
}
}