fix(parser): parse sloppy let expressions and labels (#28438)

Handle sloppy-mode `let` expressions, comma expressions, and labels
through the ordinary statement parser while preserving
lexical-declaration restrictions.

Validated with scoped Rust checks, full conformance, parser bindings,
allocation tracking.

AI-assisted (Codex).
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 11:35:20 +08:00
1 parent 5238839466
commit 5b9a7bc735
12 files changed
+54 -196

No files matched your search

+11 -24
View File
@@ -9,35 +9,22 @@ impl<'a, C: Config> ParserImpl<'a, C> {
pub(crate) fn parse_let(&mut self, stmt_ctx: StatementContext) -> Statement<'a> {
let start = self.cur_start();
let peeked = self.lexer.peek_token().kind();
let next = self.lexer.peek_token();
let peeked = next.kind();
// Fast path: avoid rewind.
if !stmt_ctx.is_single_statement() && peeked.is_after_let() {
// `let [` cannot start an expression statement, even in a single-statement
// position. Otherwise, a declaration needs a binding after `let`.
if peeked == Kind::LBrack
|| (!stmt_ctx.is_single_statement()
&& (peeked == Kind::LCurly
|| peeked.is_binding_identifier()
|| (!next.is_on_new_line() && peeked.is_after_let())))
{
self.bump_any(); // bump `let`
return self.parse_variable_statement(start, VariableDeclarationKind::Let, stmt_ctx);
}
// let = foo, let instanceof x, let + 1
if peeked.is_assignment_operator() || peeked.is_binary_operator() {
let expr = self.parse_assignment_expression_or_higher();
self.parse_expression_statement(start, expr)
// let.a = 1, let?.a = 1, let()[a] = 1, let++, let--
} else if matches!(
peeked,
Kind::Dot | Kind::QuestionDot | Kind::LParen | Kind::Plus2 | Kind::Minus2
) {
let expr = self.parse_expr();
self.parse_expression_statement(start, expr)
// single statement let declaration: while (0) let
} else if (stmt_ctx.is_single_statement() && peeked != Kind::LBrack)
|| peeked == Kind::Semicolon
{
let expr = self.parse_identifier_expression();
self.parse_expression_statement(start, expr)
} else {
self.bump_any();
self.parse_variable_statement(start, VariableDeclarationKind::Let, stmt_ctx)
}
self.parse_expression_or_labeled_statement()
}
pub(crate) fn is_using_statement(&mut self) -> bool {
+1 -1
View File
@@ -263,7 +263,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
}
}
fn parse_expression_or_labeled_statement(&mut self) -> Statement<'a> {
pub(super) fn parse_expression_or_labeled_statement(&mut self) -> Statement<'a> {
let start = self.cur_start();
let expr = self.parse_expr();
if let Expression::Identifier(ident) = &expr {
@@ -0,0 +1,8 @@
let, other;
let ? yes : no;
let: { break let; }
let
42;
let
const value = 1;
let
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 128/128 (100.00%)
Positive Passed: 128/128 (100.00%)
AST Parsed : 129/129 (100.00%)
Positive Passed: 129/129 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 128/128 (100.00%)
Positive Passed: 128/128 (100.00%)
AST Parsed : 129/129 (100.00%)
Positive Passed: 129/129 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 391/391 (100.00%)
Positive Passed: 391/391 (100.00%)
AST Parsed : 392/392 (100.00%)
Positive Passed: 392/392 (100.00%)
+13 -118
View File
@@ -1,8 +1,8 @@
commit: b835f0c6
parser_babel Summary:
AST Parsed : 2292/2304 (99.48%)
Positive Passed: 2271/2304 (98.57%)
AST Parsed : 2304/2304 (100.00%)
Positive Passed: 2283/2304 (99.09%)
Negative Passed: 1828/1843 (99.19%)
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/cast/unparenthesized-assert-and-assign/input.ts
@@ -34,15 +34,6 @@ Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/ty
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/module-namespace/module-identifier-invalid/input.ts
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-close-let-keyword/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-close-let-keyword/input.js:3:1]
2 │ */ --> c
3 │ if (x) {}
· ──
╰────
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-export-default-from/input.js
× Cannot use export statement outside a module
@@ -52,15 +43,6 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-
2 │ "x";
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-let-keyword/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-let-keyword/input.js:2:1]
1 │ let <!-- c
2 │ if (x) {}
· ──
╰────
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/3.3-function-in-if-body-lexical-redeclaration/input.js
× Identifier `f` has already been declared
@@ -90,87 +72,6 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015
2 │ if (x) {}
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword/input.js:2:1]
1 │ let
2 │ if (x) {}
· ──
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-cr/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-cr/input.js:2:1]
1 │ let if (x) {}
· ──
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-declaration/input.js
× Identifier expected. 'function' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-declaration/input.js:2:1]
1 │ let
2 │ function f() {}
· ────────
3 │ let
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-expression/input.js
× Identifier expected. 'this' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-expression/input.js:2:1]
1 │ let // comment
2 │ this.x = 1;
· ────
3 │ let /*
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-line-separator/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-line-separator/input.js:1:7]
1 │ let
if (x) {}
· ──
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-nested/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-nested/input.js:3:3]
2 │ let
3 │ if (x) {}
· ──
4 │ }
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-paragraph-separator/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-paragraph-separator/input.js:1:7]
1 │ let
if (x) {}
· ──
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-void/input.js
× Identifier expected. 'void' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-void/input.js:2:1]
1 │ let
2 │ void 0;
· ────
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-multiline-comment-and-keyword/input.js
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-multiline-comment-and-keyword/input.js:2:4]
1 │ let /*
2 │ */ if (x) {}
· ──
╰────
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/estree/class-private-method/typescript-invalid-abstract/input.ts
× TS(18019): 'abstract' modifier cannot be used with a private identifier.
@@ -424,15 +325,6 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
6 │ }
╰────
Fatal error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/enum/let-with-linebreak/input.js
× Identifier expected. 'enum' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/typescript/enum/let-with-linebreak/input.js:2:1]
1 │ let
2 │ enum E {}
· ────
╰────
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/function/declare-pattern-parameters/input.ts
× TS(1016): A required parameter cannot follow an optional parameter.
@@ -3771,33 +3663,36 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
╰────
help: Try inserting a semicolon here
× Identifier expected. 'enum' is a reserved word that cannot be used here.
× Unexpected token
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-enum/input.js:2:1]
1 │ let
2 │ enum;
· ────
╰────
× Identifier expected. 'if' is a reserved word that cannot be used here.
× Keywords cannot contain escape characters
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-escaped-keyword/input.js:2:1]
1 │ let
2 │ i\u0066 (x) {}
· ───────
╰────
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-module/input.js:2:1]
× The keyword 'let' is reserved
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-module/input.js:1:1]
1 │ let
· ───
2 │ if (x) {}
· ──
╰────
note: Modules are always strict mode code
× Identifier expected. 'if' is a reserved word that cannot be used here.
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-strict/input.js:3:1]
× The keyword 'let' is reserved
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-strict/input.js:2:1]
1 │ "use strict";
2 │ let
· ───
3 │ if (x) {}
· ──
╰────
note: This identifier is reserved in strict mode code
× Expected a semicolon or an implicit semicolon after a statement, but found none
╭─[babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-yield/input.js:3:8]
+2 -2
View File
@@ -1,6 +1,6 @@
parser_misc Summary:
AST Parsed : 128/128 (100.00%)
Positive Passed: 128/128 (100.00%)
AST Parsed : 129/129 (100.00%)
Positive Passed: 129/129 (100.00%)
Negative Passed: 265/265 (100.00%)
× TS(1275): 'accessor' modifier can only appear on a property declaration.
@@ -6544,11 +6544,13 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
1 │ const
╰────
× Unexpected token
╭─[typescript/tsc/testdata/tests/cases/compiler/downlevelLetConst11.ts:2:4]
× The keyword 'let' is reserved
╭─[typescript/tsc/testdata/tests/cases/compiler/downlevelLetConst11.ts:2:1]
1 │ "use strict";
2 │ let
· ───
╰────
note: This identifier is reserved in strict mode code
× Missing initializer in const declaration
╭─[typescript/tsc/testdata/tests/cases/compiler/downlevelLetConst2.ts:1:7]
@@ -21757,11 +21759,13 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
· ────
╰────
× Unexpected token
╭─[typescript/tsc/testdata/tests/cases/conformance/es6/variableDeclarations/VariableDeclaration11_es6.ts:2:4]
× The keyword 'let' is reserved
╭─[typescript/tsc/testdata/tests/cases/conformance/es6/variableDeclarations/VariableDeclaration11_es6.ts:2:1]
1 │ "use strict";
2 │ let
· ───
╰────
note: This identifier is reserved in strict mode code
× Unexpected token
╭─[typescript/tsc/testdata/tests/cases/conformance/es6/variableDeclarations/VariableDeclaration13_es6.ts:4:5]
+1 -37
View File
@@ -2,16 +2,10 @@ commit: b835f0c6
semantic_babel Summary:
AST Parsed : 2304/2304 (100.00%)
Positive Passed: 2197/2304 (95.36%)
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-close-let-keyword/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
Positive Passed: 2209/2304 (95.88%)
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-export-default-from/input.js
Cannot use export statement outside a module
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-let-keyword/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/3.3-function-in-if-body-lexical-redeclaration/input.js
Identifier `f` has already been declared
Identifier `g` has already been declared
@@ -29,33 +23,6 @@ rebuilt : SymbolId(3): SymbolFlags(BlockScopedVariable)
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/export-default-let-with-linebreak-and-keyword/input.js
Cannot use export statement outside a module
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-cr/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-declaration/input.js
Identifier expected. 'function' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-expression/input.js
Identifier expected. 'this' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-line-separator/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-nested/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-keyword-paragraph-separator/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-linebreak-and-void/input.js
Identifier expected. 'void' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2015/let/let-with-multiline-comment-and-keyword/input.js
Identifier expected. 'if' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2022/class-private-properties/await-in-private-property-in-params-of-async-arrow/input.js
Bindings mismatch:
after transform: ScopeId(0): ["_asyncToGenerator", "_classPrivateFieldInitSpec"]
@@ -249,9 +216,6 @@ Bindings mismatch:
after transform: ScopeId(0): ["E"]
rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/enum/let-with-linebreak/input.js
Identifier expected. 'enum' is a reserved word that cannot be used here.
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/export/declare/input.ts
Bindings mismatch:
after transform: ScopeId(0): ["C", "N", "_bar", "bar", "f", "foo", "x"]
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 128/128 (100.00%)
Positive Passed: 122/128 (95.31%)
AST Parsed : 129/129 (100.00%)
Positive Passed: 123/129 (95.35%)
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 : 128/128 (100.00%)
Positive Passed: 128/128 (100.00%)
AST Parsed : 129/129 (100.00%)
Positive Passed: 129/129 (100.00%)