fix(parser): reject escaped import-phase keywords (#26534)

The parser consumes `source`, `defer`, and sometimes `from` as binding identifiers before resolving their role in an import declaration. Validate escapes once they are known to be keywords, so declarations such as `import s\u006furce wasm from "m"` and `import source fr\u006fm "m"` report an error.

Escaped binding names remain valid, including `import s\u006furce from "m"` and `import source fr\u006fm from "m"`.
This commit is contained in:
camc314
2026-09-10 14:55:08 +00:00
parent 59e8d5fb91
commit b32d25d418
9 changed files with 62 additions and 15 deletions
+10 -2
View File
@@ -174,9 +174,9 @@ impl<'a, C: Config> ParserImpl<'a, C> {
} else if token_after_import.kind() == Kind::Source {
if self.cur_kind().is_binding_identifier() {
// `import source something ...`
let kind = self.cur_kind();
let token = self.cur_token();
let identifier_after_source = self.parse_binding_identifier();
if kind == Kind::From {
if token.kind() == Kind::From {
// `import source from ...`
if self.at(Kind::From) {
// `import source from from ...`
@@ -185,6 +185,9 @@ impl<'a, C: Config> ParserImpl<'a, C> {
has_default_specifier = true;
} else if self.at(Kind::Str) {
// `import source from 'source'`
if token.escaped() {
self.error(diagnostics::escaped_keyword(token.span()));
}
has_default_specifier = true;
should_parse_specifiers = false;
}
@@ -213,6 +216,11 @@ impl<'a, C: Config> ParserImpl<'a, C> {
// else: `import source from 'source'` - source is the binding name, no phase
}
// The first identifier is a keyword only after resolving the import phase.
if phase.is_some() && token_after_import.escaped() {
self.error(diagnostics::escaped_keyword(token_after_import.span()));
}
let specifiers = if self.at(Kind::Str) {
if has_default_specifier && !should_parse_specifiers {
match identifier_after_import {
@@ -0,0 +1,4 @@
import s\u006furce wasm from "m";
import d\u0065fer * as ns from "m";
import source fr\u006fm "m";
import s\u006furce from from "m";
@@ -0,0 +1,5 @@
import s\u006furce from "m";
import d\u0065fer from "m";
import source fr\u006fm from "m";
import source w\u0061sm from "m";
import defer * as n\u0073 from "m";
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 105/105 (100.00%)
Positive Passed: 105/105 (100.00%)
AST Parsed : 106/106 (100.00%)
Positive Passed: 106/106 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 105/105 (100.00%)
Positive Passed: 105/105 (100.00%)
AST Parsed : 106/106 (100.00%)
Positive Passed: 106/106 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 319/319 (100.00%)
Positive Passed: 319/319 (100.00%)
AST Parsed : 321/321 (100.00%)
Positive Passed: 321/321 (100.00%)
+33 -3
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 105/105 (100.00%)
Positive Passed: 105/105 (100.00%)
Negative Passed: 214/214 (100.00%)
AST Parsed : 106/106 (100.00%)
Positive Passed: 106/106 (100.00%)
Negative Passed: 215/215 (100.00%)
× Cannot assign to 'arguments' in strict mode
╭─[misc/fail/arguments-eval-ambient.ts:2:13]
@@ -494,6 +494,36 @@ Negative Passed: 214/214 (100.00%)
╰────
help: for octal literals use the '0o' prefix instead
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-import-phase.js:1:8]
1 │ import s\u006furce wasm from "m";
· ───────────
2 │ import d\u0065fer * as ns from "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-import-phase.js:2:8]
1 │ import s\u006furce wasm from "m";
2 │ import d\u0065fer * as ns from "m";
· ──────────
3 │ import source fr\u006fm "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-import-phase.js:3:15]
2 │ import d\u0065fer * as ns from "m";
3 │ import source fr\u006fm "m";
· ─────────
4 │ import s\u006furce from from "m";
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-import-phase.js:4:8]
3 │ import source fr\u006fm "m";
4 │ import s\u006furce from from "m";
· ───────────
╰────
× Keywords cannot contain escape characters
╭─[misc/fail/escaped-type-import.ts:1:8]
1 │ import t\u0079pe Foo = require("m");
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 105/105 (100.00%)
Positive Passed: 101/105 (96.19%)
AST Parsed : 106/106 (100.00%)
Positive Passed: 102/106 (96.23%)
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 : 105/105 (100.00%)
Positive Passed: 105/105 (100.00%)
AST Parsed : 106/106 (100.00%)
Positive Passed: 106/106 (100.00%)