fix(parser): allow escaped type default import bindings (#26409)

TypeScript allows a default import binding named `type` to contain Unicode escapes. Oxc currently reports `Keywords cannot contain escape characters` for these valid imports:

```ts
import t\u0079pe from "m";
import t\u0079pe, { x } from "m";
import t\u0079pe, * as ns from "m";
```

In each example, `t\u0079pe` is an identifier whose decoded name is `type`, and the declaration is a value import.

The parser initially recognizes the decoded `type` token before it has enough context to distinguish a default binding from a type-only import modifier. The escape check ran at that point, so it rejected the binding even when the following tokens established that this was a value import.

Move the escape check into the two branches that interpret `type` as a modifier: before a named or namespace import, and before another binding identifier after ruling out `import type from "m"`. The latter check also runs before the early return for a type-only import-equals declaration.

Escaped modifiers remain invalid, including these forms:

```ts
import t\u0079pe Foo = require("m");
import t\u0079pe { x } from "m";
import t\u0079pe * as ns from "m";
import { t\u0079pe Bar } from "m";
```

The change is limited to when the declaration-level escape diagnostic is emitted; import classification and specifier parsing retain their existing behavior.
This commit is contained in:
camc314
2026-09-07 16:03:31 +00:00
parent f4eacfcfef
commit 10521b288f
11 changed files with 68 additions and 17 deletions
+9 -4
View File
@@ -103,14 +103,13 @@ impl<'a, C: Config> ParserImpl<'a, C> {
} else if self.is_ts && token_after_import.kind() == Kind::Type {
// `import type ...`
if token_after_import.escaped() {
self.error(diagnostics::escaped_keyword(token_after_import.span()));
}
let kind = self.cur_kind();
if kind == Kind::LCurly || kind == Kind::Star {
// `import type { ...`
// `import type * ...`
if token_after_import.escaped() {
self.error(diagnostics::escaped_keyword(token_after_import.span()));
}
import_kind = ImportOrExportKind::Type;
has_default_specifier = false;
} else if kind.is_binding_identifier() {
@@ -119,10 +118,16 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let identifier_after_type = self.parse_binding_identifier();
if token.kind() == Kind::From && self.at(Kind::Str) {
// `import type from 'source'`
if token.escaped() {
self.error(diagnostics::escaped_keyword(token.span()));
}
has_default_specifier = true;
import_kind = ImportOrExportKind::Value;
should_parse_specifiers = false;
} else {
if token_after_import.escaped() {
self.error(diagnostics::escaped_keyword(token_after_import.span()));
}
identifier_after_import = Some(identifier_after_type);
import_kind = ImportOrExportKind::Type;
@@ -0,0 +1,5 @@
import t\u0079pe Foo = require("m");
import t\u0079pe { x } from "m";
import t\u0079pe * as ns from "m";
import { t\u0079pe Bar } from "m";
import t\u0079pe fr\u006fm "m";
@@ -0,0 +1 @@
import t\u0079pe, { x } from "m";
@@ -0,0 +1 @@
import t\u0079pe, * as ns from "m";
@@ -0,0 +1 @@
import t\u0079pe from "m";
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 86/86 (100.00%)
Positive Passed: 86/86 (100.00%)
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 86/86 (100.00%)
Positive Passed: 86/86 (100.00%)
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 274/274 (100.00%)
Positive Passed: 274/274 (100.00%)
AST Parsed : 278/278 (100.00%)
Positive Passed: 278/278 (100.00%)
+41 -3
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 86/86 (100.00%)
Positive Passed: 86/86 (100.00%)
Negative Passed: 188/188 (100.00%)
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
Negative Passed: 189/189 (100.00%)
× Cannot assign to 'arguments' in strict mode
╭─[misc/fail/arguments-eval-ambient.ts:2:13]
@@ -377,6 +377,44 @@ Negative Passed: 188/188 (100.00%)
╰────
help: for octal literals use the '0o' prefix instead
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:1:8]
1 │ import t\u0079pe Foo = require("m");
· ─────────
2 │ import t\u0079pe { x } from "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:2:8]
1 │ import t\u0079pe Foo = require("m");
2 │ import t\u0079pe { x } from "m";
· ─────────
3 │ import t\u0079pe * as ns from "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:3:8]
2 │ import t\u0079pe { x } from "m";
3 │ import t\u0079pe * as ns from "m";
· ─────────
4 │ import { t\u0079pe Bar } from "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:4:10]
3 │ import t\u0079pe * as ns from "m";
4 │ import { t\u0079pe Bar } from "m";
· ─────────
5 │ import t\u0079pe fr\u006fm "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:5:18]
4 │ import { t\u0079pe Bar } from "m";
5 │ import t\u0079pe fr\u006fm "m";
· ─────────
╰────
× A unary expression with the '-' operator cannot be used as the left operand of an exponentiation expression
╭─[misc/fail/exponentiation-left-operands.ts:1:1]
1 │ -value ** 2;
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 86/86 (100.00%)
Positive Passed: 82/86 (95.35%)
AST Parsed : 89/89 (100.00%)
Positive Passed: 85/89 (95.51%)
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 : 86/86 (100.00%)
Positive Passed: 86/86 (100.00%)
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)