jsonschema-regex 0.60.2

ECMA 262 regex translation for the jsonschema crate
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
//! ECMA-262 regular expression syntax check.
//!
//! The grammar is the `Pattern[+UnicodeMode, +NamedCaptureGroups]` production of ECMA-262,
//! including the pattern modifiers of ES2025, widened with the identity escapes that patterns
//! without the `u` flag allow: a backslash before any character that is not alphanumeric. Drafts 4
//! to 2019-09 cite the ES5.1 dialect, which has no `u` flag. Unicode property names and values are
//! checked for shape only, not against the Unicode tables.

use std::{borrow::Cow, cmp::Ordering};

/// Whether `pattern` is a syntactically valid ECMA-262 regular expression under the `u` flag, or
/// differs from one only by escaping non-alphanumeric characters.
#[must_use]
pub fn is_valid_ecma_regex(pattern: &str) -> bool {
    Parser::new(pattern).parse().is_ok()
}

struct SyntaxError;

type Parsed<T> = Result<T, SyntaxError>;

/// What the cursor follows, which decides whether a quantifier may appear.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Previous {
    /// Start of the pattern, an alternative, or a group.
    Nothing,
    Atom,
    Assertion,
    Quantifier,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum GroupKind {
    Capturing,
    NonCapturing,
    Lookaround,
}

/// An open group. Group names are tracked per alternative: two groups may share a name when
/// they can never both take part in one match, i.e. when they sit in different alternatives of
/// some enclosing disjunction.
struct Frame<'a> {
    kind: GroupKind,
    /// Names declared in the alternative being parsed, including those of groups closed inside it.
    current_alternative: Vec<Cow<'a, str>>,
    /// Names declared in the alternatives already finished.
    finished_alternatives: Vec<Cow<'a, str>>,
}

impl Frame<'_> {
    fn new(kind: GroupKind) -> Self {
        Self {
            kind,
            current_alternative: Vec::new(),
            finished_alternatives: Vec::new(),
        }
    }
}

struct Parser<'a> {
    pattern: &'a str,
    position: usize,
    previous: Previous,
    /// The pattern itself is the outermost frame, so this is never empty.
    frames: Vec<Frame<'a>>,
    capturing_groups: u64,
    /// The largest `\N` seen, checked against `capturing_groups` once the whole pattern is read.
    largest_backreference: u64,
    /// Every `\k<name>` seen, checked against the declared names once the whole pattern is read.
    referenced_names: Vec<Cow<'a, str>>,
    declared_names: Vec<Cow<'a, str>>,
}

impl<'a> Parser<'a> {
    fn new(pattern: &'a str) -> Self {
        Self {
            pattern,
            position: 0,
            previous: Previous::Nothing,
            frames: vec![Frame::new(GroupKind::NonCapturing)],
            capturing_groups: 0,
            largest_backreference: 0,
            referenced_names: Vec::new(),
            declared_names: Vec::new(),
        }
    }

    fn peek(&self) -> Option<char> {
        self.pattern[self.position..].chars().next()
    }

    fn peek_second(&self) -> Option<char> {
        self.pattern[self.position..].chars().nth(1)
    }

    fn bump(&mut self) -> Parsed<char> {
        let c = self.peek().ok_or(SyntaxError)?;
        self.position += c.len_utf8();
        Ok(c)
    }

    fn eat(&mut self, expected: char) -> bool {
        if self.peek() == Some(expected) {
            self.position += expected.len_utf8();
            true
        } else {
            false
        }
    }

    fn expect(&mut self, expected: char) -> Parsed<()> {
        if self.eat(expected) {
            Ok(())
        } else {
            Err(SyntaxError)
        }
    }

