fix(parser): switch to module goal eagerly on export (#22684)

## Summary

In unambiguous (`.js` auto-detect) sources, `export var x = await + 1;` reported a spurious `Duplicated export 'x'` error. The parser first parsed the statement with `await` as an identifier, then re-parsed it under `Context::Await` to patch the AST — but the second pass re-invoked `visit_export_named_declaration`, recording the binding `x` a second time in the module record.

`export` is unambiguously module syntax per ECMA-262 §16.2.3 (`ExportDeclaration` is only a `ModuleItem`, never a Script production). So commit to the Module goal — enable `Context::Await` — as soon as the next top-level token is `Kind::Export`. The statement then parses correctly on the first attempt; no reparse and no duplicate record.

`Kind::Import` is intentionally **not** eager-switched. TypeScript's `import name = ns.path` is a script-compatible namespace alias (see `topLevelAwait.2.ts`), so treating `import` as a module starter would break that case.

The only TS snapshot churn is +16 lines of additional error messages for already-failing tests (`topLevelAwaitErrors.5.ts`, `topLevelAwaitErrors.6.ts` — `export ... await`). Summary counts unchanged.

Closes #22520
Fixes #22158
This commit is contained in:
Boshen committed 2026-05-23 06:00:04 +00:00
1 parent b84941e69e
commit b284045af2
8 files changed
+40 -13

No files matched your search

+8 -3
View File
@@ -50,9 +50,14 @@ impl<'a, C: Config> ParserImpl<'a, C> {
break;
}
// Once ESM syntax is detected, enable await context for remaining statements
// and stop tracking (we'll reparse earlier statements at the end)
if track_await_reparse && self.module_record_builder.has_module_syntax() {
// Eagerly commit to Module goal on `export` so the statement is parsed
// under `Await` on the first pass; otherwise the reparse below runs it
// again and the export binding gets recorded twice.
// `import` is not eager: TypeScript's `import name = ns.foo` is a
// script-compatible namespace alias, not module syntax.
if track_await_reparse
&& (self.module_record_builder.has_module_syntax() || self.at(Kind::Export))
{
track_await_reparse = false;
self.ctx = self.ctx.and_await(true);
}
+6
View File
@@ -0,0 +1,6 @@
// https://github.com/oxc-project/oxc/issues/22158
// `await` in initializer of an `export var` declaration must not be
// reported as a duplicated export when parsed with unambiguous `.js`.
export var a = await + 1;
export var b = await(1);
export var c = await + 0;
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 66/66 (100.00%)
Positive Passed: 66/66 (100.00%)
AST Parsed : 67/67 (100.00%)
Positive Passed: 67/67 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 66/66 (100.00%)
Positive Passed: 66/66 (100.00%)
AST Parsed : 67/67 (100.00%)
Positive Passed: 67/67 (100.00%)
+2 -2
View File
@@ -1,6 +1,6 @@
parser_misc Summary:
AST Parsed : 66/66 (100.00%)
Positive Passed: 66/66 (100.00%)
AST Parsed : 67/67 (100.00%)
Positive Passed: 67/67 (100.00%)
Negative Passed: 137/137 (100.00%)
× Cannot assign to 'arguments' in strict mode
@@ -21019,6 +21019,14 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/parser/ecmasc
· ─────
╰────
× Cannot use `await` as an identifier in an async context
╭─[typescript/tests/cases/conformance/externalModules/topLevelAwaitErrors.5.ts:2:14]
1 │ // await in exported class name should fail
2 │ export class await {
· ─────
3 │ }
╰────
× The keyword 'await' is reserved
╭─[typescript/tests/cases/conformance/externalModules/topLevelAwaitErrors.5.ts:2:14]
1 │ // await in exported class name should fail
@@ -21027,6 +21035,14 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/parser/ecmasc
3 │ }
╰────
× Cannot use `await` as an identifier in an async context
╭─[typescript/tests/cases/conformance/externalModules/topLevelAwaitErrors.6.ts:2:17]
1 │ // await in exported function name should fail
2 │ export function await() {
· ─────
3 │ }
╰────
× The keyword 'await' is reserved
╭─[typescript/tests/cases/conformance/externalModules/topLevelAwaitErrors.6.ts:2:17]
1 │ // await in exported function name should fail
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 66/66 (100.00%)
Positive Passed: 52/66 (78.79%)
AST Parsed : 67/67 (100.00%)
Positive Passed: 53/67 (79.10%)
semantic Error: tasks/coverage/misc/pass/conditional-arrow-alternate-return-type.ts
Unresolved references mismatch:
after transform: ["Pick", "Record"]
@@ -1,3 +1,3 @@
transformer_misc Summary:
AST Parsed : 66/66 (100.00%)
Positive Passed: 66/66 (100.00%)
AST Parsed : 67/67 (100.00%)
Positive Passed: 67/67 (100.00%)