strck_ident/
rust.rs

1//! Checked strings containing Rust identifiers.
2//!
3//! Raw identifiers are unsupported.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use strck_ident::{IntoCk, rust::RustIdent};
9//!
10//! assert!("foo".ck::<RustIdent>().is_ok());
11//! assert!("_identifier".ck::<RustIdent>().is_ok());
12//! assert!("Москва".ck::<RustIdent>().is_ok());
13//! assert!("東京".ck::<RustIdent>().is_ok());
14//!
15//! assert!("struct".ck::<RustIdent>().is_err());
16//! assert!("r#try".ck::<RustIdent>().is_err());
17//! assert!("👍".ck::<RustIdent>().is_err());
18//! ```
19//!
20//! # Aliases
21//!
22//! This module exposes [`Ident`] and [`IdentBuf`], which alias `Ck<RustIdent>`
23//! and `Check<RustIdent>` respectively. These aliases are preferred to keep
24//! type signatures succinct.
25//!
26//! # Requirements
27//!
28//! This module is only available when the `rust` feature flag is enabled.
29use crate::unicode;
30use core::fmt;
31use strck::{Check, Ck, Invariant};
32
33/// An [`Invariant`] for Rust identifiers.
34///
35/// Raw identifiers are unsupported.
36///
37/// # Invariants
38///
39/// * The string is nonempty.
40/// * The first character is `_` or XID_Start.
41/// * Any following characters are XID_Continue.
42/// * The string isn't a single underscore, e.g. `"_"`.
43/// * The string isn't a [strict] or [reserved] keyword.
44///
45/// [strict]: https://doc.rust-lang.org/reference/keywords.html#strict-keywords
46/// [reserved]: https://doc.rust-lang.org/reference/keywords.html#reserved-keywords
47#[derive(Clone, Debug)]
48pub struct RustIdent;
49
50/// Borrowed checked string containing a Rust identifier.
51///
52/// See [`RustIdent`] for more details.
53pub type Ident = Ck<RustIdent>;
54
55/// Owned checked string containing a Rust identifier.
56///
57/// See [`RustIdent`] for more details.
58pub type IdentBuf<B = String> = Check<RustIdent, B>;
59
60/// The error type returned from checking the invariants of [`RustIdent`].
61#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
62pub enum Error {
63    /// An invalid unicode identifier.
64    Unicode(unicode::Error),
65
66    /// A [strict] or [reserved] keyword.
67    ///
68    /// [strict]: https://doc.rust-lang.org/reference/keywords.html#strict-keywords
69    /// [reserved]: https://doc.rust-lang.org/reference/keywords.html#reserved-keywords
70    Keyword(&'static str),
71
72    /// A single underscore.
73    Wildcard,
74}
75
76impl std::error::Error for Error {}
77
78impl fmt::Display for Error {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        match self {
81            Error::Unicode(err) => err.fmt(f),
82            Error::Keyword(keyword) => {
83                write!(f, "Rust keyword: '{keyword}'")
84            }
85            Error::Wildcard => f.pad("wildcard '_' isn't a valid Rust ident"),
86        }
87    }
88}
89
90impl Invariant for RustIdent {
91    type Error = Error;
92
93    fn check(slice: &str) -> Result<(), Self::Error> {
94        match unicode::UnicodeIdent::check(slice) {
95            Ok(()) => match KEYWORDS.binary_search(&slice) {
96                Ok(index) => Err(Error::Keyword(KEYWORDS[index])),
97                Err(_) => Ok(()),
98            },
99            Err(unicode::Error::Start('_')) => match slice.len() {
100                1 => Err(Error::Wildcard), // `_` isn't ok
101                _ => Ok(()),               // `_x` is ok
102            },
103            Err(e) => Err(Error::Unicode(e)),
104        }
105    }
106}
107
108static KEYWORDS: [&str; 51] = [
109    "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
110    "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl",
111    "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
112    "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof",
113    "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
114];
115
116#[cfg(test)]
117mod tests {
118    use super::{Error, RustIdent};
119    use strck::IntoCk;
120
121    #[test]
122    fn test_underscore() {
123        assert_eq!("_".ck::<RustIdent>().unwrap_err(), Error::Wildcard);
124        assert!("_unused".ck::<RustIdent>().is_ok());
125        assert!("__private".ck::<RustIdent>().is_ok());
126        assert!("snake_case".ck::<RustIdent>().is_ok());
127    }
128
129    #[test]
130    fn test_rust_reference() {
131        assert!("foo".ck::<RustIdent>().is_ok());
132        assert!("_identifier".ck::<RustIdent>().is_ok());
133        assert!("Москва".ck::<RustIdent>().is_ok());
134        assert!("東京".ck::<RustIdent>().is_ok());
135    }
136}