use regexr::{Regex, Result};
fn compile(pattern: &str) -> Result<Regex> {
#[cfg(feature = "jit")]
{
regexr::RegexBuilder::new(pattern).jit(true).build()
}
#[cfg(not(feature = "jit"))]
{
Regex::new(pattern)
}
}
const MALFORMED_ESCAPES: &[&str] = &[
r"\",
r"\x",
r"\xZ",
r"\xZZ",
r"\x{",
r"\u",
r"\uZZ",
r"\u{",
r"\u{ZZZZ}",
r"\p",
r"\p{",
r"\p{L",
r"\P{",
r"[[:bogus:]]",
r"[[:alpha]",
r"[[::]]",
r"\x{}",
r"\x{ZZ}",
r"\x{110000}",
r"\x{263A",
r"\c",
r"\c1",
r"[\R]",
r"[\N]",
r"\N{LATIN SMALL LETTER A}",
r"\k<nosuch>",
r"\k<>",
r"\k<w",
r"\k",
r"\kX",
r"\k<w}",
r"\k{w>",
r"(?P=nosuch)",
r"(?P=)",
r"(?P=w",
r"[a-z&&]",
r"[a-z--]",
r"[a-z~~]",
r"[&&a-z]",
r"[a-z&&",
r"a*+",
r"a++",
r"a?+",
r"a{2,3}+",
r"(?>a*)b",
r"a**",
r"a*{2}",
];
#[test]
fn malformed_escape_at_pattern_start_is_rejected() {
for pattern in MALFORMED_ESCAPES {
assert!(
compile(pattern).is_err(),
"{pattern:?} must not compile: the escape is malformed"
);
}
}
#[test]
fn malformed_escape_never_compiles_to_a_match_everything_pattern() {
for pattern in MALFORMED_ESCAPES {
if let Ok(re) = compile(pattern) {
assert!(
!(re.is_match("") && re.is_match("zzz") && re.is_match("\u{1b}")),
"{pattern:?} compiled into a pattern that matches every input"
);
}
}
}
#[test]
fn escape_acceptance_does_not_depend_on_position() {
for c in ' '..='~' {
let leading = format!("\\{c}");
let trailing = format!("a\\{c}");
assert_eq!(
compile(&leading).is_ok(),
compile(&trailing).is_ok(),
"\\{c} is accepted at one position and rejected at another \
({leading:?} vs {trailing:?})"
);
}
}
#[test]
fn malformed_escape_acceptance_does_not_depend_on_position() {
for pattern in MALFORMED_ESCAPES {
let trailing = format!("a{pattern}");
assert_eq!(
compile(pattern).is_ok(),
compile(&trailing).is_ok(),
"{pattern:?} is accepted at one position and rejected at another"
);
}
}
#[test]
fn leading_invalid_escape_reports_position_zero() {
let err = compile(r"\q")
.expect_err(r"\q must be rejected")
.to_string();
assert!(
err.contains("position 0"),
"error should locate the escape at position 0: {err}"
);
}
#[test]
fn class_escape_error_names_the_offending_escape() {
for (pattern, escape) in [(r"[\B]", r"\B"), (r"[\z]", r"\z")] {
let err = compile(pattern)
.expect_err(&format!("{pattern} must be rejected"))
.to_string();
assert!(
err.contains(escape),
"diagnostic for {pattern} should name {escape}: {err}"
);
assert!(
!err.contains(r"\?"),
"diagnostic for {pattern} reports a placeholder instead of the escape: {err}"
);
}
}
#[test]
fn possessive_and_atomic_diagnostics_name_the_construct() {
let err = compile("a*+")
.expect_err("a*+ must be rejected")
.to_string();
assert!(
err.contains("possessive"),
"diagnostic for a*+ should mention possessive quantifiers: {err}"
);
let err = compile("(?>a*)b")
.expect_err("(?>a*)b must be rejected")
.to_string();
assert!(
err.contains("atomic"),
"diagnostic for (?>a*)b should mention atomic groups: {err}"
);
}
#[test]
fn nested_quantifier_diagnostic_is_unaffected_by_possessive_detection() {
for pattern in [r"a**", r"a*{2}"] {
let err = compile(pattern)
.expect_err(&format!("{pattern} must be rejected"))
.to_string();
assert!(
err.contains("nested"),
"diagnostic for {pattern} should mention nested quantifiers: {err}"
);
assert!(
!err.contains("possessive"),
"diagnostic for {pattern} should not be misreported as possessive: {err}"
);
}
}
#[test]
fn empty_pattern_is_the_only_legitimate_match_everything() {
let re = compile("").expect("the empty pattern is valid");
assert!(re.is_match(""));
assert!(re.is_match("anything"));
}