#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use rexact_syntax::error::ErrorCode;
use rexact_syntax::{Dialect, Flags, ParseLimits, Pattern};
fn fl(d: Dialect, letters: &str) -> Flags {
Flags::from_letters(d, letters).expect("valid flags").0
}
fn tree(d: Dialect, flags: &str, p: &str) -> String {
match Pattern::parse(p, d, fl(d, flags)) {
Ok(pat) => pat.ast.to_string(),
Err(e) => panic!("{p:?} should parse: {e}"),
}
}
fn es(p: &str) -> String {
tree(Dialect::Ecma, "", p)
}
fn esu(p: &str) -> String {
tree(Dialect::Ecma, "u", p)
}
fn py(p: &str) -> String {
tree(Dialect::Python, "", p)
}
fn bad(d: Dialect, flags: &str, p: &str) -> (ErrorCode, usize, usize) {
match Pattern::parse(p, d, fl(d, flags)) {
Ok(pat) => panic!("{p:?} should fail but parsed as {}", pat.ast),
Err(e) => {
let (l, c) = e.position.unwrap_or((0, 0));
(e.kind, l, c)
}
}
}
fn es_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Ecma, "", p)
}
fn esu_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Ecma, "u", p)
}
fn py_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Python, "", p)
}
#[test]
fn es_basic_structure() {
assert_eq!(es("(a|b)*"), "(rep 0 inf (group#1 (alt a b)))");
assert_eq!(es("a{2,3}?"), "(rep? 2 3 a)");
assert_eq!(es("[ab]{2,3}"), "(rep 2 3 [61 62])");
assert_eq!(es("\\d+"), "(rep 1 inf [\\d])");
assert_eq!(es("a|"), "(alt a eps)");
assert_eq!(es("()"), "(group#1 eps)");
assert_eq!(es("(?:a)"), "(ncgroup a)");
assert_eq!(es("(?<y>a)"), "(group#1:y a)");
assert_eq!(es("a.b"), "(cat a any b)");
assert_eq!(es("^a$"), "(cat LineStart a LineEnd)");
}
#[test]
fn es_lookaround() {
assert_eq!(es("(?=a)b"), "(cat (lookahead a) b)");
assert_eq!(es("(?!a)"), "(lookahead! a)");
assert_eq!(es("(?<=a)b"), "(cat (lookbehind a) b)");
assert_eq!(es("(?<!a)"), "(lookbehind! a)");
}
#[test]
fn es_braces_that_are_not_quantifiers_are_literal_outside_unicode_mode() {
assert_eq!(es("a{,5}"), "(cat a { , 5 })");
assert_eq!(es("x{"), "(cat x {)");
assert_eq!(es("a}]"), "(cat a } ])");
}
#[test]
fn es_classes() {
assert_eq!(es("[]"), "[]");
assert_eq!(es("[^]"), "[^]");
assert_eq!(es("[a-c\\d]"), "[61-63 \\d]");
assert_eq!(es("[\\b]"), "[8]");
assert_eq!(es("[a-\\d]"), "[61 2D \\d]");
}
#[test]
fn es_escapes() {
assert_eq!(es("\\0"), "U+0000");
assert_eq!(es("\\x41"), "A");
assert_eq!(es("\\cA"), "U+0001");
assert_eq!(es("\\n\\t"), "(cat U+000A U+0009)");
assert_eq!(es("\\1"), "U+0001"); assert_eq!(es("\\8"), "8");
assert_eq!(es("\\q"), "q");
assert_eq!(es("\\u0041"), "A");
assert_eq!(es("\\xZ"), "(cat x Z)");
}
#[test]
fn es_surrogates_follow_unicode_flag() {
assert_eq!(esu("\\u{1F600}"), "U+1F600");
assert_eq!(esu("\\uD83D\\uDE00"), "U+1F600");
assert_eq!(es("\\uD83D\\uDE00"), "(cat U+D83D U+DE00)");
}
#[test]
fn es_errors_with_positions() {
assert_eq!(es_bad("(a"), (ErrorCode::P007, 1, 1));
assert_eq!(es_bad("a)"), (ErrorCode::P007, 1, 2));
assert_eq!(es_bad("["), (ErrorCode::P007, 1, 1));
assert_eq!(es_bad("*a"), (ErrorCode::P005, 1, 1));
assert_eq!(es_bad("a**"), (ErrorCode::P005, 1, 3));
assert_eq!(es_bad("a++"), (ErrorCode::P005, 1, 3));
assert_eq!(es_bad("a{2,1}"), (ErrorCode::P005, 1, 2));
assert_eq!(es_bad("{1}"), (ErrorCode::P005, 1, 1));
assert_eq!(es_bad("[b-a]"), (ErrorCode::P006, 1, 3));
assert_eq!(es_bad("\\"), (ErrorCode::P004, 1, 1));
assert_eq!(es_bad("(?x)"), (ErrorCode::P001, 1, 1));
assert_eq!(es_bad("(?<n>a)(?<n>b)"), (ErrorCode::P009, 1, 11));
assert_eq!(es_bad("(?<1>a)"), (ErrorCode::P009, 1, 4));
assert_eq!(es_bad("^*"), (ErrorCode::P005, 1, 2));
assert_eq!(es_bad("\\b+"), (ErrorCode::P005, 1, 3));
assert_eq!(es_bad("(?<=a)*"), (ErrorCode::P005, 1, 7));
assert_eq!(es("(?=a)*"), "(rep 0 inf (lookahead a))");
}
#[test]
fn es_error_positions_are_line_and_column() {
assert_eq!(es_bad("a\n(b"), (ErrorCode::P007, 2, 1));
}
#[test]
fn es_unicode_mode_is_strict() {
assert_eq!(esu_bad("{"), (ErrorCode::P001, 1, 1));
assert_eq!(esu_bad("a}"), (ErrorCode::P001, 1, 2));
assert_eq!(esu_bad("\\q"), (ErrorCode::P004, 1, 1));
assert_eq!(esu_bad("\\1"), (ErrorCode::P004, 1, 1));
assert_eq!(esu_bad("\\u{110000}"), (ErrorCode::P004, 1, 1));
assert_eq!(esu_bad("[a-\\d]"), (ErrorCode::P006, 1, 3));
assert_eq!(esu_bad("(?=a)*"), (ErrorCode::P005, 1, 6));
assert_eq!(esu("\\/"), "/");
assert_eq!(esu_bad("\\-"), (ErrorCode::P004, 1, 1)); assert_eq!(esu("[\\-]"), "[2D]");
}
#[test]
fn es_unsupported_constructs_have_their_own_codes() {
assert_eq!(es_bad("(a)\\1"), (ErrorCode::U001, 1, 4)); assert_eq!(es_bad("(?<n>a)\\k<n>"), (ErrorCode::U001, 1, 8));
}
#[test]
fn py_basic_structure() {
assert_eq!(py("(?P<n>a)"), "(group#1:n a)");
assert_eq!(py("(?i:a)b"), "(cat (flags a) b)");
assert_eq!(py("a{,3}"), "(rep 0 3 a)");
assert_eq!(py("a{,}"), "(rep 0 inf a)");
assert_eq!(py("a{}"), "(cat a { })");
assert_eq!(py("a{1"), "(cat a { 1)");
assert_eq!(py("\\Aab\\Z"), "(cat TextStart a b TextEnd)");
assert_eq!(py("(?#note)a"), "a");
assert_eq!(py("(?=a)(?<!b)"), "(cat (lookahead a) (lookbehind! b))");
assert_eq!(py("(?=a)*"), "(rep 0 inf (lookahead a))");
}
#[test]
fn py_classes() {
assert_eq!(py("[]a]"), "[5D 61]");
assert_eq!(py("[^]a]"), "[^5D 61]");
assert_eq!(py("[\\b]"), "[8]");
assert_eq!(py("[a-]"), "[61 2D]");
}
#[test]
fn py_escapes() {
assert_eq!(py("\\x41\\u0042\\U00000043"), "(cat A B C)");
assert_eq!(py("\\a\\f\\v"), "(cat U+0007 U+000C U+000B)");
assert_eq!(py("\\0"), "U+0000");
assert_eq!(py("\\101"), "A"); assert_eq!(py("\\."), ".");
assert_eq!(py("[\\1]"), "[1]");
}
#[test]
fn py_global_flags_apply_to_the_pattern() {
let p = Pattern::parse("(?im)abc", Dialect::Python, Flags::none()).expect("parse");
assert!(p.flags.i && p.flags.m && !p.flags.s);
assert_eq!(p.ast.to_string(), "(cat a b c)");
let p =
Pattern::parse("(?i)(?m)a", Dialect::Python, Flags::none()).expect("consecutive globals");
assert!(p.flags.i && p.flags.m);
}
#[test]
fn py_verbose_mode_skips_whitespace_and_comments() {
let x = fl(Dialect::Python, "x");
let p = Pattern::parse("a b # c (\n d [ ] ]", Dialect::Python, x).expect("parse");
assert_eq!(p.ast.to_string(), "(cat a b d [20] ])");
let p = Pattern::parse("a +", Dialect::Python, x).expect("space before quantifier");
assert_eq!(p.ast.to_string(), "(rep 1 inf a)");
let p = Pattern::parse("(?x) a b", Dialect::Python, Flags::none()).expect("inline x");
assert_eq!(p.ast.to_string(), "(cat a b)");
}
#[test]
fn py_errors_with_positions() {
assert_eq!(py_bad("(?P<n>a)(?P<n>b)"), (ErrorCode::P009, 1, 13));
assert_eq!(py_bad("(?<n>a)"), (ErrorCode::P001, 1, 1));
assert_eq!(py_bad("a(?i)"), (ErrorCode::P001, 1, 2));
assert_eq!(py_bad("(?)"), (ErrorCode::P001, 1, 1)); assert_eq!(py_bad("(?L)"), (ErrorCode::P003, 1, 3));
assert_eq!(py_bad("(?au)"), (ErrorCode::P003, 1, 1));
assert_eq!(py_bad("\\y"), (ErrorCode::P004, 1, 1));
assert_eq!(py_bad("\\xZ"), (ErrorCode::P004, 1, 1));
assert_eq!(py_bad("\\400"), (ErrorCode::P004, 1, 1));
assert_eq!(py_bad("a{2,1}"), (ErrorCode::P005, 1, 2));
assert_eq!(py_bad("a{4294967295}"), (ErrorCode::P005, 1, 2));
assert_eq!(py_bad("a**"), (ErrorCode::P005, 1, 3));
assert_eq!(py_bad("^*"), (ErrorCode::P005, 1, 2));
assert_eq!(py_bad("(?#x)*"), (ErrorCode::P005, 1, 6));
assert_eq!(py_bad("{1}"), (ErrorCode::P005, 1, 1));
assert_eq!(py_bad("[]"), (ErrorCode::P007, 1, 1));
assert_eq!(py_bad("(?i"), (ErrorCode::P007, 1, 1));
assert_eq!(py_bad("[b-a]"), (ErrorCode::P006, 1, 3));
assert_eq!(py_bad("[a-\\d]"), (ErrorCode::P006, 1, 3));
assert_eq!(py_bad("(a)\\2"), (ErrorCode::P004, 1, 4));
assert_eq!(py_bad("[\\A]"), (ErrorCode::P004, 1, 2));
}
#[test]
fn py_unsupported_constructs_have_their_own_codes() {
assert_eq!(py_bad("(a)\\1"), (ErrorCode::U001, 1, 4));
assert_eq!(py_bad("(?P<n>a)(?P=n)"), (ErrorCode::U001, 1, 9));
assert_eq!(py_bad("(?>a+)b"), (ErrorCode::U004, 1, 1)); assert_eq!(py_bad("a*+"), (ErrorCode::U004, 1, 3));
assert_eq!(py_bad("(a)(?(1)b|c)"), (ErrorCode::U003, 1, 4));
assert_eq!(py_bad("\\N{DIGIT ONE}"), (ErrorCode::U007, 1, 1));
}
#[test]
fn flags_per_dialect() {
let (f, notes) = Flags::from_letters(Dialect::Ecma, "gi").expect("gi");
assert!(f.i && !f.m);
assert_eq!(notes.len(), 1);
for bad in ["iz", "ii", "x", "a"] {
assert_eq!(
Flags::from_letters(Dialect::Ecma, bad).unwrap_err().kind,
ErrorCode::P003,
"{bad}"
);
}
assert_eq!(
Flags::from_letters(Dialect::Ecma, "v").unwrap_err().kind,
ErrorCode::U007
);
assert!(Flags::from_letters(Dialect::Python, "imsxa").is_ok());
assert_eq!(
Flags::from_letters(Dialect::Python, "g").unwrap_err().kind,
ErrorCode::P003
);
assert_eq!(
Dialect::from_name("perl").unwrap_err().kind,
ErrorCode::P002
);
assert_eq!(Dialect::from_name("go").expect("go"), Dialect::Go);
}
#[test]
fn literal_form() {
let p = Pattern::parse_literal("/a+/i", Dialect::Ecma).expect("literal");
assert!(p.flags.i);
assert_eq!(p.ast.to_string(), "(rep 1 inf a)");
let p = Pattern::parse_literal("/a/gi", Dialect::Ecma).expect("literal");
assert_eq!(p.notes.len(), 1);
let p = Pattern::parse_literal("/[/]/", Dialect::Ecma).expect("slash inside class");
assert_eq!(p.ast.to_string(), "[2F]");
for shape in ["a/", "//", "/a", "/a/1"] {
let e = Pattern::parse_literal(shape, Dialect::Ecma).unwrap_err();
assert_eq!(e.kind, ErrorCode::P010, "{shape}");
}
assert_eq!(
Pattern::parse_literal("/(/", Dialect::Ecma)
.unwrap_err()
.kind,
ErrorCode::P007
);
}
#[test]
fn deep_nesting_is_a_limit_error_not_a_stack_overflow() {
let p = format!("{}{}", "(".repeat(100_000), ")".repeat(100_000));
for d in [Dialect::Ecma, Dialect::Python] {
let e = Pattern::parse(&p, d, Flags::none()).unwrap_err();
assert_eq!(e.kind, ErrorCode::L006);
}
}
#[test]
fn nesting_at_the_limit_parses_and_one_more_fails() {
let lim = ParseLimits {
max_depth: 10,
..ParseLimits::default()
};
let n = |k: usize| format!("{}a{}", "(".repeat(k), ")".repeat(k));
assert!(Pattern::parse_with(&n(9), Dialect::Ecma, Flags::none(), &lim).is_ok());
let e = Pattern::parse_with(&n(10), Dialect::Ecma, Flags::none(), &lim).unwrap_err();
assert_eq!(e.kind, ErrorCode::L006);
}
#[test]
fn counted_repetition_is_not_expanded_by_the_parser() {
assert_eq!(
es("(a{1000}){1000}"),
"(rep 1000 1000 (group#1 (rep 1000 1000 a)))"
);
}
#[test]
fn pattern_length_limit() {
let lim = ParseLimits {
max_length: 5,
..ParseLimits::default()
};
assert!(Pattern::parse_with("abcde", Dialect::Ecma, Flags::none(), &lim).is_ok());
let e = Pattern::parse_with("abcdef", Dialect::Ecma, Flags::none(), &lim).unwrap_err();
assert_eq!(e.kind, ErrorCode::L005);
}
fn go(p: &str) -> String {
tree(Dialect::Go, "", p)
}
fn rs(p: &str) -> String {
tree(Dialect::Rust, "", p)
}
fn pc(p: &str) -> String {
tree(Dialect::Pcre2, "u", p)
}
fn go_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Go, "", p)
}
fn rs_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Rust, "", p)
}
fn pc_bad(p: &str) -> (ErrorCode, usize, usize) {
bad(Dialect::Pcre2, "", p)
}
#[test]
fn go_structure_and_dialect_rules() {
assert_eq!(go("(a|b)*"), "(rep 0 inf (group#1 (alt a b)))");
assert_eq!(go("a{2,3}?"), "(rep? 2 3 a)");
assert_eq!(go("a{"), "(cat a {)"); assert_eq!(go("\\Qa.b\\E"), "(cat a . b)");
assert_eq!(go("[[:alpha:]]"), "[41-5A 61-7A]");
assert_eq!(go("(?P<n>a)(?<m>b)"), "(cat (group#1:n a) (group#2:m b))");
assert_eq!(
go("^*"),
"(rep 0 inf Assert(LineStart))".replace("Assert(LineStart)", "LineStart")
);
assert_eq!(go_bad("a**").0, ErrorCode::P005);
assert_eq!(go_bad("a{1001}").0, ErrorCode::P005);
assert_eq!(go_bad("(?=a)").0, ErrorCode::P001);
assert_eq!(go_bad("\\1").0, ErrorCode::P004);
assert_eq!(go_bad("(a").0, ErrorCode::P007);
assert_eq!(go_bad("[z-a]").0, ErrorCode::P006);
assert_eq!(go_bad("\\p{Foo}").0, ErrorCode::P008);
assert_eq!(go_bad("(?u)a").0, ErrorCode::P001);
}
#[test]
fn rust_structure_and_dialect_rules() {
assert_eq!(rs("a**"), "(rep 0 inf (rep 0 inf a))"); assert_eq!(rs("a{ 2 , 3 }"), "(rep 2 3 a)");
assert_eq!(rs("\\p{Greek}"), "[\\p{Script:Greek}]");
assert_eq!(rs("\\u{41}\\x41\\x{41}"), "(cat A A A)");
let set = Pattern::parse("[a-z&&[^k]]", Dialect::Rust, Flags::none()).expect("class set");
assert!(set.ast.to_string().starts_with("[set "));
assert_eq!(rs("(?x) a b # c\n"), "(cat a b)");
assert_eq!(rs_bad("a{").0, ErrorCode::P005);
assert_eq!(rs_bad("\\1").0, ErrorCode::P004);
assert_eq!(rs_bad("(?ii)a").0, ErrorCode::P003);
assert_eq!(rs_bad("(?P<n>a)(?P<n>b)").0, ErrorCode::P009);
assert_eq!(rs_bad("[\\d-z]").0, ErrorCode::P006);
assert_eq!(rs_bad("(?-u).").0, ErrorCode::P001); assert_eq!(rs_bad("\\Q.\\E").0, ErrorCode::P004);
}
#[test]
fn pcre2_structure_and_unsupported_constructs() {
assert_eq!(pc("a{,2}"), "(rep 0 2 a)");
assert_eq!(pc("\\h"), "[9 20 A0 1680 180E 2000-200A 202F 205F 3000]");
assert_eq!(pc("(?i)a"), "(flags a)");
assert_eq!(pc("(?<n>a)"), "(group#1:n a)");
assert_eq!(pc("(?=a)b"), "(cat (lookahead a) b)");
assert_eq!(pc_bad("(a)\\1").0, ErrorCode::U001);
assert_eq!(pc_bad("(?<n>a)\\k<n>").0, ErrorCode::U001);
assert_eq!(pc_bad("(?R)").0, ErrorCode::U002);
assert_eq!(pc_bad("(a)(?1)").0, ErrorCode::U002);
assert_eq!(pc_bad("(a)(?(1)b|c)").0, ErrorCode::U003);
assert_eq!(pc_bad("(?>a+)b").0, ErrorCode::U004);
assert_eq!(pc_bad("a*+").0, ErrorCode::U004);
assert_eq!(pc_bad("a(?C)").0, ErrorCode::U006);
assert_eq!(pc_bad("(?|a|b)").0, ErrorCode::U007);
assert_eq!(pc_bad("(*FAIL)").0, ErrorCode::U007);
assert_eq!(pc_bad("a\\Kb").0, ErrorCode::U007);
assert_eq!(pc_bad("^*").0, ErrorCode::P005);
assert_eq!(pc_bad("[a-\\d]").0, ErrorCode::P006);
assert_eq!(pc_bad("(?u)a").0, ErrorCode::P001);
assert_eq!(pc_bad("(?<n>a)(?<n>b)").0, ErrorCode::P009);
}
#[test]
fn pcre2_without_u_parses_utf8_bytes() {
let p = Pattern::parse("é", Dialect::Pcre2, Flags::none()).expect("parse");
assert_eq!(p.ast.to_string(), "(cat U+00C3 U+00A9)");
let p = Pattern::parse("é", Dialect::Pcre2, fl(Dialect::Pcre2, "u")).expect("parse");
assert_eq!(p.ast.to_string(), "U+00E9");
}
#[test]
fn caret_rendering_for_unbalanced_class() {
let e = Pattern::parse("[", Dialect::Ecma, Flags::none()).unwrap_err();
assert_eq!(e.kind, ErrorCode::P007);
assert!(e.render("[").ends_with("\n [\n ^"));
}
#[test]
fn group_counts_and_names() {
let p = Pattern::parse("(a)(?:b)(?<x>c)", Dialect::Ecma, Flags::none()).expect("parse");
assert_eq!(p.group_count, 2);
assert_eq!(p.group_names, vec!["x".to_string()]);
}
#[test]
fn es_unicode_property_escapes() {
assert_eq!(esu("\\p{L}"), "[\\p{GeneralCategory:L}]");
assert_eq!(esu("\\P{Lu}"), "[\\P{GeneralCategory:Lu}]");
assert_eq!(esu("\\p{Letter}"), "[\\p{GeneralCategory:L}]");
assert_eq!(esu("\\p{Script=Greek}"), "[\\p{Script:Greek}]");
assert_eq!(esu("\\p{sc=Grek}"), "[\\p{Script:Greek}]");
assert_eq!(esu("\\p{scx=Greek}"), "[\\p{ScriptExtensions:Greek}]");
assert_eq!(esu("\\p{Alpha}"), "[\\p{Binary:Alphabetic}]");
assert_eq!(esu("[\\p{L}\\d]"), "[\\p{GeneralCategory:L} \\d]");
for bad in [
"\\p{Greek}",
"\\p{lu}",
"\\p{Foo}",
"\\p{Script=Foo}",
"\\p{Foo=Bar}",
] {
assert_eq!(esu_bad(bad).0, ErrorCode::P008, "{bad}");
}
for bad in ["\\p", "\\p{L", "\\pL"] {
assert_eq!(esu_bad(bad).0, ErrorCode::P004, "{bad}");
}
assert_eq!(es("\\p"), "p");
let e = Pattern::parse("\\p{Greeek}", Dialect::Ecma, fl(Dialect::Ecma, "u")).unwrap_err();
assert!(e.detail.unwrap().contains("did you mean"));
assert_eq!(py_bad("\\p{L}").0, ErrorCode::P004);
}