    fn current_frame(&mut self) -> &mut Frame<'a> {
        self.frames
            .last_mut()
            .expect("the outermost frame is never popped")
    }

    fn parse(mut self) -> Parsed<()> {
        while let Some(c) = self.peek() {
            self.position += c.len_utf8();
            match c {
                '|' => {
                    let frame = self.current_frame();
                    frame
                        .finished_alternatives
                        .append(&mut frame.current_alternative);
                    self.previous = Previous::Nothing;
                }
                '(' => {
                    self.open_group()?;
                    self.previous = Previous::Nothing;
                }
                ')' => self.previous = self.close_group()?,
                '*' | '+' | '?' => self.quantified()?,
                '{' => {
                    self.braced_quantifier()?;
                    self.quantified()?;
                }
                '}' | ']' => return Err(SyntaxError),
                '^' | '$' => self.previous = Previous::Assertion,
                '\\' => self.previous = self.atom_escape()?,
                '[' => {
                    self.class()?;
                    self.previous = Previous::Atom;
                }
                _ => self.previous = Previous::Atom,
            }
        }
        if self.frames.len() != 1 || self.largest_backreference > self.capturing_groups {
            return Err(SyntaxError);
        }
        if self
            .referenced_names
            .iter()
            .any(|name| !self.declared_names.contains(name))
        {
            return Err(SyntaxError);
        }
        Ok(())
    }

    /// A quantifier was consumed: it must follow an atom and may take a lazy marker.
    fn quantified(&mut self) -> Parsed<()> {
        if self.previous != Previous::Atom {
            return Err(SyntaxError);
        }
        self.eat('?');
        self.previous = Previous::Quantifier;
        Ok(())
    }

    /// `{n}`, `{n,}` or `{n,m}` with `n <= m`; the opening brace is consumed.
    fn braced_quantifier(&mut self) -> Parsed<()> {
        let lower = self.decimal_digits()?;
        if self.eat(',') && self.peek() != Some('}') {
            let upper = self.decimal_digits()?;
            if compare_decimal(lower, upper) == Ordering::Greater {
                return Err(SyntaxError);
            }
        }
        self.expect('}')
    }

    fn decimal_digits(&mut self) -> Parsed<&'a str> {
        let start = self.position;
        while self.peek().is_some_and(|c| c.is_ascii_digit()) {
            self.position += 1;
        }
        if self.position == start {
            return Err(SyntaxError);
        }
        Ok(&self.pattern[start..self.position])
    }

    /// The opening paren is consumed.
    fn open_group(&mut self) -> Parsed<()> {
        let kind = if self.eat('?') {
            match self.bump()? {
                ':' => GroupKind::NonCapturing,
                '=' | '!' => GroupKind::Lookaround,
                '<' => {
                    if matches!(self.peek(), Some('=' | '!')) {
                        self.position += 1;
                        GroupKind::Lookaround
                    } else {
                        let name = self.group_name()?;
                        self.declare_name(name)?;
                        self.capturing_groups += 1;
                        GroupKind::Capturing
                    }
                }
                first => {
                    self.modifiers(first)?;
                    GroupKind::NonCapturing
                }
            }
        } else {
            self.capturing_groups += 1;
            GroupKind::Capturing
        };
        self.frames.push(Frame::new(kind));
        Ok(())
    }

    /// `(?ims-ims:` with the first character after `?` consumed: at least one flag, none repeated.
    fn modifiers(&mut self, first: char) -> Parsed<()> {
        let mut seen = [false; 3];
        let mut flags = 0;
        let mut dashes = 0;
        let mut c = first;
        while c != ':' {
            match c {
                'i' | 'm' | 's' => {
                    let slot = &mut seen[match c {
                        'i' => 0,
                        'm' => 1,
                        _ => 2,
                    }];
                    if std::mem::replace(slot, true) {
                        return Err(SyntaxError);
                    }
                    flags += 1;
                }
                '-' if dashes == 0 => dashes = 1,
                _ => return Err(SyntaxError),
            }
            c = self.bump()?;
        }
        if flags == 0 {
            return Err(SyntaxError);
        }
        Ok(())
    }

    /// The closing paren is consumed. Returns what the closed group counts as for a quantifier.
    fn close_group(&mut self) -> Parsed<Previous> {
        if self.frames.len() == 1 {
            return Err(SyntaxError);
        }
        let mut frame = self.frames.pop().expect("a nested frame is open");
        let parent = self.current_frame();
        parent
            .current_alternative
            .append(&mut frame.current_alternative);
        parent
            .current_alternative
            .append(&mut frame.finished_alternatives);
        Ok(match frame.kind {
            GroupKind::Lookaround => Previous::Assertion,
            GroupKind::Capturing | GroupKind::NonCapturing => Previous::Atom,
        })
    }

    fn declare_name(&mut self, name: Cow<'a, str>) -> Parsed<()> {
        if self
            .frames
            .iter()
            .any(|frame| frame.current_alternative.contains(&name))
        {
            return Err(SyntaxError);
        }
        self.declared_names.push(name.clone());
        self.current_frame().current_alternative.push(name);
        Ok(())
    }

    /// `name>` of a group name; the opening angle bracket is consumed.
    fn group_name(&mut self) -> Parsed<Cow<'a, str>> {
        let start = self.position;
        let mut decoded: Option<String> = None;
        let mut first = true;
        loop {
            let char_start = self.position;
            let c = match self.bump()? {
                '>' => break,
                '\\' => {
                    self.expect('u')?;
                    let c = char::from_u32(self.unicode_escape()?).ok_or(SyntaxError)?;
                    decoded.get_or_insert_with(|| self.pattern[start..char_start].to_owned());
                    c
                }
                c => c,
            };
            let allowed = if first {
                is_identifier_start(c)
            } else {
                is_identifier_part(c)
            };
            if !allowed {
                return Err(SyntaxError);
            }
            if let Some(name) = &mut decoded {
                name.push(c);
            }
            first = false;
        }
        if first {
            return Err(SyntaxError);
        }
        Ok(match decoded {
            Some(name) => Cow::Owned(name),
            None => Cow::Borrowed(&self.pattern[start..self.position - 1]),
        })
    }

    /// An escape outside a class; the backslash is consumed.
    fn atom_escape(&mut self) -> Parsed<Previous> {
        match self.bump()? {
            'b' | 'B' => return Ok(Previous::Assertion),
            'd' | 'D' | 's' | 'S' | 'w' | 'W' => {}
            'p' | 'P' => self.unicode_property()?,
            'k' => {
                self.expect('<')?;
                let name = self.group_name()?;
                self.referenced_names.push(name);
            }
            '1'..='9' => {
                self.position -= 1;
                let number = self.decimal_digits()?.parse().unwrap_or(u64::MAX);
                self.largest_backreference = self.largest_backreference.max(number);
            }
            c => {
                self.character_escape(c)?;
            }
        }
        Ok(Previous::Atom)
    }

    /// `CharacterEscape` with its first character consumed; returns the code point.
    fn character_escape(&mut self, c: char) -> Parsed<u32> {
        Ok(match c {
            'f' => 0x0C,
            'n' => 0x0A,
            'r' => 0x0D,
            't' => 0x09,
            'v' => 0x0B,
            'c' => {
                let letter = self.bump()?;
                if !letter.is_ascii_alphabetic() {
                    return Err(SyntaxError);
                }
                letter as u32 % 32
            }
            '0' => {
                if self.peek().is_some_and(|c| c.is_ascii_digit()) {
                    return Err(SyntaxError);
                }
                0
            }
            'x' => {
                let high = self.hex_digit()?;
                let low = self.hex_digit()?;
                high * 16 + low
            }
            'u' => self.unicode_escape()?,
            c if !c.is_alphanumeric() => c as u32,
            _ => return Err(SyntaxError),
        })
    }

    fn hex_digit(&mut self) -> Parsed<u32> {
        self.bump()?.to_digit(16).ok_or(SyntaxError)
    }

    /// `RegExpUnicodeEscapeSequence` after `\u`; a lead surrogate joins a following trail one.
    fn unicode_escape(&mut self) -> Parsed<u32> {
        if self.eat('{') {
            let mut value: u32 = 0;
            let mut digits = 0;
            while !self.eat('}') {
                value = value.saturating_mul(16).saturating_add(self.hex_digit()?);
                digits += 1;
            }
            if digits == 0 || value > 0x10_FFFF {
                return Err(SyntaxError);
            }
            return Ok(value);
        }
        let lead = self.hex4()?;
        if (0xD800..=0xDBFF).contains(&lead) && self.pattern[self.position..].starts_with("\\u") {
            let position = self.position;
            self.position += 2;
            match self.hex4() {
                Ok(trail) if (0xDC00..=0xDFFF).contains(&trail) => {
                    return Ok(0x10000 + ((lead - 0xD800) << 10) + (trail - 0xDC00));
                }
                _ => self.position = position,
            }
        }
        Ok(lead)
    }

    fn hex4(&mut self) -> Parsed<u32> {
        let mut value = 0;
        for _ in 0..4 {
            value = value * 16 + self.hex_digit()?;
        }
        Ok(value)
    }

    /// `{Name}` or `{Name=Value}` after `\p` or `\P`.
    fn unicode_property(&mut self) -> Parsed<()> {
        self.expect('{')?;
        self.property_word()?;
        if self.eat('=') {
            self.property_word()?;
        }
        self.expect('}')
    }

    fn property_word(&mut self) -> Parsed<()> {
        let start = self.position;
        while self
            .peek()
            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
        {
            self.position += 1;
        }
        if self.position == start {
            return Err(SyntaxError);
        }
        Ok(())
    }

    /// A character class; the opening bracket is consumed.
    fn class(&mut self) -> Parsed<()> {
        self.eat('^');
        loop {
            if self.eat(']') {
                return Ok(());
            }
            let start = self.class_atom()?;
            // A dash right before the closing bracket is a literal.
            if self.peek() == Some('-') && self.peek_second() != Some(']') {
                self.position += 1;
                let end = self.class_atom()?;
                match (start, end) {
                    (Some(start), Some(end)) if start <= end => {}
                    _ => return Err(SyntaxError),
                }
            }
        }
    }

    /// One class atom: `Some` code point, or `None` for a class escape such as `\d`.
    fn class_atom(&mut self) -> Parsed<Option<u32>> {
        let c = self.bump()?;
        if c != '\\' {
            return Ok(Some(c as u32));
        }
        Ok(match self.bump()? {
            'b' => Some(0x08),
            '-' => Some('-' as u32),
            'd' | 'D' | 's' | 'S' | 'w' | 'W' => None,
            'p' | 'P' => {
                self.unicode_property()?;
                None
            }
            c => Some(self.character_escape(c)?),
        })
    }
}

