From 1121bc0dc161520a418945dbc610c30adc7ab3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Thu, 22 Aug 2024 18:17:33 +0900 Subject: [PATCH] feat(es/parser): Disallow `let let` (#9484) **Related issue:** - Closes https://github.com/swc-project/swc/issues/8269 --- .changeset/cyan-rules-wink.md | 6 ++++ .../tests/tsc-references/for-of51.1.normal.js | 2 +- .../tsc-references/for-of51.2.minified.js | 2 +- .../src/parser/class_and_fn.rs | 19 ++++++---- crates/swc_ecma_parser/src/parser/expr.rs | 2 +- crates/swc_ecma_parser/src/parser/object.rs | 2 +- crates/swc_ecma_parser/src/parser/pat.rs | 35 ++++++++++++------- crates/swc_ecma_parser/src/parser/stmt.rs | 8 +++-- .../src/parser/stmt/module_item.rs | 10 +++--- .../swc_ecma_parser/src/parser/typescript.rs | 2 +- .../tests/errors/issue-8269/input.js | 9 +++++ .../errors/issue-8269/input.js.swc-stderr | 7 ++++ .../fail/0abefbc80bf651fa.js.swc-stderr | 4 +-- .../fail/39551fb86dcd3b29.js.swc-stderr | 4 +-- .../fail/49624f905645b7d0.js.swc-stderr | 4 +-- .../fail/ac4ee5fb095faad0.js.swc-stderr | 4 +-- .../fail/b02b296bd115b9b9.js.swc-stderr | 4 +-- .../issue-1391/case2/input.ts.swc-stderr | 8 +---- .../typescript-errors/let/index.ts.swc-stderr | 2 +- crates/swc_ecma_parser/tests/typescript.rs | 1 + 20 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 .changeset/cyan-rules-wink.md create mode 100644 crates/swc_ecma_parser/tests/errors/issue-8269/input.js create mode 100644 crates/swc_ecma_parser/tests/errors/issue-8269/input.js.swc-stderr diff --git a/.changeset/cyan-rules-wink.md b/.changeset/cyan-rules-wink.md new file mode 100644 index 000000000000..343bd1d5cca9 --- /dev/null +++ b/.changeset/cyan-rules-wink.md @@ -0,0 +1,6 @@ +--- +swc_core: patch +swc_ecma_parser: patch +--- + +feat(es/parser): Disallow `let let` diff --git a/crates/swc/tests/tsc-references/for-of51.1.normal.js b/crates/swc/tests/tsc-references/for-of51.1.normal.js index a27d77659792..1fd65621b90a 100644 --- a/crates/swc/tests/tsc-references/for-of51.1.normal.js +++ b/crates/swc/tests/tsc-references/for-of51.1.normal.js @@ -1,5 +1,5 @@ //// [for-of51.ts] -//! x `let` cannot be used as an identifier in strict mode +//! x Unexpected token `let`. Expected let is reserved in const, let, class declaration //! ,---- //! 1 | for (let let of []) {} //! : ^^^ diff --git a/crates/swc/tests/tsc-references/for-of51.2.minified.js b/crates/swc/tests/tsc-references/for-of51.2.minified.js index a27d77659792..1fd65621b90a 100644 --- a/crates/swc/tests/tsc-references/for-of51.2.minified.js +++ b/crates/swc/tests/tsc-references/for-of51.2.minified.js @@ -1,5 +1,5 @@ //// [for-of51.ts] -//! x `let` cannot be used as an identifier in strict mode +//! x Unexpected token `let`. Expected let is reserved in const, let, class declaration //! ,---- //! 1 | for (let let of []) {} //! : ^^^ diff --git a/crates/swc_ecma_parser/src/parser/class_and_fn.rs b/crates/swc_ecma_parser/src/parser/class_and_fn.rs index ac171620eb36..2e2ed8b41562 100644 --- a/crates/swc_ecma_parser/src/parser/class_and_fn.rs +++ b/crates/swc_ecma_parser/src/parser/class_and_fn.rs @@ -84,7 +84,7 @@ impl Parser { expect!(p, "class"); let ident = p - .parse_maybe_opt_binding_ident(is_ident_required)? + .parse_maybe_opt_binding_ident(is_ident_required, true)? .map(Ident::from); if p.input.syntax().typescript() { if let Some(span) = ident.invalid_class_name() { @@ -1194,7 +1194,7 @@ impl Parser { in_class_field: false, ..self.ctx() }) - .parse_maybe_opt_binding_ident(is_ident_required)? + .parse_maybe_opt_binding_ident(is_ident_required, false)? } else { // function declaration does not change context for `BindingIdentifier`. self.with_ctx(Context { @@ -1202,7 +1202,7 @@ impl Parser { in_class_field: false, ..self.ctx() }) - .parse_maybe_opt_binding_ident(is_ident_required)? + .parse_maybe_opt_binding_ident(is_ident_required, false)? } .map(Ident::from); @@ -1254,11 +1254,18 @@ impl Parser { } /// If `required` is `true`, this never returns `None`. - fn parse_maybe_opt_binding_ident(&mut self, required: bool) -> PResult> { + fn parse_maybe_opt_binding_ident( + &mut self, + required: bool, + disallow_let: bool, + ) -> PResult> { if required { - self.parse_binding_ident().map(|v| v.id).map(Some) + self.parse_binding_ident(disallow_let) + .map(|v| v.id) + .map(Some) } else { - Ok(self.parse_opt_binding_ident()?.map(|v| v.id)) + self.parse_opt_binding_ident(disallow_let) + .map(|v| v.map(|v| v.id)) } } diff --git a/crates/swc_ecma_parser/src/parser/expr.rs b/crates/swc_ecma_parser/src/parser/expr.rs index 9f714ddb15be..6f8015fa3f27 100644 --- a/crates/swc_ecma_parser/src/parser/expr.rs +++ b/crates/swc_ecma_parser/src/parser/expr.rs @@ -431,7 +431,7 @@ impl Parser { return Ok(id.into()); } - let ident = self.parse_binding_ident()?; + let ident = self.parse_binding_ident(false)?; if self.input.syntax().typescript() && ident.sym == "as" && !is!(self, "=>") { // async as type let type_ann = self.in_type().parse_with(|p| p.parse_ts_type())?; diff --git a/crates/swc_ecma_parser/src/parser/object.rs b/crates/swc_ecma_parser/src/parser/object.rs index c3da76d2f8fe..f01bf7c346f6 100644 --- a/crates/swc_ecma_parser/src/parser/object.rs +++ b/crates/swc_ecma_parser/src/parser/object.rs @@ -472,7 +472,7 @@ impl ParseObject for Parser { // spread element let dot3_token = span!(self, start); - let arg = Box::new(self.parse_binding_pat_or_ident()?); + let arg = Box::new(self.parse_binding_pat_or_ident(false)?); return Ok(ObjectPatProp::Rest(RestPat { span: span!(self, start), diff --git a/crates/swc_ecma_parser/src/parser/pat.rs b/crates/swc_ecma_parser/src/parser/pat.rs index ba501bba7e10..aa353e81e687 100644 --- a/crates/swc_ecma_parser/src/parser/pat.rs +++ b/crates/swc_ecma_parser/src/parser/pat.rs @@ -6,19 +6,22 @@ use swc_common::Spanned; use super::{util::ExprExt, *}; use crate::{ parser::{class_and_fn::is_not_this, expr::AssignTargetOrSpread}, - token::IdentLike, + token::{IdentLike, Keyword}, }; impl Parser { pub fn parse_pat(&mut self) -> PResult { - self.parse_binding_pat_or_ident() + self.parse_binding_pat_or_ident(false) } - pub(super) fn parse_opt_binding_ident(&mut self) -> PResult> { + pub(super) fn parse_opt_binding_ident( + &mut self, + disallow_let: bool, + ) -> PResult> { trace_cur!(self, parse_opt_binding_ident); if is!(self, BindingIdent) || (self.input.syntax().typescript() && is!(self, "this")) { - self.parse_binding_ident().map(Some) + self.parse_binding_ident(disallow_let).map(Some) } else { Ok(None) } @@ -27,9 +30,15 @@ impl Parser { /// babel: `parseBindingIdentifier` /// /// spec: `BindingIdentifier` - pub(super) fn parse_binding_ident(&mut self) -> PResult { + pub(super) fn parse_binding_ident(&mut self, disallow_let: bool) -> PResult { trace_cur!(self, parse_binding_ident); + if disallow_let { + if let Some(Token::Word(Word::Keyword(Keyword::Let))) = self.input.cur() { + unexpected!(self, "let is reserved in const, let, class declaration") + } + } + // "yield" and "await" is **lexically** accepted. let ident = self.parse_ident(true, true)?; if ident.is_reserved_in_strict_bind() { @@ -45,11 +54,11 @@ impl Parser { Ok(ident.into()) } - pub(super) fn parse_binding_pat_or_ident(&mut self) -> PResult { + pub(super) fn parse_binding_pat_or_ident(&mut self, disallow_let: bool) -> PResult { trace_cur!(self, parse_binding_pat_or_ident); match *cur!(self, true) { - tok!("yield") | Word(..) => self.parse_binding_ident().map(Pat::from), + tok!("yield") | Word(..) => self.parse_binding_ident(disallow_let).map(Pat::from), tok!('[') => self.parse_array_binding_pat(), tok!('{') => self.parse_object(), // tok!('(') => { @@ -67,7 +76,7 @@ impl Parser { trace_cur!(self, parse_binding_element); let start = cur_pos!(self); - let left = self.parse_binding_pat_or_ident()?; + let left = self.parse_binding_pat_or_ident(false)?; if eat!(self, '=') { let right = self.include_in_expr(true).parse_assignment_expr()?; @@ -116,7 +125,7 @@ impl Parser { is_rest = true; let dot3_token = span!(self, start); - let pat = self.parse_binding_pat_or_ident()?; + let pat = self.parse_binding_pat_or_ident(false)?; rest_span = span!(self, start); let pat = RestPat { span: rest_span, @@ -299,7 +308,7 @@ impl Parser { is_rest = true; let dot3_token = span!(self, pat_start); - let pat = self.parse_binding_pat_or_ident()?; + let pat = self.parse_binding_pat_or_ident(false)?; let type_ann = if self.input.syntax().typescript() && is!(self, ':') { let cur_pos = cur_pos!(self); Some(self.parse_ts_type_ann(/* eat_colon */ true, cur_pos)?) @@ -412,7 +421,7 @@ impl Parser { let pat = if eat!(self, "...") { let dot3_token = span!(self, pat_start); - let mut pat = self.parse_binding_pat_or_ident()?; + let mut pat = self.parse_binding_pat_or_ident(false)?; if eat!(self, '=') { let right = self.parse_assignment_expr()?; @@ -940,7 +949,9 @@ mod tests { } fn object_pat(s: &'static str) -> Pat { - test_parser(s, Syntax::default(), |p| p.parse_binding_pat_or_ident()) + test_parser(s, Syntax::default(), |p| { + p.parse_binding_pat_or_ident(false) + }) } fn ident(s: &str) -> Ident { diff --git a/crates/swc_ecma_parser/src/parser/stmt.rs b/crates/swc_ecma_parser/src/parser/stmt.rs index 0cf7905ca478..9762cf12bc5f 100644 --- a/crates/swc_ecma_parser/src/parser/stmt.rs +++ b/crates/swc_ecma_parser/src/parser/stmt.rs @@ -753,7 +753,7 @@ impl<'a, I: Tokens> Parser { /// It's optional since es2019 fn parse_catch_param(&mut self) -> PResult> { if eat!(self, '(') { - let mut pat = self.parse_binding_pat_or_ident()?; + let mut pat = self.parse_binding_pat_or_ident(false)?; let type_ann_start = cur_pos!(self); @@ -967,7 +967,9 @@ impl<'a, I: Tokens> Parser { ) -> PResult { let start = cur_pos!(self); - let mut name = self.parse_binding_pat_or_ident()?; + let is_let_or_const = matches!(kind, VarDeclKind::Let | VarDeclKind::Const); + + let mut name = self.parse_binding_pat_or_ident(is_let_or_const)?; let definite = if self.input.syntax().typescript() { match name { @@ -1332,7 +1334,7 @@ impl<'a, I: Tokens> Parser { } if is_using_decl { - let name = self.parse_binding_ident()?; + let name = self.parse_binding_ident(false)?; let decl = VarDeclarator { name: name.into(), span: span!(self, start), diff --git a/crates/swc_ecma_parser/src/parser/stmt/module_item.rs b/crates/swc_ecma_parser/src/parser/stmt/module_item.rs index 3d12b988998e..9022a41dbd17 100644 --- a/crates/swc_ecma_parser/src/parser/stmt/module_item.rs +++ b/crates/swc_ecma_parser/src/parser/stmt/module_item.rs @@ -241,12 +241,12 @@ impl Parser { })); } - let maybe_as: Ident = self.parse_binding_ident()?.into(); + let maybe_as: Ident = self.parse_binding_ident(false)?.into(); if maybe_as.sym == "as" { if is!(self, IdentName) { // `import { type as as as } from 'mod'` // `import { type as as foo } from 'mod'` - let local: Ident = self.parse_binding_ident()?.into(); + let local: Ident = self.parse_binding_ident(false)?.into(); if type_only { self.emit_err(orig_name.span, SyntaxError::TS2206); @@ -289,7 +289,7 @@ impl Parser { } if eat!(self, "as") { - let local: Ident = self.parse_binding_ident()?.into(); + let local: Ident = self.parse_binding_ident(false)?.into(); return Ok(ImportSpecifier::Named(ImportNamedSpecifier { span: Span::new(start, local.span.hi()), local, @@ -316,7 +316,7 @@ impl Parser { } ModuleExportName::Str(orig_str) => { if eat!(self, "as") { - let local: Ident = self.parse_binding_ident()?.into(); + let local: Ident = self.parse_binding_ident(false)?.into(); Ok(ImportSpecifier::Named(ImportNamedSpecifier { span: Span::new(start, local.span.hi()), local, @@ -344,7 +344,7 @@ impl Parser { in_generator: false, ..self.ctx() }; - Ok(self.with_ctx(ctx).parse_binding_ident()?.into()) + Ok(self.with_ctx(ctx).parse_binding_ident(false)?.into()) } fn parse_export(&mut self, mut decorators: Vec) -> PResult { diff --git a/crates/swc_ecma_parser/src/parser/typescript.rs b/crates/swc_ecma_parser/src/parser/typescript.rs index 13e91ddb52bb..43ebfada133a 100644 --- a/crates/swc_ecma_parser/src/parser/typescript.rs +++ b/crates/swc_ecma_parser/src/parser/typescript.rs @@ -1247,7 +1247,7 @@ impl Parser { return Ok(true); } - if (is!(self, '{') || is!(self, '[')) && self.parse_binding_pat_or_ident().is_ok() { + if (is!(self, '{') || is!(self, '[')) && self.parse_binding_pat_or_ident(false).is_ok() { return Ok(true); } diff --git a/crates/swc_ecma_parser/tests/errors/issue-8269/input.js b/crates/swc_ecma_parser/tests/errors/issue-8269/input.js new file mode 100644 index 000000000000..b8d7e0dbc443 --- /dev/null +++ b/crates/swc_ecma_parser/tests/errors/issue-8269/input.js @@ -0,0 +1,9 @@ +{ + let let +} +{ + const let = 0 +} +{ + class let { } +} \ No newline at end of file diff --git a/crates/swc_ecma_parser/tests/errors/issue-8269/input.js.swc-stderr b/crates/swc_ecma_parser/tests/errors/issue-8269/input.js.swc-stderr new file mode 100644 index 000000000000..fdfd0568a025 --- /dev/null +++ b/crates/swc_ecma_parser/tests/errors/issue-8269/input.js.swc-stderr @@ -0,0 +1,7 @@ + x Unexpected token `let`. Expected let is reserved in const, let, class declaration + ,-[$DIR/tests/errors/issue-8269/input.js:2:1] + 1 | { + 2 | let let + : ^^^ + 3 | } + `---- diff --git a/crates/swc_ecma_parser/tests/test262-error-references/fail/0abefbc80bf651fa.js.swc-stderr b/crates/swc_ecma_parser/tests/test262-error-references/fail/0abefbc80bf651fa.js.swc-stderr index d2ea5d6cde61..8f306c6b8333 100644 --- a/crates/swc_ecma_parser/tests/test262-error-references/fail/0abefbc80bf651fa.js.swc-stderr +++ b/crates/swc_ecma_parser/tests/test262-error-references/fail/0abefbc80bf651fa.js.swc-stderr @@ -1,5 +1,5 @@ - x Expression expected + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/test262-parser/fail/0abefbc80bf651fa.js:1:1] 1 | for (let let;;;) {} - : ^ + : ^^^ `---- diff --git a/crates/swc_ecma_parser/tests/test262-error-references/fail/39551fb86dcd3b29.js.swc-stderr b/crates/swc_ecma_parser/tests/test262-error-references/fail/39551fb86dcd3b29.js.swc-stderr index e2bb9c8089f3..772a1973141a 100644 --- a/crates/swc_ecma_parser/tests/test262-error-references/fail/39551fb86dcd3b29.js.swc-stderr +++ b/crates/swc_ecma_parser/tests/test262-error-references/fail/39551fb86dcd3b29.js.swc-stderr @@ -1,5 +1,5 @@ - x Expression expected + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/test262-parser/fail/39551fb86dcd3b29.js:1:1] 1 | for (const let = 1;;;) {} - : ^ + : ^^^ `---- diff --git a/crates/swc_ecma_parser/tests/test262-error-references/fail/49624f905645b7d0.js.swc-stderr b/crates/swc_ecma_parser/tests/test262-error-references/fail/49624f905645b7d0.js.swc-stderr index a273b289121b..4fb7b4b551c5 100644 --- a/crates/swc_ecma_parser/tests/test262-error-references/fail/49624f905645b7d0.js.swc-stderr +++ b/crates/swc_ecma_parser/tests/test262-error-references/fail/49624f905645b7d0.js.swc-stderr @@ -1,5 +1,5 @@ - x Expression expected + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/test262-parser/fail/49624f905645b7d0.js:1:1] 1 | for (let x, y, z, let;;;) {} - : ^ + : ^^^ `---- diff --git a/crates/swc_ecma_parser/tests/test262-error-references/fail/ac4ee5fb095faad0.js.swc-stderr b/crates/swc_ecma_parser/tests/test262-error-references/fail/ac4ee5fb095faad0.js.swc-stderr index 89a09f177a17..c48b2fc2cc5a 100644 --- a/crates/swc_ecma_parser/tests/test262-error-references/fail/ac4ee5fb095faad0.js.swc-stderr +++ b/crates/swc_ecma_parser/tests/test262-error-references/fail/ac4ee5fb095faad0.js.swc-stderr @@ -1,5 +1,5 @@ - x Expression expected + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/test262-parser/fail/ac4ee5fb095faad0.js:1:1] 1 | for (let x, y, z, let = 1;;;) {} - : ^ + : ^^^ `---- diff --git a/crates/swc_ecma_parser/tests/test262-error-references/fail/b02b296bd115b9b9.js.swc-stderr b/crates/swc_ecma_parser/tests/test262-error-references/fail/b02b296bd115b9b9.js.swc-stderr index a479a2bfee91..89fe01b5f661 100644 --- a/crates/swc_ecma_parser/tests/test262-error-references/fail/b02b296bd115b9b9.js.swc-stderr +++ b/crates/swc_ecma_parser/tests/test262-error-references/fail/b02b296bd115b9b9.js.swc-stderr @@ -1,5 +1,5 @@ - x Expression expected + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/test262-parser/fail/b02b296bd115b9b9.js:1:1] 1 | for (const x = 1, y = 2, z = 3, let = 0;;;) {} - : ^ + : ^^^ `---- diff --git a/crates/swc_ecma_parser/tests/typescript-errors/issue-1391/case2/input.ts.swc-stderr b/crates/swc_ecma_parser/tests/typescript-errors/issue-1391/case2/input.ts.swc-stderr index 31ec1780b899..955093bc8a17 100644 --- a/crates/swc_ecma_parser/tests/typescript-errors/issue-1391/case2/input.ts.swc-stderr +++ b/crates/swc_ecma_parser/tests/typescript-errors/issue-1391/case2/input.ts.swc-stderr @@ -1,12 +1,6 @@ - x `let` cannot be used as an identifier in strict mode + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/typescript-errors/issue-1391/case2/input.ts:2:1] 1 | let a = 0, 2 | let b = 1; : ^^^ `---- - x Expected a semicolon - ,-[$DIR/tests/typescript-errors/issue-1391/case2/input.ts:2:1] - 1 | let a = 0, - 2 | let b = 1; - : ^ - `---- diff --git a/crates/swc_ecma_parser/tests/typescript-errors/let/index.ts.swc-stderr b/crates/swc_ecma_parser/tests/typescript-errors/let/index.ts.swc-stderr index 0904eccb1ef3..bf995a99780d 100644 --- a/crates/swc_ecma_parser/tests/typescript-errors/let/index.ts.swc-stderr +++ b/crates/swc_ecma_parser/tests/typescript-errors/let/index.ts.swc-stderr @@ -1,4 +1,4 @@ - x `let` cannot be used as an identifier in strict mode + x Unexpected token `let`. Expected let is reserved in const, let, class declaration ,-[$DIR/tests/typescript-errors/let/index.ts:3:1] 2 | 3 | let let; diff --git a/crates/swc_ecma_parser/tests/typescript.rs b/crates/swc_ecma_parser/tests/typescript.rs index e0263609f9f9..c7aa1313936d 100644 --- a/crates/swc_ecma_parser/tests/typescript.rs +++ b/crates/swc_ecma_parser/tests/typescript.rs @@ -81,6 +81,7 @@ fn spec(file: PathBuf) { #[testing::fixture( "tests/tsc/**/*.ts", exclude( + "for-of51.ts", "parserArrowFunctionExpression11", "esDecorators-decoratorExpression.1" )