diff --git a/ecmascript/parser/Cargo.toml b/ecmascript/parser/Cargo.toml index d23c4aa85944..da0402b72e8b 100644 --- a/ecmascript/parser/Cargo.toml +++ b/ecmascript/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_ecma_parser" -version = "0.33.1" +version = "0.33.2" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" diff --git a/ecmascript/parser/src/error.rs b/ecmascript/parser/src/error.rs index a8dd1b1451ca..89e4bb966bd1 100644 --- a/ecmascript/parser/src/error.rs +++ b/ecmascript/parser/src/error.rs @@ -364,6 +364,7 @@ impl Error { Eof => "Unexpected eof".into(), + TS2703 => "The operand of a delete operator must be a property reference.".into(), // TODO: _ => format!("{:?}", kind).into(), }; diff --git a/ecmascript/parser/src/parser/class_and_fn.rs b/ecmascript/parser/src/parser/class_and_fn.rs index 76ffad231b21..037f19ec01ed 100644 --- a/ecmascript/parser/src/parser/class_and_fn.rs +++ b/ecmascript/parser/src/parser/class_and_fn.rs @@ -750,6 +750,7 @@ impl<'a, I: Tokens> Parser { let ctx = Context { in_class_prop: true, in_method: false, + include_in_expr: true, ..self.ctx() }; self.with_ctx(ctx).parse_with(|p| { diff --git a/ecmascript/parser/src/parser/expr/ops.rs b/ecmascript/parser/src/parser/expr/ops.rs index 2cb5f6ab64ca..d05aff9f9760 100644 --- a/ecmascript/parser/src/parser/expr/ops.rs +++ b/ecmascript/parser/src/parser/expr/ops.rs @@ -284,8 +284,13 @@ impl<'a, I: Tokens> Parser { _ => e, } } - match *arg { + match &*arg { Expr::Member(..) => {} + Expr::OptChain(e) + if match &*e.expr { + Expr::Member(..) => true, + _ => false, + } => {} _ => self.emit_err(unwrap_paren(&arg).span(), SyntaxError::TS2703), } } diff --git a/ecmascript/parser/tests/typescript/issue-944/input.ts b/ecmascript/parser/tests/typescript/issue-944/input.ts new file mode 100644 index 000000000000..7e5295777b9e --- /dev/null +++ b/ecmascript/parser/tests/typescript/issue-944/input.ts @@ -0,0 +1,3 @@ +class CTest { + myFunc = () => "key" in {}; +} \ No newline at end of file diff --git a/ecmascript/parser/tests/typescript/issue-944/input.ts.json b/ecmascript/parser/tests/typescript/issue-944/input.ts.json new file mode 100644 index 000000000000..8905beb64895 --- /dev/null +++ b/ecmascript/parser/tests/typescript/issue-944/input.ts.json @@ -0,0 +1,109 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 47, + "ctxt": 0 + }, + "body": [ + { + "type": "ClassDeclaration", + "identifier": { + "type": "Identifier", + "span": { + "start": 6, + "end": 11, + "ctxt": 0 + }, + "value": "CTest", + "typeAnnotation": null, + "optional": false + }, + "declare": false, + "span": { + "start": 0, + "end": 47, + "ctxt": 0 + }, + "decorators": [], + "body": [ + { + "type": "ClassProperty", + "span": { + "start": 18, + "end": 45, + "ctxt": 0 + }, + "key": { + "type": "Identifier", + "span": { + "start": 18, + "end": 24, + "ctxt": 0 + }, + "value": "myFunc", + "typeAnnotation": null, + "optional": false + }, + "value": { + "type": "ArrowFunctionExpression", + "span": { + "start": 27, + "end": 44, + "ctxt": 0 + }, + "params": [], + "body": { + "type": "BinaryExpression", + "span": { + "start": 33, + "end": 44, + "ctxt": 0 + }, + "operator": "in", + "left": { + "type": "StringLiteral", + "span": { + "start": 33, + "end": 38, + "ctxt": 0 + }, + "value": "key", + "hasEscape": false + }, + "right": { + "type": "ObjectExpression", + "span": { + "start": 42, + "end": 44, + "ctxt": 0 + }, + "properties": [] + } + }, + "async": false, + "generator": false, + "typeParameters": null, + "returnType": null + }, + "typeAnnotation": null, + "isStatic": false, + "decorators": [], + "computed": false, + "accessibility": null, + "isAbstract": false, + "isOptional": false, + "readonly": false, + "declare": false, + "definite": false + } + ], + "superClass": null, + "isAbstract": false, + "typeParams": null, + "superTypeParams": null, + "implements": [] + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/issue-947/input.ts b/ecmascript/parser/tests/typescript/issue-947/input.ts new file mode 100644 index 000000000000..38cf25becd6c --- /dev/null +++ b/ecmascript/parser/tests/typescript/issue-947/input.ts @@ -0,0 +1 @@ +delete obj?.myProp; \ No newline at end of file diff --git a/ecmascript/parser/tests/typescript/issue-947/input.ts.json b/ecmascript/parser/tests/typescript/issue-947/input.ts.json new file mode 100644 index 000000000000..d94383f18851 --- /dev/null +++ b/ecmascript/parser/tests/typescript/issue-947/input.ts.json @@ -0,0 +1,72 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 19, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 19, + "ctxt": 0 + }, + "expression": { + "type": "UnaryExpression", + "span": { + "start": 0, + "end": 18, + "ctxt": 0 + }, + "operator": "delete", + "argument": { + "type": "OptionalChainingExpression", + "span": { + "start": 7, + "end": 18, + "ctxt": 0 + }, + "questionDotToken": { + "start": 10, + "end": 11, + "ctxt": 0 + }, + "expr": { + "type": "MemberExpression", + "span": { + "start": 7, + "end": 18, + "ctxt": 0 + }, + "object": { + "type": "Identifier", + "span": { + "start": 7, + "end": 10, + "ctxt": 0 + }, + "value": "obj", + "typeAnnotation": null, + "optional": false + }, + "property": { + "type": "Identifier", + "span": { + "start": 12, + "end": 18, + "ctxt": 0 + }, + "value": "myProp", + "typeAnnotation": null, + "optional": false + }, + "computed": false + } + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/v4/issue-941/input.ts.json b/ecmascript/parser/tests/typescript/v4/issue-941/input.ts.json new file mode 100644 index 000000000000..af6fb7641b97 --- /dev/null +++ b/ecmascript/parser/tests/typescript/v4/issue-941/input.ts.json @@ -0,0 +1,73 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 30, + "ctxt": 0 + }, + "body": [ + { + "type": "TryStatement", + "span": { + "start": 0, + "end": 30, + "ctxt": 0 + }, + "block": { + "type": "BlockStatement", + "span": { + "start": 4, + "end": 7, + "ctxt": 0 + }, + "stmts": [] + }, + "handler": { + "type": "CatchClause", + "span": { + "start": 8, + "end": 30, + "ctxt": 0 + }, + "param": { + "type": "Identifier", + "span": { + "start": 15, + "end": 16, + "ctxt": 0 + }, + "value": "e", + "typeAnnotation": { + "type": "TsTypeAnnotation", + "span": { + "start": 16, + "end": 25, + "ctxt": 0 + }, + "typeAnnotation": { + "type": "TsKeywordType", + "span": { + "start": 18, + "end": 25, + "ctxt": 0 + }, + "kind": "unknown" + } + }, + "optional": false + }, + "body": { + "type": "BlockStatement", + "span": { + "start": 27, + "end": 30, + "ctxt": 0 + }, + "stmts": [] + } + }, + "finalizer": null + } + ], + "interpreter": null +} diff --git a/ecmascript/transforms/src/compat/es2020.rs b/ecmascript/transforms/src/compat/es2020.rs index 43f06a0246e4..f11b4066d46f 100644 --- a/ecmascript/transforms/src/compat/es2020.rs +++ b/ecmascript/transforms/src/compat/es2020.rs @@ -1,5 +1,6 @@ pub use self::{ - class_properties::class_properties, nullish_coalescing::nullish_coalescing, + class_properties::{class_properties, typescript_class_properties}, + nullish_coalescing::nullish_coalescing, opt_chaining::optional_chaining, }; diff --git a/ecmascript/transforms/src/compat/es2020/class_properties.rs b/ecmascript/transforms/src/compat/es2020/class_properties.rs index 122c2777fcf0..63ca3655951b 100644 --- a/ecmascript/transforms/src/compat/es2020/class_properties.rs +++ b/ecmascript/transforms/src/compat/es2020/class_properties.rs @@ -30,11 +30,23 @@ mod used_name; /// /// We use custom helper to handle export defaul class pub fn class_properties() -> impl Fold { - ClassProperties { mark: Mark::root() } + ClassProperties { + typescript: false, + mark: Mark::root(), + } +} + +/// Class properties pass for the typescript. +pub fn typescript_class_properties() -> impl Fold { + ClassProperties { + typescript: true, + mark: Mark::root(), + } } #[derive(Clone)] struct ClassProperties { + typescript: bool, mark: Mark, } @@ -329,77 +341,135 @@ impl ClassProperties { ); } - let key = match *prop.key { - Expr::Ident(ref i) if !prop.computed => Lit::Str(Str { - span: i.span, - value: i.sym.clone(), - has_escape: false, - }) - .as_arg(), - Expr::Lit(ref lit) if !prop.computed => lit.clone().as_arg(), - - _ => { - let (ident, aliased) = if let Expr::Ident(ref i) = *prop.key { - if used_key_names.contains(&i.sym) { - (alias_ident_for(&prop.key, "_ref"), true) + let key = if self.typescript { + // `b` in + // + // class A { + // b = 'foo'; + // } + prop.key + } else { + match *prop.key { + Expr::Ident(ref i) if !prop.computed => { + Box::new(Expr::from(Lit::Str(Str { + span: i.span, + value: i.sym.clone(), + has_escape: false, + }))) + } + Expr::Lit(ref lit) if !prop.computed => { + Box::new(Expr::from(lit.clone())) + } + + _ => { + let (ident, aliased) = if let Expr::Ident(ref i) = *prop.key { + if used_key_names.contains(&i.sym) { + (alias_ident_for(&prop.key, "_ref"), true) + } else { + alias_if_required(&prop.key, "_ref") + } } else { alias_if_required(&prop.key, "_ref") + }; + // ident.span = ident.span.apply_mark(Mark::fresh(Mark::root())); + if aliased { + // Handle computed property + vars.push(VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(ident.clone()), + init: Some(prop.key), + definite: false, + }); } - } else { - alias_if_required(&prop.key, "_ref") - }; - // ident.span = ident.span.apply_mark(Mark::fresh(Mark::root())); - if aliased { - // Handle computed property - vars.push(VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(ident.clone()), - init: Some(prop.key), - definite: false, - }); + Box::new(Expr::from(ident)) } - ident.as_arg() } }; - let value = prop.value.unwrap_or_else(|| undefined(prop_span)).as_arg(); + let value = prop.value.unwrap_or_else(|| undefined(prop_span)); + let value = if prop.is_static { + value + .fold_with(&mut SuperFieldAccessFolder { + class_name: &ident, + vars: &mut vars, + constructor_this_mark: None, + is_static: true, + folding_constructor: false, + in_injected_define_property_call: false, + in_nested_scope: false, + this_alias_mark: None, + }) + .fold_with(&mut ThisInStaticFolder { + ident: ident.clone(), + }) + } else { + value + }; - let callee = helper!(define_property, "defineProperty"); + if self.typescript { + if prop.is_static { + extra_stmts.push( + AssignExpr { + span: DUMMY_SP, + left: PatOrExpr::Expr(Box::new( + MemberExpr { + span: DUMMY_SP, + obj: ident.clone().as_obj(), + computed: false, + prop: key, + } + .into(), + )), + op: op!("="), + right: value, + } + .into_stmt(), + ); + } else { + constructor_exprs.push(Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: (PatOrExpr::Expr(Box::new( + MemberExpr { + span: DUMMY_SP, + obj: ThisExpr { span: DUMMY_SP }.as_obj(), + computed: false, + prop: key, + } + .into(), + ))), + op: op!("="), + right: value, + }))); + } + } else { + let callee = helper!(define_property, "defineProperty"); - if prop.is_static { - extra_stmts.push( - CallExpr { + if prop.is_static { + extra_stmts.push( + CallExpr { + span: DUMMY_SP, + callee, + args: vec![ + ident.clone().as_arg(), + key.as_arg(), + value.as_arg(), + ], + type_args: Default::default(), + } + .into_stmt(), + ) + } else { + constructor_exprs.push(Box::new(Expr::Call(CallExpr { span: DUMMY_SP, callee, args: vec![ - ident.clone().as_arg(), - key, - value - .fold_with(&mut SuperFieldAccessFolder { - class_name: &ident, - vars: &mut vars, - constructor_this_mark: None, - is_static: true, - folding_constructor: false, - in_injected_define_property_call: false, - in_nested_scope: false, - this_alias_mark: None, - }) - .fold_with(&mut ThisInStaticFolder { - ident: ident.clone(), - }), + ThisExpr { span: DUMMY_SP }.as_arg(), + key.as_arg(), + value.as_arg(), ], type_args: Default::default(), - } - .into_stmt(), - ) - } else { - constructor_exprs.push(Box::new(Expr::Call(CallExpr { - span: DUMMY_SP, - callee, - args: vec![ThisExpr { span: DUMMY_SP }.as_arg(), key, value], - type_args: Default::default(), - }))); + }))); + } } } ClassMember::PrivateProp(prop) => { @@ -524,6 +594,35 @@ impl ClassProperties { ) } + /// # Legacy support. + /// + /// ## Why is this required? + /// + /// Hygiene data of + /// + ///```ts + /// class A { + /// b = this.a; + /// constructor(a){ + /// this.a = a; + /// } + /// } + /// ``` + /// + /// is + /// + ///```ts + /// class A0 { + /// constructor(a1){ + /// this.a0 = a0; + /// this.b0 = this.a0; + /// } + /// } + /// ``` + /// + /// which is valid only for es2020 properties. + /// + /// Legacy proposal which is used by typescript requires different hygiene. #[allow(clippy::vec_box)] fn process_constructor( &mut self, @@ -534,28 +633,32 @@ impl ClassProperties { ) -> Option { let constructor = constructor .map(|c| { - let mut folder = UsedNameRenamer { - mark: Mark::fresh(Mark::root()), - used_names, - }; - - // Handle collisions like - // - // var foo = "bar"; - // - // class Foo { - // bar = foo; - // static bar = baz; - // - // constructor() { - // var foo = "foo"; - // var baz = "baz"; - // } - // } - let body = c.body.fold_with(&mut folder); - - let params = c.params.fold_with(&mut folder); - Constructor { body, params, ..c } + if self.typescript { + c + } else { + let mut folder = UsedNameRenamer { + mark: Mark::fresh(Mark::root()), + used_names, + }; + + // Handle collisions like + // + // var foo = "bar"; + // + // class Foo { + // bar = foo; + // static bar = baz; + // + // constructor() { + // var foo = "foo"; + // var baz = "baz"; + // } + // } + let body = c.body.fold_with(&mut folder); + + let params = c.params.fold_with(&mut folder); + Constructor { body, params, ..c } + } }) .or_else(|| { if constructor_exprs.is_empty() { @@ -565,8 +668,19 @@ impl ClassProperties { } }); - if let Some(c) = constructor { - Some(inject_after_super(c, constructor_exprs)) + if let Some(mut c) = constructor { + if self.typescript { + // Append properties + c.body + .as_mut() + .unwrap() + .stmts + .extend(constructor_exprs.into_iter().map(|v| v.into_stmt())); + Some(c) + } else { + // Prepend properties + Some(inject_after_super(c, constructor_exprs)) + } } else { None } diff --git a/ecmascript/transforms/tests/typescript_strip.rs b/ecmascript/transforms/tests/typescript_strip.rs index 4e979eb852f8..604e8ba0adb1 100644 --- a/ecmascript/transforms/tests/typescript_strip.rs +++ b/ecmascript/transforms/tests/typescript_strip.rs @@ -1,15 +1,22 @@ #![feature(test)] use swc_common::chain; -use swc_ecma_transforms::{resolver, typescript::strip}; +use swc_ecma_transforms::{ + compat::es2020::typescript_class_properties, resolver, typescript::strip, +}; +use swc_ecma_visit::Fold; #[macro_use] mod common; +fn tr() -> impl Fold { + strip() +} + macro_rules! to { ($name:ident, $from:expr, $to:expr) => { test!( ::swc_ecma_parser::Syntax::Typescript(Default::default()), - |_| strip(), + |_| tr(), $name, $from, $to, @@ -695,3 +702,37 @@ to!( } }" ); + +test!( + ::swc_ecma_parser::Syntax::Typescript(Default::default()), + |_| chain!(tr(), typescript_class_properties()), + issue_930_instance, + "class A { + b = this.a; + constructor(a){ + this.a = a; + } + }", + "class A { + constructor(a) { + this.a = a; + this.b = this.a; + } +}" +); + +test!( + ::swc_ecma_parser::Syntax::Typescript(Default::default()), + |_| chain!(tr(), typescript_class_properties()), + issue_930_static, + "class A { + static b = 'foo'; + constructor(a){ + } + }", + "class A { + constructor(a) { + } + } + A.b = 'foo';" +); diff --git a/src/builder.rs b/src/builder.rs index fc516ef24308..7c3e7212d5bd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -144,10 +144,17 @@ impl<'a, 'b, P: swc_ecma_visit::Fold> PassBuilder<'a, 'b, P> { compat::es2020::optional_chaining(), self.target < JscTarget::Es2020 ), - Optional::new( - compat::es2020::class_properties(), - self.target < JscTarget::Es2020 - ), + if syntax.typescript() { + Either::Left(Optional::new( + compat::es2020::typescript_class_properties(), + self.target < JscTarget::Es2020, + )) + } else { + Either::Right(Optional::new( + compat::es2020::class_properties(), + self.target < JscTarget::Es2020, + )) + }, Optional::new(compat::es2018(), self.target <= JscTarget::Es2018), Optional::new(compat::es2017(), self.target <= JscTarget::Es2017), Optional::new(compat::es2016(), self.target <= JscTarget::Es2016),