/// `ID_Start` plus `$` and `_`, approximated by the Alphabetic property.
fn is_identifier_start(c: char) -> bool {
    c.is_alphabetic() || matches!(c, '$' | '_')
}

/// `ID_Continue` plus `$`, `_`, ZWNJ and ZWJ, approximated by the Alphabetic and Numeric properties.
fn is_identifier_part(c: char) -> bool {
    c.is_alphanumeric() || matches!(c, '$' | '_' | '\u{200C}' | '\u{200D}')
}

/// Compares two digit strings by value.
fn compare_decimal(left: &str, right: &str) -> Ordering {
    let left = left.trim_start_matches('0');
    let right = right.trim_start_matches('0');
    left.len().cmp(&right.len()).then_with(|| left.cmp(right))
}

#[cfg(test)]
mod tests {
    use super::is_valid_ecma_regex;
    use test_case::test_case;

    #[test_case(""; "empty pattern")]
    #[test_case("|"; "empty alternatives")]
    #[test_case("a|"; "trailing empty alternative")]
    #[test_case("()"; "empty group")]
    #[test_case("([abc])+\\s+$"; "suite valid pattern")]
    #[test_case("(?<name>x)"; "named group")]
    #[test_case("(?<n>a)\\k<n>"; "named backreference")]
    #[test_case("(?<n>a)|(?<n>b)"; "duplicate name in different alternatives")]
    #[test_case("(?<\\u0061>x)\\k<a>"; "escaped group name")]
    #[test_case("(?<$_>x)"; "dollar and underscore in group name")]
    #[test_case("(?<Ï€>x)"; "non-ascii group name")]
    #[test_case("(?<=a+)b"; "variable width lookbehind")]
    #[test_case("(?<!a)b"; "negative lookbehind")]
    #[test_case("(?=a)"; "lookahead")]
    #[test_case("(?!a)"; "negative lookahead")]
    #[test_case("(?:a)"; "non-capturing group")]
    #[test_case("(?i:a)"; "inline flag modifier group")]
    #[test_case("(?-i:a)"; "removed flag modifier group")]
    #[test_case("(?im-s:a)"; "mixed flag modifier group")]
    #[test_case("(a)\\1"; "backreference")]
    #[test_case("\\1(a)"; "forward backreference")]
    #[test_case("(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)\\10"; "two digit backreference")]
    #[test_case("[]"; "empty class")]
    #[test_case("[^]"; "negated empty class")]
    #[test_case("[a-]"; "trailing dash in class")]
    #[test_case("[-a]"; "leading dash in class")]
    #[test_case("[a-z-9]"; "dash after range")]
    #[test_case("[---]"; "dash range")]
    #[test_case("[\\d-]"; "class escape before trailing dash")]
    #[test_case("[\\b]"; "backspace in class")]
    #[test_case("[\\-]"; "escaped dash in class")]
    #[test_case("[\\]]"; "escaped bracket in class")]
    #[test_case("[\\0]"; "nul in class")]
    #[test_case("[\\cA]"; "control escape in class")]
    #[test_case("[\\p{L}]"; "property in class")]
    #[test_case("[{}]"; "braces in class")]
    #[test_case("[a-z&&^b]"; "class intersection syntax is literal")]
    #[test_case("[a-z--b]"; "adjacent ranges with doubled hyphen")]
    #[test_case("[a~~b]"; "class symmetric difference syntax is literal")]
    #[test_case("\\cA"; "control escape")]
    #[test_case("\\cz"; "lowercase control escape")]
    #[test_case("\\0"; "nul escape")]
    #[test_case("\\x41"; "hex escape")]
    #[test_case("\\u0041"; "unicode escape")]
    #[test_case("\\u{1F600}"; "braced unicode escape")]
    #[test_case("\\u{0000000041}"; "braced unicode escape with leading zeros")]
    #[test_case("\\uD83D\\uDE00"; "surrogate pair escape")]
    #[test_case("\\uD83D"; "lone surrogate escape")]
    #[test_case("[\\uD83D\\uDE00-\\u{1F64F}]"; "surrogate pair range")]
    #[test_case("\\p{L}"; "property escape")]
    #[test_case("\\P{Lu}"; "negated property escape")]
    #[test_case("\\p{Script=Greek}"; "property name and value")]
    #[test_case("\\p{Letter}"; "long property name")]
    #[test_case("\\d\\D\\s\\S\\w\\W"; "class escapes")]
    #[test_case("\\f\\n\\r\\t\\v"; "control escapes")]
    #[test_case("\\/\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|"; "identity escapes")]
    #[test_case("\\b\\B"; "word boundaries")]
    #[test_case("^\\-?\\d+$"; "escaped dash outside class")]
    #[test_case("^(\\-?\\d+(\\.\\d+)?),\\s*(\\-?\\d+(\\.\\d+)?)$"; "escaped dashes in groups")]
    #[test_case("\\:"; "escaped colon")]
    #[test_case("\\ "; "escaped space")]
    #[test_case("\\@"; "escaped at sign")]
    #[test_case("\\#\\%\\&\\'\\\"\\,\\;\\<\\=\\>\\!\\~\\`"; "escaped punctuation")]
    #[test_case("\\_"; "escaped underscore")]
    #[test_case("\\§"; "escaped non-ascii symbol")]
    #[test_case("[\\:\\@\\ \\_]"; "escaped punctuation in class")]
    #[test_case("[\\!-\\/]"; "range of escaped punctuation")]
    #[test_case("a*?b+?c??d{2}?e{2,}?f{2,3}?"; "lazy quantifiers")]
    #[test_case("a{2}"; "exact count")]
    #[test_case("a{2,}"; "open count")]
    #[test_case("a{2,3}"; "bounded count")]
    #[test_case("a{0,0}"; "zero count")]
    #[test_case("a{99999999999999999999,99999999999999999999}"; "huge count")]
    #[test_case("a{010,10}"; "leading zero count")]
    #[test_case("^$"; "anchors")]
    #[test_case("."; "dot")]
    #[test_case("\u{1F600}"; "supplementary plane literal")]
    #[test_case("\n"; "newline literal")]
    #[test_case(","; "comma literal")]
    #[test_case("-"; "dash literal")]
    fn valid(pattern: &str) {
        assert!(is_valid_ecma_regex(pattern));
    }

