knave-expand 0.13.2

The reading, scoping and expansion stages of knave
Documentation
pub struct NameMangler;

impl NameMangler {
    const PUNCTUATION: [(char, &'static str); 23] = [
        ('-', "_"),
        ('+', "plus"),
        ('*', "star"),
        ('/', "slash"),
        ('<', "lt"),
        ('>', "gt"),
        ('=', "eq"),
        ('!', "bang"),
        ('?', "query"),
        ('%', "percent"),
        ('&', "and"),
        ('|', "or"),
        ('^', "caret"),
        ('~', "tilde"),
        ('.', "dot"),
        (':', "colon"),
        ('@', "at"),
        ('$', "dollar"),
        ('#', "hash"),
        ('\'', "prime"),
        (',', "comma"),
        ('\\', "backslash"),
        ('`', "backtick"),
    ];
    const LONE_SEPARATORS: [(char, &'static str); 2] = [('-', "minus"), ('_', "underscore")];
    const CODE_POINT_PREFIX: char = 'u';
    const WILDCARD: &str = "_";

    const PREDICATE_SUFFIX: char = '?';
    const CHANGE_SUFFIX: char = '!';
    const PREDICATE_PREFIX: &str = "is_";
    const ARROW_START: char = '-';
    const ARROW_END: char = '>';
    const ARROW_WORD: &str = "to";
    const STRICT_KEYWORDS: [&str; 35] = [
        "as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern",
        "false", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "match", "mod", "move",
        "mut", "pub", "ref", "return", "static", "struct", "trait", "true", "type", "unsafe",
        "use", "where", "while",
    ];
    const RESERVED_KEYWORDS: [&str; 13] = [
        "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof",
        "unsized", "virtual", "yield",
    ];
    const NON_RAW_KEYWORDS: [&str; 4] = ["self", "Self", "super", "crate"];
    const PRELUDE_NAMES: [&str; 34] = [
        "AsMut",
        "AsRef",
        "Box",
        "Clone",
        "Copy",
        "Default",
        "DoubleEndedIterator",
        "Drop",
        "Eq",
        "ExactSizeIterator",
        "Extend",
        "Fn",
        "FnMut",
        "FnOnce",
        "From",
        "FromIterator",
        "Into",
        "IntoIterator",
        "Iterator",
        "Option",
        "Ord",
        "PartialEq",
        "PartialOrd",
        "Result",
        "Send",
        "Sized",
        "String",
        "Sync",
        "ToOwned",
        "ToString",
        "TryFrom",
        "TryInto",
        "Unpin",
        "Vec",
    ];
    const PRELUDE_SUFFIX: char = '2';

    pub fn local(name: &str) -> String {
        if name == Self::WILDCARD {
            name.to_string()
        } else {
            Self::value(name)
        }
    }

    pub fn value(name: &str) -> String {
        Self::guard_keyword(Self::snake(Self::without_change_suffix(name)))
    }

    fn without_change_suffix(name: &str) -> &str {
        match name.strip_suffix(Self::CHANGE_SUFFIX) {
            Some(body) if !body.is_empty() => body,
            _ => name,
        }
    }

    pub fn constant(name: &str) -> String {
        Self::snake(name).to_uppercase()
    }

    pub fn lifetime(name: &str) -> String {
        let body = name.strip_prefix('\'').unwrap_or(name);
        let mangled = Self::lifetime_body(body);
        let reserved = mangled == "_"
            || mangled == "static"
            || Self::NON_RAW_KEYWORDS.contains(&mangled.as_str())
            || Self::STRICT_KEYWORDS.contains(&mangled.as_str())
            || Self::RESERVED_KEYWORDS.contains(&mangled.as_str());
        if reserved {
            format!("'{mangled}_")
        } else {
            format!("'{mangled}")
        }
    }

    fn lifetime_body(body: &str) -> String {
        if Self::is_separators(body) {
            Self::WILDCARD.to_string()
        } else {
            Self::snake(body)
        }
    }

    fn is_separators(name: &str) -> bool {
        name.chars()
            .all(|character| Self::separator_word(character).is_some())
    }

    pub fn type_name(name: &str) -> String {
        let name = Self::variant_name(name);
        if Self::PRELUDE_NAMES.contains(&name.as_str()) {
            format!("{name}{}", Self::PRELUDE_SUFFIX)
        } else {
            name
        }
    }

    pub fn variant_name(name: &str) -> String {
        Self::snake(name)
            .split('_')
            .filter(|word| !word.is_empty())
            .map(Self::capitalized)
            .collect()
    }

    fn snake(name: &str) -> String {
        let (body, is_predicate) = match name.strip_suffix(Self::PREDICATE_SUFFIX) {
            Some(body) if !body.is_empty() => (body, true),
            _ => (name, false),
        };
        let spelled = Self::spell(body);
        let prefixed = if is_predicate {
            format!("{}{spelled}", Self::PREDICATE_PREFIX)
        } else {
            Self::spelled_or_separators(spelled, body)
        };
        Self::valid_start(prefixed)
    }

    fn spelled_or_separators(spelled: String, name: &str) -> String {
        if spelled.is_empty() {
            Self::spell_separators(name)
        } else {
            spelled
        }
    }

    fn spell_separators(name: &str) -> String {
        name.chars()
            .filter_map(Self::separator_word)
            .collect::<Vec<_>>()
            .join("_")
    }

    fn separator_word(character: char) -> Option<&'static str> {
        Self::LONE_SEPARATORS
            .iter()
            .find(|(separator, _)| *separator == character)
            .map(|(_, word)| *word)
    }

    fn spell(name: &str) -> String {
        let mut spelled = String::new();
        let mut characters = name.chars().peekable();
        while let Some(character) = characters.next() {
            if character == Self::ARROW_START && characters.peek() == Some(&Self::ARROW_END) {
                characters.next();
                Self::push_word(&mut spelled, Self::ARROW_WORD);
            } else if let Some(word) = Self::spelling(character) {
                Self::push_word(&mut spelled, word);
            } else if Self::continues_identifier(character) {
                spelled.push(character);
            } else {
                Self::push_word(&mut spelled, &Self::code_point(character));
            }
        }
        spelled.trim_end_matches('_').to_string().replace("__", "_")
    }

    fn push_word(spelled: &mut String, word: &str) {
        if !spelled.is_empty() && !spelled.ends_with('_') && word != "_" {
            spelled.push('_');
        }
        spelled.push_str(word);
        if word != "_" {
            spelled.push('_');
        }
    }

    fn spelling(character: char) -> Option<&'static str> {
        Self::PUNCTUATION
            .iter()
            .find(|(punctuation, _)| *punctuation == character)
            .map(|(_, word)| *word)
    }

    fn continues_identifier(character: char) -> bool {
        character == '_' || unicode_ident::is_xid_continue(character)
    }

    fn starts_identifier(character: char) -> bool {
        character == '_' || unicode_ident::is_xid_start(character)
    }

    fn code_point(character: char) -> String {
        format!("{}{:x}", Self::CODE_POINT_PREFIX, u32::from(character))
    }

    fn valid_start(name: String) -> String {
        if !name.starts_with(Self::starts_identifier) {
            format!("_{name}")
        } else {
            name
        }
    }

    fn guard_keyword(name: String) -> String {
        if Self::NON_RAW_KEYWORDS.contains(&name.as_str()) {
            format!("{name}_")
        } else if Self::STRICT_KEYWORDS.contains(&name.as_str())
            || Self::RESERVED_KEYWORDS.contains(&name.as_str())
        {
            format!("r#{name}")
        } else {
            name
        }
    }

    fn capitalized(word: &str) -> String {
        let mut characters = word.chars();
        match characters.next() {
            Some(first) => first.to_uppercase().chain(characters).collect(),
            None => String::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use rustc_hash::FxHashSet;

    use super::NameMangler;

    #[test]
    fn kebab_names_become_snake_case() {
        assert_eq!(NameMangler::value("make-point"), "make_point");
    }

    #[test]
    fn predicates_become_is_prefixed() {
        assert_eq!(NameMangler::value("admin?"), "is_admin");
        assert_eq!(NameMangler::constant("admin?"), "IS_ADMIN");
    }

    #[test]
    fn lifetimes_named_like_keywords_get_a_suffix() {
        assert_eq!(NameMangler::lifetime("'type"), "'type_");
        assert_eq!(NameMangler::lifetime("'static"), "'static_");
        assert_eq!(NameMangler::lifetime("'_"), "'__");
    }

    #[test]
    fn types_become_pascal_case() {
        assert_eq!(NameMangler::type_name("bank-account"), "BankAccount");
    }

    #[test]
    fn prelude_names_get_a_suffix() {
        assert_eq!(NameMangler::type_name("sized"), "Sized2");
        assert_eq!(NameMangler::type_name("send"), "Send2");
        assert_eq!(NameMangler::type_name("iterator"), "Iterator2");
    }

    #[test]
    fn punctuation_is_spelled_out() {
        assert_eq!(NameMangler::value("+"), "plus");
        assert_eq!(NameMangler::value("->string"), "to_string");
        assert_eq!(NameMangler::value("point->string"), "point_to_string");
        assert_eq!(NameMangler::value("set!"), "set");
        assert_eq!(NameMangler::value("set!x"), "set_bang_x");
    }

    #[test]
    fn every_printable_character_becomes_a_distinct_identifier() {
        let names: FxHashSet<String> = PrintableCharacters::ALL
            .map(|character| NameMangler::value(&character.to_string()))
            .inspect(|name| assert!(Identifier::is_valid(name), "`{name}` is not valid"))
            .collect();
        assert_eq!(names.len(), PrintableCharacters::ALL.count());
    }

    #[test]
    fn lone_separators_are_spelled_out() {
        assert_eq!(NameMangler::value("-"), "minus");
        assert_eq!(NameMangler::value("_"), "underscore");
        assert_eq!(NameMangler::value("_-"), "underscore_minus");
        assert_eq!(NameMangler::constant("_"), "UNDERSCORE");
    }

    #[test]
    fn a_wildcard_local_stays_a_wildcard() {
        assert_eq!(NameMangler::local("_"), "_");
        assert_eq!(NameMangler::local("-"), "minus");
    }

    #[test]
    fn characters_outside_identifiers_become_code_points() {
        assert_eq!(NameMangler::value("café"), "café");
        assert_eq!(NameMangler::value("cafe\u{301}"), "cafe\u{301}");
        assert_eq!(NameMangler::value("a·b"), "a·b");
        assert_eq!(NameMangler::value("☃"), "u2603");
        assert_eq!(NameMangler::value("snow☃man"), "snow_u2603_man");
    }

    struct PrintableCharacters;

    impl PrintableCharacters {
        const ALL: std::ops::RangeInclusive<char> = '!'..='~';
    }

    struct Identifier;

    impl Identifier {
        fn is_valid(name: &str) -> bool {
            let mut characters = name.chars();
            let starts = characters
                .next()
                .is_some_and(|first| first == '_' || first.is_alphabetic());
            starts && name != "_" && characters.all(|rest| rest == '_' || rest.is_alphanumeric())
        }
    }

    #[test]
    fn keywords_become_raw_identifiers() {
        for keyword in NameMangler::STRICT_KEYWORDS
            .into_iter()
            .chain(NameMangler::RESERVED_KEYWORDS)
        {
            assert_eq!(NameMangler::value(keyword), format!("r#{keyword}"));
        }
        for keyword in NameMangler::NON_RAW_KEYWORDS {
            assert_eq!(NameMangler::value(keyword), format!("{keyword}_"));
        }
    }
}