fix(regular_expression): reject incomplete Unicode atom escapes (#28448)

Reject a consumed backslash without a valid Unicode AtomEscape,
including trailing escapes, and report the invalid escape at its source
span.

Validated with scoped Rust checks, full conformance, parser bindings,
the linter suite.

AI-assisted (Codex).
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 11:37:30 +08:00
1 parent fc0eed3404
commit cb64638ed4
8 files changed
+79 -36

No files matched your search

@@ -145,19 +145,19 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Check for unbalanced brackets or invalid escape sequences
⚠ eslint(no-invalid-regexp): Invalid regular expression: Could not parse the entire pattern
╭─[no_invalid_regexp.tsx:1:15]
⚠ eslint(no-invalid-regexp): Invalid regular expression: Invalid escape sequence
╭─[no_invalid_regexp.tsx:1:13]
1 │ new RegExp('\\a', 'u');
· ▲
· ──
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
⚠ eslint(no-invalid-regexp): Invalid regular expression: Could not parse the entire pattern
╭─[no_invalid_regexp.tsx:1:15]
⚠ eslint(no-invalid-regexp): Invalid regular expression: Invalid escape sequence
╭─[no_invalid_regexp.tsx:1:13]
1 │ new RegExp('\\a', 'u');
· ▲
· ──
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
⚠ eslint(no-invalid-regexp): Invalid regular expression: Invalid braced quantifier
╭─[no_invalid_regexp.tsx:1:15]
@@ -75,6 +75,13 @@ pub fn invalid_extended_atom_escape(span: Span) -> OxcDiagnostic {
.with_help("Use a valid escape sequence or remove the backslash")
}
#[cold]
pub fn invalid_atom_escape(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error(format!("{PREFIX} Invalid escape sequence"))
.with_label(span)
.with_help("Use a valid escape sequence or escape the backslash with `\\\\`")
}
#[cold]
pub fn invalid_braced_quantifier(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error(format!("{PREFIX} Invalid braced quantifier"))
@@ -12,6 +12,40 @@ mod test {
use crate::{ConstructorParser, LiteralParser, Options, ast::Term};
#[test]
fn trailing_escape() {
let allocator = Allocator::default();
for flags in ["", "u", "v"] {
for pattern in [r"\", r"a\", r"a|\"] {
assert!(
LiteralParser::new(&allocator, pattern, Some(flags), Options::default())
.parse()
.is_err(),
"/{pattern}/{flags}"
);
}
let flags_source = format!("\"{flags}\"");
for source in [r#""\\""#, r#""a\\""#, r"`a\\`"] {
assert!(
ConstructorParser::new(
&allocator,
source,
Some(&flags_source),
Options::default()
)
.parse()
.is_err(),
"new RegExp({source}, {flags_source})"
);
}
assert!(
LiteralParser::new(&allocator, r"\\", Some(flags), Options::default())
.parse()
.is_ok()
);
}
}
#[test]
fn escaped_group_names() {
let allocator = Allocator::default();
@@ -359,10 +359,12 @@ impl<'a> PatternParser<'a> {
}
// \ AtomEscape[?UnicodeMode, ?NamedCaptureGroups]
if self.reader.eat('\\')
&& let Some(atom_escape) = self.parse_atom_escape(span_start)?
{
return Ok(Some(atom_escape));
if self.reader.eat('\\') {
return self.parse_atom_escape(span_start)?.map(Some).ok_or_else(|| {
diagnostics::invalid_atom_escape(
self.span_factory.create(span_start, self.reader.offset()),
)
});
}
// CharacterClass[?UnicodeMode, ?UnicodeSetsMode]
@@ -283,12 +283,12 @@ expression: snapshot
# invalid_extended_atom_escape
× Invalid regular expression: Could not parse the entire pattern
╭─[/\c0/u:1:3]
× Invalid regular expression: Invalid escape sequence
╭─[/\c0/u:1:2]
1 │ /\c0/u
· ▲
· ─
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
# duplicated_modifiers
+4 -4
View File
@@ -5144,12 +5144,12 @@ Negative Passed: 265/265 (100.00%)
╰────
help: This character or escape sequence is not valid inside a character class
× Invalid regular expression: Could not parse the entire pattern
╭─[misc/fail/regexp-unicode-leading-zero-reference.js:1:5]
× Invalid regular expression: Invalid escape sequence
╭─[misc/fail/regexp-unicode-leading-zero-reference.js:1:4]
1 │ /()\01/u;
· ▲
· ─
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
× Invalid rest element target in destructuring assignment
╭─[misc/fail/rest-assignment-target.js:1:6]
+12 -12
View File
@@ -24615,13 +24615,13 @@ Negative Passed: 4653/4653 (100.00%)
╰────
help: Add the missing closing delimiter
× Invalid regular expression: Unterminated capturing group
╭─[test262/test/language/literals/regexp/named-groups/invalid-identity-escape-in-capture-u.js:16:2]
× Invalid regular expression: Invalid escape sequence
╭─[test262/test/language/literals/regexp/named-groups/invalid-identity-escape-in-capture-u.js:16:7]
15 │
16 │ /(?<a>\a)/u;
· ──────
· ─
╰────
help: Add the missing closing delimiter
help: Use a valid escape sequence or escape the backslash with `\\`
× Invalid regular expression: Unterminated capturing group name
╭─[test262/test/language/literals/regexp/named-groups/invalid-incomplete-groupname-2-u.js:16:11]
@@ -24991,13 +24991,13 @@ Negative Passed: 4653/4653 (100.00%)
34 │ ╰─▶ */
╰────
× Invalid regular expression: Could not parse the entire pattern
╭─[test262/test/language/literals/regexp/u-invalid-class-escape.js:24:3]
× Invalid regular expression: Invalid escape sequence
╭─[test262/test/language/literals/regexp/u-invalid-class-escape.js:24:2]
23 │
24 │ /\c0/u;
· ▲
· ─
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
× Invalid regular expression: Could not parse the entire pattern
╭─[test262/test/language/literals/regexp/u-invalid-extended-pattern-char.js:21:2]
@@ -25007,13 +25007,13 @@ Negative Passed: 4653/4653 (100.00%)
╰────
help: Check for unbalanced brackets or invalid escape sequences
× Invalid regular expression: Could not parse the entire pattern
╭─[test262/test/language/literals/regexp/u-invalid-identity-escape.js:23:3]
× Invalid regular expression: Invalid escape sequence
╭─[test262/test/language/literals/regexp/u-invalid-identity-escape.js:23:2]
22 │
23 │ /\M/u;
· ▲
· ─
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
× Invalid regular expression: Invalid indexed reference
╭─[test262/test/language/literals/regexp/u-invalid-legacy-octal-escape.js:26:2]
@@ -12389,14 +12389,14 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
╰────
help: Duplicate names must be in different alternatives
× Invalid regular expression: Could not parse the entire pattern
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionAnnexB.ts:7:5]
× Invalid regular expression: Invalid escape sequence
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionAnnexB.ts:7:4]
6 │ // Compare to
7 │ /\q\u\i\c\k\_\f\o\x\-\j\u\m\p\s/u,
· ▲
· ─
8 │ /[\q\u\i\c\k\_\f\o\x\-\j\u\m\p\s]/u,
╰────
help: Check for unbalanced brackets or invalid escape sequences
help: Use a valid escape sequence or escape the backslash with `\\`
× Invalid regular expression: Invalid class atom
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionAnnexB.ts:8:5]