    #[test_case("^(abc]"; "suite unclosed group")]
    #[test_case("\\a"; "identity escape of a letter")]
    #[test_case("\\é"; "identity escape of a non-ascii letter")]
    #[test_case("\\Ù£"; "identity escape of a non-ascii digit")]
    #[test_case("\\"; "trailing backslash")]
    #[test_case("(?P<name>x)"; "python named group")]
    #[test_case("(?P<n>a)(?P=n)"; "python named backreference")]
    #[test_case("(?#comment)a"; "inline comment")]
    #[test_case("(?i)abc"; "global inline flag")]
    #[test_case("(?ims)abc"; "global inline flags")]
    #[test_case("(?x) a"; "extended flag")]
    #[test_case("(?-:a)"; "empty modifiers")]
    #[test_case("(?ii:a)"; "duplicate modifier")]
    #[test_case("(?i-i:a)"; "modifier added and removed")]
    #[test_case("(?u:a)"; "unknown modifier")]
    #[test_case("(?'n'a)"; "quoted group name")]
    #[test_case("(?<>x)"; "empty group name")]
    #[test_case("(?<1a>x)"; "digit leading group name")]
    #[test_case("(?<a-b>x)"; "dash in group name")]
    #[test_case("(?<a>x)(?<a>y)"; "duplicate group name")]
    #[test_case("(?<a>x)|(?:(?<a>y)(?<a>z))"; "duplicate group name in one alternative")]
    #[test_case("(?<a>x)(?:y|(?<a>z))"; "duplicate group name through nested alternative")]
    #[test_case("\\k<nope>"; "backreference to missing name")]
    #[test_case("(?<n>a)\\k"; "bare k escape")]
    #[test_case("(?<n>a)\\k<n"; "unclosed name reference")]
    #[test_case("\\2(a)"; "backreference beyond group count")]
    #[test_case("(a)\\10"; "two digit backreference beyond group count")]
    #[test_case("\\00"; "octal escape")]
    #[test_case("\\01"; "octal escape with digit")]
    #[test_case("\\x4"; "short hex escape")]
    #[test_case("\\xZZ"; "non hex escape")]
    #[test_case("\\u123"; "short unicode escape")]
    #[test_case("\\u{}"; "empty braced unicode escape")]
    #[test_case("\\u{110000}"; "out of range braced unicode escape")]
    #[test_case("\\u{41"; "unclosed braced unicode escape")]
    #[test_case("\\c1"; "control escape with digit")]
    #[test_case("\\c"; "bare control escape")]
    #[test_case("\\p"; "bare property escape")]
    #[test_case("\\pL"; "property escape without braces")]
    #[test_case("\\p{}"; "empty property escape")]
    #[test_case("\\p{L"; "unclosed property escape")]
    #[test_case("\\p{L=}"; "property escape with empty value")]
    #[test_case("\\p{a b}"; "property escape with space")]
    #[test_case("\\z\\Z\\A"; "anchor escapes from other dialects")]
    #[test_case("["; "unclosed class")]
    #[test_case("[a"; "unclosed class with content")]
    #[test_case("[\\"; "unclosed class with escape")]
    #[test_case("]"; "lone closing bracket")]
    #[test_case("}"; "lone closing brace")]
    #[test_case("{"; "lone opening brace")]
    #[test_case("a{"; "unclosed quantifier")]
    #[test_case("a{,5}"; "quantifier without lower bound")]
    #[test_case("a{2,1}"; "inverted quantifier bounds")]
    #[test_case("a{2"; "unclosed quantifier with digit")]
    #[test_case("a{2,"; "unclosed open quantifier")]
    #[test_case("a{x}"; "non numeric quantifier")]
    #[test_case("[z-a]"; "inverted range")]
    #[test_case("[\\w-a]"; "class escape as range start")]
    #[test_case("[a-\\d]"; "class escape as range end")]
    #[test_case("[\\B]"; "non word boundary in class")]
    #[test_case("[\\1]"; "backreference in class")]
    #[test_case("[\\k<n>]"; "named backreference in class")]
    #[test_case("[\\c1]"; "control escape with digit in class")]
    #[test_case("[\\a]"; "identity escape of a letter in class")]
    #[test_case("[a-z&&[^aeiou]]"; "class intersection syntax")]
    #[test_case("*a"; "leading quantifier")]
    #[test_case("a**"; "double quantifier")]
    #[test_case("a*??"; "double lazy marker")]
    #[test_case("x{2}{3}"; "double counted quantifier")]
    #[test_case("a|*"; "quantifier after alternation")]
    #[test_case("(*)"; "quantifier after group open")]
    #[test_case("^*"; "quantified caret")]
    #[test_case("$+"; "quantified dollar")]
    #[test_case("\\b*"; "quantified word boundary")]
    #[test_case("(?=a)*"; "quantified lookahead")]
    #[test_case("(?<=a)?"; "quantified lookbehind")]
    #[test_case(")"; "lone closing paren")]
    #[test_case("(a"; "unclosed group")]
    #[test_case("(?"; "unclosed group opener")]
    #[test_case("(?<"; "unclosed named group opener")]
    #[test_case("(?<n"; "unclosed group name")]
    #[test_case("(?<n>"; "unclosed named group")]
    fn invalid(pattern: &str) {
        assert!(!is_valid_ecma_regex(pattern));
    }

    #[test]
    fn accepts_every_escape_the_translation_compiles() {
        for c in (' '..='~').filter(|c| !c.is_ascii_alphanumeric()) {
            for pattern in [format!("\\{c}"), format!("[\\{c}]")] {
                if crate::to_rust_regex(&pattern).is_ok_and(|rust| regex::Regex::new(&rust).is_ok())
                {
                    assert!(is_valid_ecma_regex(&pattern), "{pattern}");
                }
            }
        }
    }
}