pub(crate) const VALID_FLAGS: &str = "dgimsuvy";
const REGEX_ALLOWING_KEYWORDS: [&str; 15] = [
"return",
"typeof",
"instanceof",
"in",
"of",
"new",
"delete",
"void",
"do",
"else",
"case",
"yield",
"await",
"throw",
"",
];
pub(crate) fn is_valid_flag_string(flags: &str) -> bool {
let mut seen = Vec::new();
for flag in flags.chars() {
if !VALID_FLAGS.contains(flag) || seen.contains(&flag) {
return false;
}
seen.push(flag);
}
true
}
pub(crate) fn compiles(pattern: &str, flags: &str) -> bool {
if !is_valid_flag_string(flags) {
return false;
}
regress::Regex::with_flags(pattern, flags).is_ok()
}
fn is_word_character(character: char) -> bool {
character.is_ascii_alphanumeric() || character == '_' || character == '$'
}
pub(crate) fn is_regex_context(text: &str, offset: usize) -> bool {
let mut end = offset.min(text.len());
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
let before = text[..end].trim_end_matches([' ', '\t']);
let Some(previous) = before.chars().next_back() else {
return true; };
if previous == '\n' || previous == '\r' {
return true; }
if is_word_character(previous) {
let word = before.trim_end_matches(is_word_character);
return REGEX_ALLOWING_KEYWORDS.contains(&&before[word.len()..]);
}
!matches!(previous, ')' | ']' | '.' | '/')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legal_flags_appear_at_most_once() {
assert!(is_valid_flag_string(""));
assert!(is_valid_flag_string("gi"));
assert!(is_valid_flag_string("dgimsuvy"));
assert!(!is_valid_flag_string("gg"), "a repeat is not valid");
assert!(!is_valid_flag_string("x"));
assert!(!is_valid_flag_string("GI"), "flags are lowercase");
}
#[test]
fn an_invalid_pattern_does_not_compile() {
assert!(compiles("a+", ""));
assert!(!compiles("(", ""));
assert!(!compiles("a{2,1}", ""));
assert!(!compiles("[z-a]", ""));
}
#[test]
fn bad_flags_fail_to_compile() {
assert!(!compiles("x", "zz"));
assert!(!compiles("x", "q"));
}
#[test]
fn a_slash_at_the_start_opens_a_regex() {
assert!(is_regex_context("/a/", 0));
assert!(is_regex_context("\n/a/", 1));
assert!(is_regex_context(" /a/", 2));
}
#[test]
fn a_slash_after_a_value_is_division() {
assert!(!is_regex_context("a / b", 2));
assert!(!is_regex_context("1 / 2", 2));
assert!(!is_regex_context("] / 2", 2));
assert!(!is_regex_context(")/a/", 1));
}
#[test]
fn a_slash_after_a_slash_is_not_a_regex() {
assert!(!is_regex_context("https://x", 7));
}
#[test]
fn a_keyword_may_be_followed_by_a_regex() {
assert!(is_regex_context("return /a/", 7));
assert!(is_regex_context("case /a/", 5));
assert!(!is_regex_context("count /a/", 6), "not a keyword");
}
#[test]
fn a_non_ascii_letter_does_not_make_an_identifier() {
assert!(is_regex_context("café /a/", 5));
}
#[test]
fn a_keyword_is_read_off_the_end_of_the_identifier() {
assert!(is_regex_context("x = return /a/", 11));
assert!(!is_regex_context("x = noreturn /a/", 13));
}
#[test]
fn an_operator_may_be_followed_by_a_regex() {
assert!(is_regex_context("x = /a/", 4));
assert!(is_regex_context("foo(/a/)", 4));
}
}