#[cfg(feature = "proc_macro2")]
use crate::{IParse, Ident, ToTokens};
#[cfg(feature = "hash_keywords")]
use rustc_hash::FxHashSet;
const MAX_LINEAR_SEARCH: usize = 4;
#[doc(hidden)]
pub enum KeywordGroup {
List(&'static [&'static KeywordGroup]),
Keyword(&'static str),
}
impl KeywordGroup {
#[allow(clippy::iter_without_into_iter)] fn iter(&self) -> KeywordGroupIter {
let KeywordGroup::List(nodes) = self else {
panic!();
};
let mut vec = Vec::with_capacity(8);
vec.push(nodes.iter());
KeywordGroupIter(vec)
}
#[cfg(feature = "proc_macro2")]
#[mutants::skip]
fn assert_identifiers(&self) {
for token in self.iter() {
assert!(
token.to_token_iter().parse::<Ident>().is_ok(),
"'{token}' is not a valid keyword"
);
}
}
#[cfg(not(feature = "proc_macro2"))]
#[mutants::skip]
fn assert_identifiers(&self) {
for token in self.iter() {
let mut chars = token.chars();
let is_valid = chars.next().map_or(false, |c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_');
assert!(is_valid, "'{token}' is not a valid keyword");
}
}
fn bounded_len(&self, mut max: usize) -> usize {
let mut count = 0usize;
let mut iter = self.iter();
while iter.next().is_some() && max > 0 {
count += 1;
max -= 1;
}
count
}
}
struct KeywordGroupIter(Vec<std::slice::Iter<'static, &'static KeywordGroup>>);
impl Iterator for KeywordGroupIter {
type Item = &'static str;
#[mutants::skip] fn next(&mut self) -> Option<&'static str> {
while let Some(tree) = self.0.last_mut() {
match tree.next() {
Some(KeywordGroup::List(nodes)) => self.0.push(nodes.iter()),
Some(KeywordGroup::Keyword(s)) => {
return Some(s);
}
None => {
self.0.pop();
}
}
}
None
}
}
#[doc(hidden)]
#[must_use]
#[mutants::skip]
pub fn create_matchfn(group: &'static KeywordGroup) -> Box<dyn Fn(&str) -> bool + Send + Sync> {
group.assert_identifiers();
match group.bounded_len(MAX_LINEAR_SEARCH + 1) {
#[cfg(debug_assertions)]
0 => {
panic!("empty KeywordGroup")
}
1 => {
#[allow(clippy::unwrap_used)]
let s = group.iter().next().unwrap();
Box::new(move |this| this == s)
}
len @ 2..=MAX_LINEAR_SEARCH => {
let mut array = [""; MAX_LINEAR_SEARCH];
for (index, element) in group.iter().enumerate() {
array[index] = element;
}
Box::new(move |this| array[..len].contains(&this))
}
#[cfg(feature = "hash_keywords")]
_ => {
let hash = group.iter().collect::<FxHashSet<_>>();
Box::new(move |this| hash.contains(&this))
}
#[cfg(not(feature = "hash_keywords"))]
_ => {
let mut vec = group.iter().collect::<Vec<_>>();
vec.sort_unstable();
vec.dedup();
Box::new(move |this| vec.binary_search(&this).is_ok())
}
}
}
#[cfg(test)]
mod test {
use crate::*;
keyword! {
If = "if";
Else = "else";
IfElseThen = [If, Else, "then"]
}
#[test]
fn keyword_bounded_iter() {
assert_eq!(IfElseThen::keywords().bounded_len(2), 2);
assert_eq!(IfElseThen::keywords().bounded_len(20), 3);
}
#[test]
#[should_panic(expected = "not a valid keyword")]
fn invalid_keyword() {
keyword! {
InvalidKeyword = [IfElseThen, "works", "000 is not a keyword"];
}
let mut tokens = "test".into_token_iter();
let _: InvalidKeyword = tokens.parse().unwrap();
}
}