check_keyword/
lib.rs

1//! A trait for String-like types to check if a string is a keyword,
2//! and convert it to a safe non-keyword if so. All types of keywords are supported,
3//! and compile features can be used to check against past rust editions.
4//! (Default is Rust 2021.)
5//!
6//! This library assumes the strings being checked are already valid identifiers in
7//! every way *except* that it might be a reserved keyword.
8//!
9//! You can add this dependency with:
10//!
11//! ```toml
12//! [dependencies]
13//! check_keyword = "0.3.1"
14//! ```
15//!
16//! # Example
17//!
18//! ```
19//! # use check_keyword::CheckKeyword;
20//! assert!(!"not_a_keyword".is_keyword());
21//! assert_eq!("not_a_keyword".into_safe(), "not_a_keyword");
22//!
23//! assert!("match".is_keyword());
24//! assert_eq!("match".into_safe(), "r#match");
25//! ```
26//!
27//! The [CheckKeyword::into_safe] method automatically checks [CheckKeyword::is_keyword] for you.
28//! You don't need to call [CheckKeyword::is_keyword]
29//! if you don't care whether it was originally a keyword or not.
30//!
31//! [CheckKeyword::is_keyword] only checks for strict and reserved keywords. For more detail, and support
32//! for weak keywords, use [CheckKeyword::keyword_status].
33//!
34//! # Implementors
35//!
36//! This trait has a blanket implementation for all types that implement `AsRef<str>`. This includes
37//! `&str` and `String`.
38//!
39//! # Raw Identifiers
40//!
41//! Raw identifiers are identifiers that start with `r#`, and most keywords are allowed
42//! to be used as raw identifiers.
43//!
44//! # Rust Editions
45//!
46//! By default, the keywords added in Rust Edition 2018 are included in the list of checked keywords.
47//! This can be disabled with `default-features = false` in your Cargo.toml.
48//!
49//! ```toml
50//! [dependencies]
51//! check_keyword = { version = "0.3.1", default-features = false }
52//! ```
53//!
54//! This crate is up-to-date with Rust 2021. Future Rust editions may add new keywords, and this
55//! crate will be updated to reflect that.
56//! (Or you can create an issue on github if I forget.)
57
58use phf::phf_map;
59
60/// A trait for checking if `self` is a keyword.
61pub trait CheckKeyword {
62    /// Check if `self` is a strict or reserved keyword.
63    ///
64    /// If you want to check weak keywords, use [CheckKeyword::keyword_status].
65    fn is_keyword(&self) -> bool;
66
67    /// Returns a detailed description of the type of keyword.
68    fn keyword_status(&self) -> KeywordStatus;
69
70    /// If it is a keyword, add "r#" to the beginning if possible,
71    /// or "_" to the end if not.
72    fn into_safe(self) -> String;
73}
74
75/// Detailed information about keywords.
76#[derive(Copy, Clone, Eq, PartialEq, Debug)]
77pub enum KeywordStatus {
78    /// The input was not any kind of keyword.
79    NotKeyword,
80
81    /// Strict keywords are always invalid identifiers.
82    Strict {
83        /// Whether this keyword can be converted to a valid identifier
84        /// by prepending "r#".
85        can_be_raw: bool,
86    },
87
88    /// Reserved keywords are always invalid identifiers,
89    /// but are not currently used within Rust.
90    Reserved,
91
92    /// Weak keywords are only keywords in certain contexts.
93    ///
94    /// Some weak keywords, such as `union` or `macro_rules`,
95    /// are technically keywords but can still be used in all
96    /// contexts.
97    Weak {
98        /// The restriction where the keyword cannot be used.
99        restriction: WeakRestriction,
100    },
101}
102
103/// Restricted contexts where a weak keyword cannot be used.
104#[derive(Copy, Clone, Eq, PartialEq, Debug)]
105pub enum WeakRestriction {
106    /// The keyword can be used anywhere that permits an identifier.
107    None,
108
109    /// The keyword cannot be used as lifetime or loop label.
110    LifetimeOrLoop,
111
112    /// The keyword is `dyn`. In 2015 edition, `dyn` cannot be used
113    /// in type position followed by a path that does not start with `::`.
114    Dyn,
115}
116
117use KeywordStatus::*;
118use WeakRestriction::*;
119
120static KEYWORDS: phf::Map<&'static str, KeywordStatus> = phf_map! {
121
122    // STRICT, 2015
123
124    "as" => Strict { can_be_raw: true },
125    "break" => Strict { can_be_raw: true },
126    "const" => Strict { can_be_raw: true },
127    "continue" => Strict { can_be_raw: true },
128    "crate" => Strict { can_be_raw: false },
129    "else" => Strict { can_be_raw: true },
130    "enum" => Strict { can_be_raw: true },
131    "extern" => Strict { can_be_raw: true },
132    "false" => Strict { can_be_raw: true },
133    "fn" => Strict { can_be_raw: true },
134    "for" => Strict { can_be_raw: true },
135    "if" => Strict { can_be_raw: true },
136    "impl" => Strict { can_be_raw: true },
137    "in" => Strict { can_be_raw: true },
138    "let" => Strict { can_be_raw: true },
139    "loop" => Strict { can_be_raw: true },
140    "match" => Strict { can_be_raw: true },
141    "mod" => Strict { can_be_raw: true },
142    "move" => Strict { can_be_raw: true },
143    "mut" => Strict { can_be_raw: true },
144    "pub" => Strict { can_be_raw: true },
145    "ref" => Strict { can_be_raw: true },
146    "return" => Strict { can_be_raw: true },
147    "self" => Strict { can_be_raw: false },
148    "Self" => Strict { can_be_raw: false },
149    "static" => Strict { can_be_raw: true },
150    "struct" => Strict { can_be_raw: true },
151    "super" => Strict { can_be_raw: false },
152    "trait" => Strict { can_be_raw: true },
153    "true" => Strict { can_be_raw: true },
154    "type" => Strict { can_be_raw: true },
155    "unsafe" => Strict { can_be_raw: true },
156    "use" => Strict { can_be_raw: true },
157    "where" => Strict { can_be_raw: true },
158    "while" => Strict { can_be_raw: true },
159
160    // STRICT, 2018
161
162    "async" => if cfg!(feature = "2018") { Strict { can_be_raw: true } } else { NotKeyword },
163    "await" => if cfg!(feature = "2018") { Strict { can_be_raw: true } } else { NotKeyword },
164
165    // DYN
166
167    "dyn" => if cfg!(feature = "2018") {
168        Strict { can_be_raw: true }
169    } else {
170        Weak { restriction: Dyn }
171    },
172
173    // RESERVED, 2015
174
175    "abstract" => Reserved,
176    "become" => Reserved,
177    "box" => Reserved,
178    "do" => Reserved,
179    "final" => Reserved,
180    "macro" => Reserved,
181    "override" => Reserved,
182    "priv" => Reserved,
183    "typeof" => Reserved,
184    "unsized" => Reserved,
185    "virtual" => Reserved,
186    "yield" => Reserved,
187
188    // RESERVED, 2018
189
190    "try" => if cfg!(feature = "2018") { Reserved } else { NotKeyword },
191
192    // WEAK
193
194    "macro_rules" => Weak { restriction: None },
195    "union" => Weak { restriction: None },
196    "'static" => Weak { restriction: LifetimeOrLoop }
197};
198
199impl<T: AsRef<str>> CheckKeyword for T {
200    fn is_keyword(&self) -> bool {
201        matches!(self.keyword_status(), Strict { .. } | Reserved)
202    }
203
204    fn keyword_status(&self) -> KeywordStatus {
205        *KEYWORDS.get(self.as_ref()).unwrap_or(&NotKeyword)
206    }
207
208    fn into_safe(self) -> String {
209        let self_ref = self.as_ref();
210        match self.keyword_status() {
211            Strict { can_be_raw: false }
212            | Weak {
213                restriction: LifetimeOrLoop,
214            } => format!("{self_ref}_"),
215            Strict { .. } | Reserved | Weak { restriction: Dyn } => format!("r#{self_ref}"),
216            _ => self_ref.to_string(),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn is_keyword() {
227        assert!(String::from("match").is_keyword());
228        assert!(!"hello".is_keyword());
229
230        assert!("crate".is_keyword());
231
232        assert_eq!(String::from("async").is_keyword(), cfg!(feature = "2018"));
233    }
234
235    #[test]
236    fn keyword_status() {
237        assert_eq!("asdf".keyword_status(), NotKeyword);
238
239        assert_eq!(
240            "dyn".keyword_status(),
241            if cfg!(feature = "2018") {
242                Strict { can_be_raw: true }
243            } else {
244                Weak { restriction: Dyn }
245            }
246        );
247
248        assert_eq!(
249            "'static".keyword_status(),
250            Weak {
251                restriction: LifetimeOrLoop
252            }
253        );
254    }
255
256    #[test]
257    fn into_safe() {
258        assert_eq!(String::from("match").into_safe(), "r#match");
259        assert_eq!("asdf".into_safe(), "asdf");
260
261        assert_eq!(
262            "await".into_safe(),
263            if cfg!(feature = "2018") {
264                "r#await"
265            } else {
266                "await"
267            }
268        );
269
270        assert_eq!("self".into_safe(), "self_");
271    }
272}