From 5b9a7bc735c0fbc29a5c3dcc123db8e337d17930 Mon Sep 17 00:00:00 2001 From: Boshen Date: Fri, 30 Oct 2026 11:35:20 +0800 Subject: [PATCH] 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). --- crates/oxc_parser/src/js/declaration.rs | 35 ++--- crates/oxc_parser/src/js/statement.rs | 2 +- .../misc/pass/let-expression-statements.cjs | 8 ++ tasks/coverage/snapshots/codegen_misc.snap | 4 +- tasks/coverage/snapshots/formatter_misc.snap | 4 +- tasks/coverage/snapshots/lexer_misc.snap | 4 +- tasks/coverage/snapshots/parser_babel.snap | 131 ++---------------- tasks/coverage/snapshots/parser_misc.snap | 4 +- .../coverage/snapshots/parser_typescript.snap | 12 +- tasks/coverage/snapshots/semantic_babel.snap | 38 +---- tasks/coverage/snapshots/semantic_misc.snap | 4 +- .../coverage/snapshots/transformer_misc.snap | 4 +- 12 files changed, 54 insertions(+), 196 deletions(-) create mode 100644 tasks/coverage/misc/pass/let-expression-statements.cjs diff --git a/crates/oxc_parser/src/js/declaration.rs b/crates/oxc_parser/src/js/declaration.rs index eac2787d0e..1dbe79e2d7 100644 --- a/crates/oxc_parser/src/js/declaration.rs +++ b/crates/oxc_parser/src/js/declaration.rs @@ -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 { diff --git a/crates/oxc_parser/src/js/statement.rs b/crates/oxc_parser/src/js/statement.rs index fd1b1b1002..a6add9e8de 100644 --- a/crates/oxc_parser/src/js/statement.rs +++ b/crates/oxc_parser/src/js/statement.rs @@ -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 { diff --git a/tasks/coverage/misc/pass/let-expression-statements.cjs b/tasks/coverage/misc/pass/let-expression-statements.cjs new file mode 100644 index 0000000000..b7973456e3 --- /dev/null +++ b/tasks/coverage/misc/pass/let-expression-statements.cjs @@ -0,0 +1,8 @@ +let, other; +let ? yes : no; +let: { break let; } +let +42; +let +const value = 1; +let diff --git a/tasks/coverage/snapshots/codegen_misc.snap b/tasks/coverage/snapshots/codegen_misc.snap index 870d817118..744bc76f85 100644 --- a/tasks/coverage/snapshots/codegen_misc.snap +++ b/tasks/coverage/snapshots/codegen_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/formatter_misc.snap b/tasks/coverage/snapshots/formatter_misc.snap index d1b1c41756..5c043308a0 100644 --- a/tasks/coverage/snapshots/formatter_misc.snap +++ b/tasks/coverage/snapshots/formatter_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/lexer_misc.snap b/tasks/coverage/snapshots/lexer_misc.snap index 977730253f..b01f8a4f9a 100644 --- a/tasks/coverage/snapshots/lexer_misc.snap +++ b/tasks/coverage/snapshots/lexer_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/parser_babel.snap b/tasks/coverage/snapshots/parser_babel.snap index 571f09d56e..8d0b5cf406 100644 --- a/tasks/coverage/snapshots/parser_babel.snap +++ b/tasks/coverage/snapshots/parser_babel.snap @@ -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