fix(parser): reject module export names containing a lone surrogate (#26953)

A module export name that is not well-formed Unicode is an [early error](https://tc39.es/ecma262/#sec-module-semantics-static-semantics-early-errors) (`ModuleExportName : StringLiteral` is a Syntax Error if `IsStringWellFormedUnicode` of its value is false), and the parser reported it, but it still built the node and returned the recovered AST. Every consumer of `ModuleExportName::name` therefore had to tolerate an export name that cannot exist in a valid module. Acorn rejects the input outright, and Babel does the same unless error recovery is enabled.

Make the error fatal, so no AST carries an ill-formed export name. Consumers can treat the name as well-formed by construction. This is preparation for the JSStr stack (#26242), where `ModuleExportName::name` keeps returning UTF-8 text and currently needs a fallback for exactly this case; with this change the fallback becomes dead and is removed there.

## Example

```js
export { x as "\uD800" };
```

Before, the error was reported and parsing continued with a recovered AST. After, the same diagnostic is reported and parsing stops:

```
x An export name cannot include a unicode lone surrogate
```

The test262 negative test `early-export-ill-formed-string.js` still fails to parse; it now reports only its first error instead of three, and a duplicate-export diagnostic that printed the surrogate as marker text disappears with it. Verified with four new misc fixtures covering the export, re-export, namespace re-export, and import forms, the parser, semantic, codegen, and lexer conformance runs, and the parser, semantic, and linter test suites.

Implemented with AI assistance.
This commit is contained in:
Dunqing committed 2026-09-23 02:11:12 +00:00
1 parent d77d5bfe5d
commit 01ff33e9c8
8 files changed
+36 -32

No files matched your search

+3 -1
View File
@@ -982,8 +982,10 @@ 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.
// The error is fatal so that no AST ever carries an ill-formed export name.
if !literal.is_string_well_formed_unicode() {
self.error(diagnostics::export_lone_surrogate(literal.span));
let error = diagnostics::export_lone_surrogate(literal.span);
return self.fatal_error(error);
}
ModuleExportName::StringLiteral(literal)
}
@@ -0,0 +1 @@
export { "\uDC00" as y } from "m";
@@ -0,0 +1 @@
export * as "\uD800" from "m";
@@ -0,0 +1,2 @@
const x = 1;
export { x as "\uD800" };
@@ -0,0 +1 @@
import { "\uD800" as y } from "m";
+2 -2
View File
@@ -1,6 +1,6 @@
lexer_misc Summary:
AST Parsed : 345/345 (100.00%)
Positive Passed: 343/345 (99.42%)
AST Parsed : 349/349 (100.00%)
Positive Passed: 347/349 (99.43%)
Tokens: tasks/coverage/misc/pass/babel-16776-m.js
Tokens: tasks/coverage/misc/pass/unambiguous-await-regex.js
+26 -1
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 111/111 (100.00%)
Positive Passed: 111/111 (100.00%)
Negative Passed: 234/234 (100.00%)
Negative Passed: 238/238 (100.00%)
× TS(1275): 'accessor' modifier can only appear on a property declaration.
╭─[misc/fail/accessor-modifier-abstract-method.ts:1:29]
@@ -719,6 +719,25 @@ Negative Passed: 234/234 (100.00%)
· ──
╰────
× An export name cannot include a unicode lone surrogate
╭─[misc/fail/export-name-lone-surrogate-from.js:1:10]
1 │ export { "\uDC00" as y } from "m";
· ────────
╰────
× An export name cannot include a unicode lone surrogate
╭─[misc/fail/export-name-lone-surrogate-star.js:1:13]
1 │ export * as "\uD800" from "m";
· ────────
╰────
× An export name cannot include a unicode lone surrogate
╭─[misc/fail/export-name-lone-surrogate.js:2:15]
1 │ const x = 1;
2 │ export { x as "\uD800" };
· ────────
╰────
× The left-hand side of a for...in statement cannot be an await using declaration.
╭─[misc/fail/for-await-using-in-initializer.js:2:8]
1 │ async function f() {
@@ -1006,6 +1025,12 @@ Negative Passed: 234/234 (100.00%)
· ╰── `from` expected
╰────
× An export name cannot include a unicode lone surrogate
╭─[misc/fail/import-name-lone-surrogate.js:1:10]
1 │ import { "\uD800" as y } from "m";
· ────────
╰────
× Expected `from` but found `Identifier`
╭─[misc/fail/import-source-non-from.js:1:19]
1 │ import source foo bar from 'module';
@@ -25603,34 +25603,6 @@ Negative Passed: 4653/4653 (100.00%)
22 │ export {"\uD83C"} from "./early-export-ill-formed-string.js";
╰────
× An export name cannot include a unicode lone surrogate
╭─[test262/test/language/module-code/early-export-ill-formed-string.js:22:9]
21 │ export {Moon as "\uD83C",} from "./early-export-ill-formed-string.js";
22 │ export {"\uD83C"} from "./early-export-ill-formed-string.js";
· ────────
23 │ import {'\uD83C' as Usagi} from "./early-export-ill-formed-string.js";
╰────
× An export name cannot include a unicode lone surrogate
╭─[test262/test/language/module-code/early-export-ill-formed-string.js:23:9]
22 │ export {"\uD83C"} from "./early-export-ill-formed-string.js";
23 │ import {'\uD83C' as Usagi} from "./early-export-ill-formed-string.js";
· ────────
24 │
╰────
× Duplicated export '�d83c'
╭─[test262/test/language/module-code/early-export-ill-formed-string.js:21:17]
20 │ // 🌙 is '\uD83C\uDF19'
21 │ export {Moon as "\uD83C",} from "./early-export-ill-formed-string.js";
· ────┬───
· ╰── Export has already been declared here
22 │ export {"\uD83C"} from "./early-export-ill-formed-string.js";
· ────┬───
· ╰── It cannot be redeclared here
23 │ import {'\uD83C' as Usagi} from "./early-export-ill-formed-string.js";
╰────
× Export 'unresolvable' is not defined
╭─[test262/test/language/module-code/early-export-unresolvable.js:17:10]
16 │