fix(parser): avoid panic on escaped string export names (#26146)

## Summary

Fix a parser panic when a string-named export decodes to a literal `\u` sequence.

Lone-surrogate validation now uses the metadata captured by the lexer instead of rescanning decoded string contents. This also prevents false diagnostics for names such as `"\\uD800"`.

## Before:

```

thread 'main' (10990283) panicked at crates/oxc_ast/src/ast_impl/literal.rs:89:42:
end byte index 4 is out of bounds for string of length 0
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::str::slice_error_fail_rt
   3: core::str::slice_error_fail
   4: <core::ops::range::RangeTo<usize> as core::slice::index::SliceIndex<str>>::index
   5: <str as core::ops::index::Index<core::ops::range::RangeTo<usize>>>::index
   6: <oxc_ast::ast::literal::StringLiteral>::is_string_well_formed_unicode
   7: <oxc_parser::ParserImpl<oxc_parser::config::NoTokensParserConfig>>::parse_module_export_name
```
This commit is contained in:
camc314 committed 2026-08-28 13:37:48 +00:00
1 parent e412cf2ce3
commit dc09a3aa9d
8 files changed
+17 -24

No files matched your search

+2 -13
View File
@@ -79,22 +79,11 @@ impl Display for NumericLiteral<'_> {
impl StringLiteral<'_> {
/// Static Semantics: `IsStringWellFormedUnicode`
/// test for \uD800-\uDFFF
///
/// See: <https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstringwellformedunicode>
#[inline]
pub fn is_string_well_formed_unicode(&self) -> bool {
let mut chars = self.value.chars();
while let Some(c) = chars.next() {
if c == '\\' && chars.next() == Some('u') {
let hex = &chars.as_str()[..4];
if let Ok(hex) = u32::from_str_radix(hex, 16)
&& (0xd800..=0xdfff).contains(&hex)
{
return false;
}
}
}
true
!self.lone_surrogates
}
}
+1 -1
View File
@@ -971,7 +971,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let literal = self.parse_literal_string();
// ModuleExportName : StringLiteral
// It is a Syntax Error if IsStringWellFormedUnicode(the SV of StringLiteral) is false.
if literal.lone_surrogates || !literal.is_string_well_formed_unicode() {
if !literal.is_string_well_formed_unicode() {
self.error(diagnostics::export_lone_surrogate(literal.span));
}
ModuleExportName::StringLiteral(literal)
@@ -0,0 +1,4 @@
const foo = 1;
export { foo as "\\u" };
export { foo as "\\uD800" };
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 79/79 (100.00%)
Positive Passed: 79/79 (100.00%)
AST Parsed : 80/80 (100.00%)
Positive Passed: 80/80 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 79/79 (100.00%)
Positive Passed: 79/79 (100.00%)
AST Parsed : 80/80 (100.00%)
Positive Passed: 80/80 (100.00%)
+2 -2
View File
@@ -1,6 +1,6 @@
parser_misc Summary:
AST Parsed : 79/79 (100.00%)
Positive Passed: 79/79 (100.00%)
AST Parsed : 80/80 (100.00%)
Positive Passed: 80/80 (100.00%)
Negative Passed: 167/167 (100.00%)
× Cannot assign to 'arguments' in strict mode
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 79/79 (100.00%)
Positive Passed: 75/79 (94.94%)
AST Parsed : 80/80 (100.00%)
Positive Passed: 76/80 (95.00%)
semantic Error: tasks/coverage/misc/pass/declare-let-private.ts
Bindings mismatch:
after transform: ScopeId(0): ["private"]
@@ -1,3 +1,3 @@
transformer_misc Summary:
AST Parsed : 79/79 (100.00%)
Positive Passed: 79/79 (100.00%)
AST Parsed : 80/80 (100.00%)
Positive Passed: 80/80 (100.00%)