use regexr::Regex;
fn compile(pattern: &str) -> Result<Regex, regexr::Error> {
#[cfg(feature = "jit")]
{
regexr::RegexBuilder::new(pattern).jit(true).build()
}
#[cfg(not(feature = "jit"))]
{
Regex::new(pattern)
}
}
fn assert_escape_denotes(escape: &str, expected: char) {
let subject = expected.to_string();
let other = if expected == 'x' { 'y' } else { 'x' }.to_string();
for pattern in [
escape.to_string(),
format!("a{escape}"),
format!("[{escape}]"),
] {
let re = compile(&pattern).unwrap_or_else(|e| panic!("{pattern:?} should compile: {e}"));
let haystack = if pattern.starts_with('a') {
format!("a{subject}")
} else {
subject.clone()
};
assert!(
re.is_match(&haystack),
"{pattern:?} should match {haystack:?} (U+{:04X})",
expected as u32
);
assert!(
!re.is_match(&other),
"{pattern:?} matched {other:?}, so it is not anchored to U+{:04X}",
expected as u32
);
}
}
#[test]
fn control_character_escapes_denote_their_control_characters() {
assert_escape_denotes(r"\n", '\n');
assert_escape_denotes(r"\r", '\r');
assert_escape_denotes(r"\t", '\t');
assert_escape_denotes(r"\f", '\u{0c}');
assert_escape_denotes(r"\v", '\u{0b}');
assert_escape_denotes(r"\0", '\0');
}
#[test]
fn escape_character_escape_denotes_u001b() {
assert_escape_denotes(r"\e", '\u{1b}');
}
#[test]
fn alert_escape_denotes_u0007() {
assert_escape_denotes(r"\a", '\u{07}');
}
#[test]
fn escaped_ascii_punctuation_denotes_itself() {
for c in ' '..='~' {
if c.is_ascii_alphanumeric() {
continue;
}
assert_escape_denotes(&format!("\\{c}"), c);
}
}
#[test]
fn braced_hex_escape_denotes_the_code_point() {
assert_escape_denotes(r"\x{41}", 'A');
assert_escape_denotes(r"\x{263A}", '\u{263A}');
assert_escape_denotes(r"\x{1F600}", '\u{1F600}');
}
#[test]
fn two_digit_hex_escape_still_works() {
assert_escape_denotes(r"\x41", 'A');
}
#[test]
fn control_escape_denotes_the_control_character() {
assert_escape_denotes(r"\cA", '\u{01}');
assert_escape_denotes(r"\ca", '\u{01}');
assert_escape_denotes(r"\cZ", '\u{1A}');
assert_escape_denotes(r"\cz", '\u{1A}');
}
#[test]
fn backspace_inside_class_denotes_u0008() {
let re = compile(r"[\b]").unwrap_or_else(|e| panic!(r"[\b] should compile: {e}"));
assert!(re.is_match("\u{8}"));
assert!(!re.is_match("b"));
}
#[test]
fn bare_word_boundary_is_unaffected_by_class_backspace() {
let re = compile(r"\bcat\b").unwrap();
assert!(re.is_match("a cat sat"));
assert!(!re.is_match("concatenate"));
}
#[test]
fn unassigned_letter_escapes_remain_errors() {
for pattern in [r"\j", r"\J", r"\m", r"\T", r"\Y", r"\g", r"\l"] {
assert!(
compile(pattern).is_err(),
"{pattern:?} has no assigned meaning and must not compile"
);
}
}
#[test]
fn horizontal_whitespace_matches_tab_space_and_unicode_separators() {
for pattern in [r"\h", r"a\h", r"[\h]"] {
let re = compile(pattern).unwrap_or_else(|e| panic!("{pattern:?} should compile: {e}"));
for c in ['\t', ' ', '\u{A0}', '\u{3000}'] {
let haystack = if pattern.starts_with('a') {
format!("a{c}")
} else {
c.to_string()
};
assert!(
re.is_match(&haystack),
"{pattern:?} should match {haystack:?} (U+{:04X})",
c as u32
);
}
let rejected = if pattern.starts_with('a') { "ab" } else { "a" };
assert!(!re.is_match(rejected), "{pattern:?} matched {rejected:?}");
assert!(!re.is_match("\n"), "{pattern:?} matched a newline");
}
}
#[test]
fn not_horizontal_whitespace_matches_everything_else() {
for pattern in [r"\H", r"[\H]"] {
let re = compile(pattern).unwrap_or_else(|e| panic!("{pattern:?} should compile: {e}"));
assert!(re.is_match("a"), "{pattern:?} should match 'a'");
assert!(re.is_match("\n"), "{pattern:?} should match a newline");
assert!(!re.is_match("\t"), "{pattern:?} should not match a tab");
assert!(
!re.is_match("\u{A0}"),
"{pattern:?} should not match U+00A0"
);
}
}
#[test]
fn not_horizontal_whitespace_matches_whole_multibyte_character() {
let re = compile(r"\H").unwrap();
let m = re.find("é").expect(r"\H should match 'é'");
assert_eq!(m.as_str(), "é");
assert_eq!(m.end() - m.start(), 2, "match should span both UTF-8 bytes");
}
#[test]
fn horizontal_whitespace_unions_with_other_perl_classes_in_a_class() {
let re = compile(r"[\h\d]").unwrap();
assert!(re.is_match(" "));
assert!(re.is_match("5"));
assert!(!re.is_match("a"));
}
#[test]
fn line_break_consumes_crlf_as_one_match() {
let re = compile(r"\R").unwrap();
let m = re.find("\r\n").expect(r"\R should match \r\n");
assert_eq!(m.as_str(), "\r\n");
assert_eq!(m.start(), 0);
assert_eq!(m.end(), 2);
}
#[test]
fn line_break_matches_each_single_character_break() {
let re = compile(r"\R").unwrap();
for c in [
'\n', '\u{0B}', '\u{0C}', '\r', '\u{85}', '\u{2028}', '\u{2029}',
] {
let haystack = c.to_string();
let m = re
.find(&haystack)
.unwrap_or_else(|| panic!(r"\R should match U+{:04X}", c as u32));
assert_eq!(m.as_str(), haystack);
}
}
#[test]
fn line_break_composes_with_surrounding_literals() {
let re = compile(r"a\Rb").unwrap();
assert!(re.is_match("a\r\nb"));
let m = re.find("a\r\nb").unwrap();
assert_eq!(m.as_str(), "a\r\nb");
}
#[test]
fn any_except_newline_matches_any_char_but_newline() {
let re = compile(r"\N").unwrap();
assert!(re.is_match("a"));
let m = re.find("é").expect(r"\N should match 'é'");
assert_eq!(m.as_str(), "é");
assert!(!re.is_match("\n"));
}
#[test]
fn any_except_newline_ignores_dot_all_flag() {
let re = compile(r"(?s)\N").unwrap();
assert!(!re.is_match("\n"), "(?s)\\N must still reject a newline");
let dot = compile(r"(?s).").unwrap();
assert!(
dot.is_match("\n"),
"(?s). must still match a newline, unaffected by \\N"
);
}
#[test]
fn named_unicode_character_escape_is_rejected() {
let err = compile(r"\N{LATIN SMALL LETTER A}")
.expect_err(r"\N{...} should be rejected")
.to_string();
assert!(
err.to_lowercase().contains("named"),
"error should explain the named form is unsupported: {err}"
);
}
#[test]
fn line_break_and_any_except_newline_are_rejected_inside_a_class() {
for pattern in [r"[\R]", r"[\N]"] {
assert!(
compile(pattern).is_err(),
"{pattern:?} must be rejected inside a character class"
);
}
}