#[inline]
pub fn is_ident_start(c: char) -> bool {
c == '_' || unicode_ident::is_xid_start(c)
}
#[inline]
pub fn is_ident_continue(c: char) -> bool {
c == '_' || unicode_ident::is_xid_continue(c)
}
pub fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
None => false,
Some(first) if !is_ident_start(first) => false,
Some(_) => chars.all(is_ident_continue),
}
}
pub fn ident_run_len(s: &str) -> usize {
let mut chars = s.char_indices();
match chars.next() {
Some((_, first)) if is_ident_start(first) => chars
.find(|(_, c)| !is_ident_continue(*c))
.map_or(s.len(), |(i, _)| i),
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_identifiers_are_accepted() {
assert!(is_ident("x"));
assert!(is_ident("_"));
assert!(is_ident("_x9"));
assert!(is_ident("snake_case_9"));
}
#[test]
fn a_digit_may_continue_but_not_start() {
assert!(is_ident_continue('9'));
assert!(!is_ident_start('9'));
assert!(is_ident("x9"));
assert!(!is_ident("9x"));
}
#[test]
fn unicode_scalars_may_start_and_continue_an_identifier() {
assert!(is_ident("λ"));
assert!(is_ident("Ünicode"));
assert!(is_ident("δx"));
assert!(is_ident("日本語"));
}
#[test]
fn non_letter_scalars_are_not_identifiers() {
assert!(!is_ident("+"));
assert!(!is_ident("→"));
assert!(!is_ident("🦀"));
assert!(!is_ident(""));
assert!(!is_ident("a b"));
}
#[test]
fn a_run_ends_at_the_first_character_outside_the_class() {
assert_eq!(ident_run_len("x9 rest"), 2);
assert_eq!(ident_run_len("snake_case_9("), 12);
assert_eq!(ident_run_len("x"), 1);
}
#[test]
fn a_run_is_measured_in_bytes_over_whole_scalars() {
assert_eq!(ident_run_len("δx+"), 3);
assert_eq!(ident_run_len("日本語"), 9);
assert_eq!(ident_run_len("λ→"), 2);
}
#[test]
fn a_run_that_does_not_start_one_is_zero() {
assert_eq!(ident_run_len(""), 0);
assert_eq!(ident_run_len("9x"), 0);
assert_eq!(ident_run_len("+x"), 0);
assert_eq!(ident_run_len("\u{0301}x"), 0);
}
#[test]
fn a_full_length_run_agrees_with_is_ident() {
for s in ["x", "_x9", "Ünicode", "9x", "", "a b", "🦀"] {
assert_eq!(is_ident(s), !s.is_empty() && ident_run_len(s) == s.len());
}
}
#[test]
fn a_combining_mark_may_continue_but_not_start() {
assert!(is_ident_continue('\u{0301}'));
assert!(!is_ident_start('\u{0301}'));
assert!(is_ident("e\u{0301}"));
assert!(!is_ident("\u{0301}"));
}
}