From b1e4122b0212e6c6ee371fe524a89c6e95c82121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Tue, 24 Dec 2019 17:01:32 +0900 Subject: [PATCH] Nullish coalescing / optional chaining / comments (#529) swc_ecma_ast: - rename `TsOptChain` to `OptChainExpr` (Fixes #525) - add `BinOp::NullishCoalescing` swc_ecma_parser: - parse `??` (Fixes #526) swc_ecma_transforms: - remap comments from fixer (Fixes #528) --- README.md | 12 + common/src/comments.rs | 28 +++ ecmascript/ast/Cargo.toml | 2 +- ecmascript/ast/src/expr.rs | 14 +- ecmascript/ast/src/lib.rs | 16 +- ecmascript/ast/src/macros.rs | 3 + ecmascript/ast/src/operators.rs | 4 + ecmascript/ast/src/typescript.rs | 6 - ecmascript/codegen/Cargo.toml | 6 +- ecmascript/codegen/src/lib.rs | 31 ++- ecmascript/codegen/src/typescript.rs | 5 - ecmascript/codegen/src/util.rs | 2 +- ecmascript/parser/Cargo.toml | 4 +- ecmascript/parser/src/error.rs | 10 + ecmascript/parser/src/lexer.rs | 14 +- ecmascript/parser/src/lib.rs | 17 ++ ecmascript/parser/src/macros.rs | 3 + ecmascript/parser/src/parser/expr.rs | 2 +- ecmascript/parser/src/parser/expr/ops.rs | 41 ++++ ecmascript/parser/src/parser/util.rs | 2 +- ecmascript/parser/src/token.rs | 4 + .../no-paren-and-nullish/input.ts | 1 + .../no-paren-and-nullish/input.ts.stderr | 6 + .../no-paren-nullish-and/input.ts | 1 + .../no-paren-nullish-and/input.ts.stderr | 6 + .../no-paren-nullish-or/input.ts | 1 + .../no-paren-nullish-or/input.ts.stderr | 6 + .../no-paren-or-nullish/input.ts | 1 + .../no-paren-or-nullish/input.ts.stderr | 6 + .../and-nullish/input.ts | 1 + .../and-nullish/input.ts.json | 78 +++++++ .../associativity/input.ts | 1 + .../associativity/input.ts.json | 70 ++++++ .../expression/input.ts | 1 + .../expression/input.ts.json | 48 ++++ .../multiline/input.ts | 3 + .../multiline/input.ts.json | 70 ++++++ .../nullish-and/input.ts | 1 + .../nullish-and/input.ts.json | 78 +++++++ .../nullish-or/input.ts | 1 + .../nullish-or/input.ts.json | 78 +++++++ .../or-nullish/input.ts | 1 + .../or-nullish/input.ts.json | 78 +++++++ .../type-arguments/input.ts.json | 2 +- ecmascript/transforms/Cargo.toml | 6 +- .../src/compat/es2015/destructuring.rs | 2 +- ecmascript/transforms/src/fixer.rs | 216 +++++++++++------- .../src/optimization/simplify/expr.rs | 2 +- ecmascript/transforms/src/proposals.rs | 6 +- .../{typescript => proposals}/opt_chaining.rs | 20 +- .../opt_chaining/tests.rs | 0 ecmascript/transforms/src/typescript.rs | 2 - ecmascript/transforms/src/util.rs | 8 +- src/config.rs | 4 +- src/lib.rs | 34 ++- tests/projects.rs | 22 ++ tests/projects/issue-528/input.js | 11 + 57 files changed, 954 insertions(+), 144 deletions(-) create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts.stderr create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts.stderr create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts.stderr create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts create mode 100644 ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts.stderr create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts.json create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts create mode 100644 ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts.json rename ecmascript/transforms/src/{typescript => proposals}/opt_chaining.rs (94%) rename ecmascript/transforms/src/{typescript => proposals}/opt_chaining/tests.rs (100%) create mode 100644 tests/projects/issue-528/input.js diff --git a/README.md b/README.md index a599d779e7e..5f2553d1cca 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,18 @@ New generation javascript to old-days javascript. - [x] optional-catch-binding - [ ] unicode-property-regex +- es2019 (stage 4) + - [x] optional catch binding + - [x] big int literal + +- stage 3 + - [x] class fields + - [x] numeric separator + - [ ] nullish coalescing + +- stage 2 + - [x] decorators + - react - [x] jsx diff --git a/common/src/comments.rs b/common/src/comments.rs index 452d290e790..a316c80f728 100644 --- a/common/src/comments.rs +++ b/common/src/comments.rs @@ -31,6 +31,34 @@ impl Comments { pub fn leading_comments(&self, pos: BytePos) -> Option>> { self.leading.get(&pos) } + + pub fn move_leading(&self, from: BytePos, to: BytePos) { + let cmt = self.leading.remove(&from); + + if let Some(cmt) = cmt { + self.leading.alter(to, |v| match v { + Some(mut value) => { + value.extend(cmt); + Some(value) + } + None => Some(cmt), + }); + } + } + + pub fn move_trailing(&self, from: BytePos, to: BytePos) { + let cmt = self.trailing.remove(&from); + + if let Some(cmt) = cmt { + self.trailing.alter(to, |v| match v { + Some(mut value) => { + value.extend(cmt); + Some(value) + } + None => Some(cmt), + }); + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/ecmascript/ast/Cargo.toml b/ecmascript/ast/Cargo.toml index de1d91fe094..e5ed1cb070c 100644 --- a/ecmascript/ast/Cargo.toml +++ b/ecmascript/ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_ecma_ast" -version = "0.12.1" +version = "0.13.0" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" diff --git a/ecmascript/ast/src/expr.rs b/ecmascript/ast/src/expr.rs index 61b809258fb..c70cf0410c2 100644 --- a/ecmascript/ast/src/expr.rs +++ b/ecmascript/ast/src/expr.rs @@ -10,8 +10,8 @@ use crate::{ prop::Prop, stmt::BlockStmt, typescript::{ - TsAsExpr, TsConstAssertion, TsNonNullExpr, TsOptChain, TsTypeAnn, TsTypeAssertion, - TsTypeCastExpr, TsTypeParamDecl, TsTypeParamInstantiation, + TsAsExpr, TsConstAssertion, TsNonNullExpr, TsTypeAnn, TsTypeAssertion, TsTypeCastExpr, + TsTypeParamDecl, TsTypeParamInstantiation, }, Invalid, }; @@ -144,8 +144,8 @@ pub enum Expr { #[tag("PrivateName")] PrivateName(PrivateName), - #[tag("TsOptionalChainingExpression")] - TsOptChain(TsOptChain), + #[tag("OptionalChainingExpression")] + OptChain(OptChainExpr), #[tag("Invalid")] Invalid(Invalid), @@ -544,6 +544,12 @@ impl From for Expr { } } +#[ast_node("OptionalChainingExpression")] +pub struct OptChainExpr { + pub span: Span, + pub expr: Box, +} + test_de!( jsx_element, JSXElement, diff --git a/ecmascript/ast/src/lib.rs b/ecmascript/ast/src/lib.rs index 0d704349b8c..d26ee436bd7 100644 --- a/ecmascript/ast/src/lib.rs +++ b/ecmascript/ast/src/lib.rs @@ -15,8 +15,8 @@ pub use self::{ expr::{ ArrayLit, ArrowExpr, AssignExpr, AwaitExpr, BinExpr, BlockStmtOrExpr, CallExpr, ClassExpr, CondExpr, Expr, ExprOrSpread, ExprOrSuper, FnExpr, MemberExpr, MetaPropExpr, NewExpr, - ObjectLit, ParenExpr, PatOrExpr, PropOrSpread, SeqExpr, SpreadElement, Super, TaggedTpl, - ThisExpr, Tpl, TplElement, UnaryExpr, UpdateExpr, YieldExpr, + ObjectLit, OptChainExpr, ParenExpr, PatOrExpr, PropOrSpread, SeqExpr, SpreadElement, Super, + TaggedTpl, ThisExpr, Tpl, TplElement, UnaryExpr, UpdateExpr, YieldExpr, }, function::{Function, PatOrTsParamProp}, ident::{Ident, IdentExt, PrivateName}, @@ -56,12 +56,12 @@ pub use self::{ TsInterfaceBody, TsInterfaceDecl, TsIntersectionType, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsMappedType, TsMethodSignature, TsModuleBlock, TsModuleDecl, TsModuleName, TsModuleRef, TsNamespaceBody, TsNamespaceDecl, TsNamespaceExportDecl, - TsNonNullExpr, TsOptChain, TsOptionalType, TsParamProp, TsParamPropParam, - TsParenthesizedType, TsPropertySignature, TsQualifiedName, TsRestType, TsSignatureDecl, - TsThisType, TsThisTypeOrIdent, TsTupleType, TsType, TsTypeAliasDecl, TsTypeAnn, - TsTypeAssertion, TsTypeCastExpr, TsTypeElement, TsTypeLit, TsTypeOperator, - TsTypeOperatorOp, TsTypeParam, TsTypeParamDecl, TsTypeParamInstantiation, TsTypePredicate, - TsTypeQuery, TsTypeQueryExpr, TsTypeRef, TsUnionOrIntersectionType, TsUnionType, + TsNonNullExpr, TsOptionalType, TsParamProp, TsParamPropParam, TsParenthesizedType, + TsPropertySignature, TsQualifiedName, TsRestType, TsSignatureDecl, TsThisType, + TsThisTypeOrIdent, TsTupleType, TsType, TsTypeAliasDecl, TsTypeAnn, TsTypeAssertion, + TsTypeCastExpr, TsTypeElement, TsTypeLit, TsTypeOperator, TsTypeOperatorOp, TsTypeParam, + TsTypeParamDecl, TsTypeParamInstantiation, TsTypePredicate, TsTypeQuery, TsTypeQueryExpr, + TsTypeRef, TsUnionOrIntersectionType, TsUnionType, }, }; use swc_common::{ast_node, Span}; diff --git a/ecmascript/ast/src/macros.rs b/ecmascript/ast/src/macros.rs index 5c8d1b24fc1..a6d29fd1a53 100644 --- a/ecmascript/ast/src/macros.rs +++ b/ecmascript/ast/src/macros.rs @@ -106,6 +106,9 @@ macro_rules! op { ("**") => { $crate::BinaryOp::Exp }; + ("??") => { + $crate::BinaryOp::NullishCoalescing + }; ("=") => { $crate::AssignOp::Assign diff --git a/ecmascript/ast/src/operators.rs b/ecmascript/ast/src/operators.rs index f7281bfed4c..f8304db5ecc 100644 --- a/ecmascript/ast/src/operators.rs +++ b/ecmascript/ast/src/operators.rs @@ -85,6 +85,10 @@ pub enum BinaryOp { /// `**` #[kind(precedence = "11")] Exp, + + /// `??` + #[kind(precedence = "1")] + NullishCoalescing, } #[derive(StringEnum, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] diff --git a/ecmascript/ast/src/typescript.rs b/ecmascript/ast/src/typescript.rs index 7fb2d8d5baf..963fb75906a 100644 --- a/ecmascript/ast/src/typescript.rs +++ b/ecmascript/ast/src/typescript.rs @@ -837,9 +837,3 @@ pub struct TsConstAssertion { pub span: Span, pub expr: Box, } - -#[ast_node("TsOptionalChainingExpression")] -pub struct TsOptChain { - pub span: Span, - pub expr: Box, -} diff --git a/ecmascript/codegen/Cargo.toml b/ecmascript/codegen/Cargo.toml index 15314bc6f20..fb4ab83c793 100644 --- a/ecmascript/codegen/Cargo.toml +++ b/ecmascript/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_ecma_codegen" -version = "0.10.0" +version = "0.11.0" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" @@ -13,11 +13,11 @@ bitflags = "1" hashbrown = "0.6" swc_atoms = { version = "0.2", path ="../../atoms" } swc_common = { version = "0.4.0", path ="../../common" } -swc_ecma_ast = { version = "0.12.0", path ="../ast" } +swc_ecma_ast = { version = "0.13.0", path ="../ast" } swc_ecma_codegen_macros = { version = "0.4", path ="./macros" } sourcemap = "4.1.1" num-bigint = { version = "0.2", features = ["serde"] } [dev-dependencies] testing = { version = "0.4", path ="../../testing" } -swc_ecma_parser = { version = "0.14", path ="../parser" } \ No newline at end of file +swc_ecma_parser = { version = "0.15", path ="../parser" } \ No newline at end of file diff --git a/ecmascript/codegen/src/lib.rs b/ecmascript/codegen/src/lib.rs index 1f5db1c0fd2..f13cfb3b23b 100644 --- a/ecmascript/codegen/src/lib.rs +++ b/ecmascript/codegen/src/lib.rs @@ -480,11 +480,40 @@ impl<'a> Emitter<'a> { Expr::TsTypeAssertion(ref n) => emit!(n), Expr::TsConstAssertion(ref n) => emit!(n), Expr::TsTypeCast(ref n) => emit!(n), - Expr::TsOptChain(ref n) => emit!(n), + Expr::OptChain(ref n) => emit!(n), Expr::Invalid(..) => unimplemented!("emit Expr::Invalid"), } } + #[emitter] + pub fn emit_opt_chain(&mut self, n: &OptChainExpr) -> Result { + self.emit_leading_comments_of_pos(n.span().lo())?; + + match *n.expr { + Expr::Member(ref e) => { + emit!(e.obj); + self.wr.write_operator("?.")?; + + if e.computed { + punct!("["); + emit!(e.prop); + punct!("]"); + } else { + emit!(e.prop); + } + } + Expr::Call(ref e) => { + emit!(e.callee); + self.wr.write_operator("?.")?; + + punct!("("); + self.emit_expr_or_spreads(n.span(), &e.args, ListFormat::CallExpressionArguments)?; + punct!(")"); + } + _ => {} + } + } + #[emitter] pub fn emit_call_expr(&mut self, node: &CallExpr) -> Result { self.emit_leading_comments_of_pos(node.span().lo())?; diff --git a/ecmascript/codegen/src/typescript.rs b/ecmascript/codegen/src/typescript.rs index 1c4478223fc..8679deb8d1d 100644 --- a/ecmascript/codegen/src/typescript.rs +++ b/ecmascript/codegen/src/typescript.rs @@ -16,11 +16,6 @@ impl<'a> Emitter<'a> { unimplemented!("emit_ts_array_type") } - #[emitter] - pub fn emit_ts_opt_chain(&mut self, n: &TsOptChain) -> Result { - unimplemented!("emit_ts_opt_chain") - } - #[emitter] pub fn emit_ts_as_expr(&mut self, n: &TsAsExpr) -> Result { unimplemented!("emit_ts_as_expr") diff --git a/ecmascript/codegen/src/util.rs b/ecmascript/codegen/src/util.rs index 8e2bd233560..930faffc8aa 100644 --- a/ecmascript/codegen/src/util.rs +++ b/ecmascript/codegen/src/util.rs @@ -210,7 +210,7 @@ impl StartsWithAlphaNum for Expr { // TODO Expr::TsTypeCast(..) => true, - Expr::TsOptChain(ref e) => e.expr.starts_with_alpha_num(), + Expr::OptChain(ref e) => e.expr.starts_with_alpha_num(), Expr::Invalid(..) => true, } diff --git a/ecmascript/parser/Cargo.toml b/ecmascript/parser/Cargo.toml index 25fc527f2ff..bd3e8be66a8 100644 --- a/ecmascript/parser/Cargo.toml +++ b/ecmascript/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_ecma_parser" -version = "0.14.0" +version = "0.15.0" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" @@ -18,7 +18,7 @@ verify = ["fold"] [dependencies] swc_atoms = { version = "0.2", path ="../../atoms" } swc_common = { version = "0.4.0", path ="../../common" } -swc_ecma_ast = { version = "0.12.0", path ="../ast" } +swc_ecma_ast = { version = "0.13.0", path ="../ast" } swc_ecma_parser_macros = { package = "swc_ecma_parser_macros", version = "0.4", path ="./macros" } enum_kind = { version = "0.2", path ="../../macros/enum_kind" } unicode-xid = "0.2" diff --git a/ecmascript/parser/src/error.rs b/ecmascript/parser/src/error.rs index 95eb8ab627e..998d93e6de3 100644 --- a/ecmascript/parser/src/error.rs +++ b/ecmascript/parser/src/error.rs @@ -103,6 +103,9 @@ pub enum SyntaxError { AwaitStar, ReservedWordInObjShorthandOrPat, + NullishCoalescingWithLogicalOp, + NullishCoalescingNotEnabled, + MultipleDefault { /// Span of the previous default case previous: Span, @@ -359,6 +362,13 @@ impl<'a> From> for DiagnosticBuilder<'a> { "A numeric separator is only allowed between two digits".into() } + NullishCoalescingWithLogicalOp => "Nullish coalescing operator(??) requires parens \ + when mixing with logical operators" + .into(), + NullishCoalescingNotEnabled => { + "Nullish coalescing operator(??) requires jsc.parser.Coalescing".into() + } + TS1056 => "jsc.taraget should be es5 or upper to use getter / setter".into(), TS1141 => "literal in an import type should be string literal".into(), diff --git a/ecmascript/parser/src/lexer.rs b/ecmascript/parser/src/lexer.rs index 577d359923f..818a802e414 100644 --- a/ecmascript/parser/src/lexer.rs +++ b/ecmascript/parser/src/lexer.rs @@ -179,7 +179,7 @@ impl<'a, I: Input> Lexer<'a, I> { return Ok(Some(tok!('.'))); } - '(' | ')' | ';' | ',' | '[' | ']' | '{' | '}' | '@' | '?' => { + '(' | ')' | ';' | ',' | '[' | ']' | '{' | '}' | '@' => { // These tokens are emitted directly. self.input.bump(); return Ok(Some(match c { @@ -197,6 +197,18 @@ impl<'a, I: Input> Lexer<'a, I> { })); } + '?' => match self.input.peek() { + Some('?') => { + self.input.bump(); + self.input.bump(); + return Ok(Some(tok!("??"))); + } + _ => { + self.input.bump(); + return Ok(Some(tok!('?'))); + } + }, + '`' => { self.bump(); return Ok(Some(tok!('`'))); diff --git a/ecmascript/parser/src/lib.rs b/ecmascript/parser/src/lib.rs index 668fbfc6da8..9b5f9f25ffb 100644 --- a/ecmascript/parser/src/lib.rs +++ b/ecmascript/parser/src/lib.rs @@ -247,6 +247,18 @@ impl Syntax { _ => false, } } + + pub fn nullish_coalescing(self) -> bool { + match self { + Syntax::Es(EsConfig { + nullish_coalescing: true, + .. + }) + | Syntax::Typescript(..) => true, + + _ => false, + } + } } #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] @@ -292,6 +304,7 @@ pub struct EsConfig { #[serde(default)] pub jsx: bool, /// Support numeric separator. + /// Stage 3. #[serde(rename = "numericSeparator")] #[serde(default)] pub num_sep: bool, @@ -332,6 +345,10 @@ pub struct EsConfig { #[serde(default)] pub dynamic_import: bool, + + /// Stage 3. + #[serde(default)] + pub nullish_coalescing: bool, } /// Syntactic context. diff --git a/ecmascript/parser/src/macros.rs b/ecmascript/parser/src/macros.rs index 0541f7e0bfb..5432f7d730f 100644 --- a/ecmascript/parser/src/macros.rs +++ b/ecmascript/parser/src/macros.rs @@ -19,6 +19,9 @@ macro_rules! tok { ('-') => { crate::token::Token::BinOp(crate::token::BinOpToken::Sub) }; + ("??") => { + crate::token::Token::BinOp(crate::token::BinOpToken::NullishCoalescing) + }; ('~') => { crate::token::Token::Tilde }; diff --git a/ecmascript/parser/src/parser/expr.rs b/ecmascript/parser/src/parser/expr.rs index f6d9104eb9f..619f53511be 100644 --- a/ecmascript/parser/src/parser/expr.rs +++ b/ecmascript/parser/src/parser/expr.rs @@ -869,7 +869,7 @@ impl<'a, I: Tokens> Parser<'a, I> { macro_rules! wrap { ($e:expr) => {{ if is_optional_chaining { - Expr::TsOptChain(TsOptChain { + Expr::OptChain(OptChainExpr { span: span!(self, start), expr: Box::new($e), }) diff --git a/ecmascript/parser/src/parser/expr/ops.rs b/ecmascript/parser/src/parser/expr/ops.rs index c77b650b132..d79986c3163 100644 --- a/ecmascript/parser/src/parser/expr/ops.rs +++ b/ecmascript/parser/src/parser/expr/ops.rs @@ -107,6 +107,10 @@ impl<'a, I: Tokens> Parser<'a, I> { } }; + if !self.syntax().nullish_coalescing() && op == op!("??") { + syntax_error!(left.span(), SyntaxError::NullishCoalescingNotEnabled) + } + if op.precedence() <= min_prec { trace!( "returning {:?} without parsing {:?} because min_prec={}, prec={}", @@ -154,6 +158,33 @@ impl<'a, I: Tokens> Parser<'a, I> { }, )? }; + /* this check is for all ?? operators + * a ?? b && c for this example + * b && c => This is considered as a logical expression in the ast tree + * a => Identifier + * so for ?? operator we need to check in this case the right expression to + * have parenthesis second case a && b ?? c + * here a && b => This is considered as a logical expression in the ast tree + * c => identifier + * so now here for ?? operator we need to check the left expression to have + * parenthesis if the parenthesis is missing we raise an error and + * throw it + */ + if op == op!("??") { + match *left { + Expr::Bin(BinExpr { span, op, .. }) if op == op!("&&") || op == op!("||") => { + syntax_error!(span, SyntaxError::NullishCoalescingWithLogicalOp); + } + _ => {} + } + + match *right { + Expr::Bin(BinExpr { span, op, .. }) if op == op!("&&") || op == op!("||") => { + syntax_error!(span, SyntaxError::NullishCoalescingWithLogicalOp); + } + _ => {} + } + } let node = Box::new(Expr::Bin(BinExpr { span: Span::new(left.span().lo(), right.span().hi(), Default::default()), @@ -163,6 +194,16 @@ impl<'a, I: Tokens> Parser<'a, I> { })); let expr = self.parse_bin_op_recursively(node, min_prec)?; + + if op == op!("??") { + match *expr { + Expr::Bin(BinExpr { span, op, .. }) if op == op!("&&") || op == op!("||") => { + syntax_error!(span, SyntaxError::NullishCoalescingWithLogicalOp); + } + + _ => {} + } + } Ok(expr) } diff --git a/ecmascript/parser/src/parser/util.rs b/ecmascript/parser/src/parser/util.rs index 75bd9b38ad7..782def289f1 100644 --- a/ecmascript/parser/src/parser/util.rs +++ b/ecmascript/parser/src/parser/util.rs @@ -243,7 +243,7 @@ pub(super) trait ExprExt { | Expr::JSXFragment(..) => false, // typescript - Expr::TsOptChain(TsOptChain { ref expr, .. }) + Expr::OptChain(OptChainExpr { ref expr, .. }) | Expr::TsNonNull(TsNonNullExpr { ref expr, .. }) | Expr::TsTypeAssertion(TsTypeAssertion { ref expr, .. }) | Expr::TsTypeCast(TsTypeCastExpr { ref expr, .. }) diff --git a/ecmascript/parser/src/token.rs b/ecmascript/parser/src/token.rs index fc8f3324b1d..3b69fa684ce 100644 --- a/ecmascript/parser/src/token.rs +++ b/ecmascript/parser/src/token.rs @@ -203,6 +203,9 @@ pub enum BinOpToken { LogicalOr, /// `&&` LogicalAnd, + + /// `??` + NullishCoalescing, } impl BinOpToken { @@ -539,6 +542,7 @@ impl From for BinaryOp { BinOpToken::LogicalOr => LogicalOr, BinOpToken::LogicalAnd => LogicalAnd, BinOpToken::Exp => Exp, + BinOpToken::NullishCoalescing => NullishCoalescing, } } } diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts new file mode 100644 index 00000000000..650d93b39a3 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts @@ -0,0 +1 @@ +c && d ?? e; diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts.stderr b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts.stderr new file mode 100644 index 00000000000..82e677dd216 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts.stderr @@ -0,0 +1,6 @@ +error: Nullish coalescing operator(??) requires parens when mixing with logical operators + --> $DIR/tests/typescript-errors/nullish-coalescing-operator/no-paren-and-nullish/input.ts:1:1 + | +1 | c && d ?? e; + | ^^^^^^ + diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts new file mode 100644 index 00000000000..b8805af4953 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts @@ -0,0 +1 @@ +a ?? b && c; diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts.stderr b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts.stderr new file mode 100644 index 00000000000..08a06d2494e --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts.stderr @@ -0,0 +1,6 @@ +error: Nullish coalescing operator(??) requires parens when mixing with logical operators + --> $DIR/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-and/input.ts:1:6 + | +1 | a ?? b && c; + | ^^^^^^ + diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts new file mode 100644 index 00000000000..1fd223be11f --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts @@ -0,0 +1 @@ +e ?? f ?? g || h; diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts.stderr b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts.stderr new file mode 100644 index 00000000000..710d576e540 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts.stderr @@ -0,0 +1,6 @@ +error: Nullish coalescing operator(??) requires parens when mixing with logical operators + --> $DIR/tests/typescript-errors/nullish-coalescing-operator/no-paren-nullish-or/input.ts:1:1 + | +1 | e ?? f ?? g || h; + | ^^^^^^^^^^^^^^^^ + diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts new file mode 100644 index 00000000000..5647ff20498 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts @@ -0,0 +1 @@ +h || i ?? j; diff --git a/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts.stderr b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts.stderr new file mode 100644 index 00000000000..195cfd2b329 --- /dev/null +++ b/ecmascript/parser/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts.stderr @@ -0,0 +1,6 @@ +error: Nullish coalescing operator(??) requires parens when mixing with logical operators + --> $DIR/tests/typescript-errors/nullish-coalescing-operator/no-paren-or-nullish/input.ts:1:1 + | +1 | h || i ?? j; + | ^^^^^^ + diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts new file mode 100644 index 00000000000..4080eadc396 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts @@ -0,0 +1 @@ +(a && b) ?? c; diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts.json new file mode 100644 index 00000000000..1832222a42c --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/and-nullish/input.ts.json @@ -0,0 +1,78 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 13, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "ParenthesisExpression", + "span": { + "start": 0, + "end": 8, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 1, + "end": 7, + "ctxt": 0 + }, + "operator": "&&", + "left": { + "type": "Identifier", + "span": { + "start": 1, + "end": 2, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 6, + "end": 7, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + } + } + }, + "right": { + "type": "Identifier", + "span": { + "start": 12, + "end": 13, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts new file mode 100644 index 00000000000..1b247875fce --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts @@ -0,0 +1 @@ +a ?? b ?? c; diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts.json new file mode 100644 index 00000000000..46b593de8fc --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/associativity/input.ts.json @@ -0,0 +1,70 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 12, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 12, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 11, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 6, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "Identifier", + "span": { + "start": 0, + "end": 1, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 5, + "end": 6, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + } + }, + "right": { + "type": "Identifier", + "span": { + "start": 10, + "end": 11, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts new file mode 100644 index 00000000000..ea99cb192c7 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts @@ -0,0 +1 @@ +foo ?? 1; diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts.json new file mode 100644 index 00000000000..fbecba36fd6 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/expression/input.ts.json @@ -0,0 +1,48 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 9, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 9, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 8, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "Identifier", + "span": { + "start": 0, + "end": 3, + "ctxt": 0 + }, + "value": "foo", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "NumericLiteral", + "span": { + "start": 7, + "end": 8, + "ctxt": 0 + }, + "value": 1.0 + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts new file mode 100644 index 00000000000..fc603903fff --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts @@ -0,0 +1,3 @@ +a + ?? b + ?? c; diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts.json new file mode 100644 index 00000000000..74e39860ecc --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/multiline/input.ts.json @@ -0,0 +1,70 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 17, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 17, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 16, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 9, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "Identifier", + "span": { + "start": 0, + "end": 1, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 8, + "end": 9, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + } + }, + "right": { + "type": "Identifier", + "span": { + "start": 15, + "end": 16, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts new file mode 100644 index 00000000000..886259bfb68 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts @@ -0,0 +1 @@ +a ?? (b && c); diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts.json new file mode 100644 index 00000000000..d2873e3a96a --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-and/input.ts.json @@ -0,0 +1,78 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 13, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "Identifier", + "span": { + "start": 0, + "end": 1, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "ParenthesisExpression", + "span": { + "start": 5, + "end": 13, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 6, + "end": 12, + "ctxt": 0 + }, + "operator": "&&", + "left": { + "type": "Identifier", + "span": { + "start": 6, + "end": 7, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 11, + "end": 12, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts new file mode 100644 index 00000000000..12162ba162d --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts @@ -0,0 +1 @@ +a ?? (b || c); diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts.json new file mode 100644 index 00000000000..58bd7c2e92e --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/nullish-or/input.ts.json @@ -0,0 +1,78 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 13, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "Identifier", + "span": { + "start": 0, + "end": 1, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "ParenthesisExpression", + "span": { + "start": 5, + "end": 13, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 6, + "end": 12, + "ctxt": 0 + }, + "operator": "||", + "left": { + "type": "Identifier", + "span": { + "start": 6, + "end": 7, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 11, + "end": 12, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts new file mode 100644 index 00000000000..a8204525df1 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts @@ -0,0 +1 @@ +(a || b) ?? c; diff --git a/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts.json b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts.json new file mode 100644 index 00000000000..da86e6fcd73 --- /dev/null +++ b/ecmascript/parser/tests/typescript/nullish-coalescing-operator/or-nullish/input.ts.json @@ -0,0 +1,78 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "body": [ + { + "type": "ExpressionStatement", + "span": { + "start": 0, + "end": 14, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 0, + "end": 13, + "ctxt": 0 + }, + "operator": "??", + "left": { + "type": "ParenthesisExpression", + "span": { + "start": 0, + "end": 8, + "ctxt": 0 + }, + "expression": { + "type": "BinaryExpression", + "span": { + "start": 1, + "end": 7, + "ctxt": 0 + }, + "operator": "||", + "left": { + "type": "Identifier", + "span": { + "start": 1, + "end": 2, + "ctxt": 0 + }, + "value": "a", + "typeAnnotation": null, + "optional": false + }, + "right": { + "type": "Identifier", + "span": { + "start": 6, + "end": 7, + "ctxt": 0 + }, + "value": "b", + "typeAnnotation": null, + "optional": false + } + } + }, + "right": { + "type": "Identifier", + "span": { + "start": 12, + "end": 13, + "ctxt": 0 + }, + "value": "c", + "typeAnnotation": null, + "optional": false + } + } + } + ], + "interpreter": null +} diff --git a/ecmascript/parser/tests/typescript/optional-chaining/type-arguments/input.ts.json b/ecmascript/parser/tests/typescript/optional-chaining/type-arguments/input.ts.json index cd2ffefee33..0992ad8945a 100644 --- a/ecmascript/parser/tests/typescript/optional-chaining/type-arguments/input.ts.json +++ b/ecmascript/parser/tests/typescript/optional-chaining/type-arguments/input.ts.json @@ -21,7 +21,7 @@ "ctxt": 0 }, "callee": { - "type": "TsOptionalChainingExpression", + "type": "OptionalChainingExpression", "span": { "start": 0, "end": 20, diff --git a/ecmascript/transforms/Cargo.toml b/ecmascript/transforms/Cargo.toml index 398a31520c1..46a22144130 100644 --- a/ecmascript/transforms/Cargo.toml +++ b/ecmascript/transforms/Cargo.toml @@ -11,8 +11,8 @@ edition = "2018" [dependencies] swc_atoms = { version = "0.2.0", path ="../../atoms" } swc_common = { version = "0.4.2", path ="../../common" } -ast = { package = "swc_ecma_ast", version = "0.12.0", path ="../ast" } -swc_ecma_parser = { version = "0.14", path ="../parser", features = ["verify"] } +ast = { package = "swc_ecma_ast", version = "0.13.0", path ="../ast" } +swc_ecma_parser = { version = "0.15", path ="../parser", features = ["verify"] } chashmap = "2.2.0" either = "1.5" fxhash = "0.2" @@ -31,7 +31,7 @@ smallvec = "1" [dev-dependencies] testing = { version = "0.4", path ="../../testing" } -swc_ecma_codegen = { version = "0.10.0", path ="../codegen" } +swc_ecma_codegen = { version = "0.11.0", path ="../codegen" } tempfile = "3" pretty_assertions = "0.6" sourcemap = "4.1.1" \ No newline at end of file diff --git a/ecmascript/transforms/src/compat/es2015/destructuring.rs b/ecmascript/transforms/src/compat/es2015/destructuring.rs index 1ff01b7b926..b6c0c384902 100644 --- a/ecmascript/transforms/src/compat/es2015/destructuring.rs +++ b/ecmascript/transforms/src/compat/es2015/destructuring.rs @@ -1053,7 +1053,7 @@ fn can_be_null(e: &Expr) -> bool { | Expr::TsTypeAssertion(TsTypeAssertion { ref expr, .. }) | Expr::TsTypeCast(TsTypeCastExpr { ref expr, .. }) | Expr::TsConstAssertion(TsConstAssertion { ref expr, .. }) => can_be_null(expr), - Expr::TsOptChain(ref e) => can_be_null(&e.expr), + Expr::OptChain(ref e) => can_be_null(&e.expr), Expr::Invalid(..) => unreachable!(), } diff --git a/ecmascript/transforms/src/fixer.rs b/ecmascript/transforms/src/fixer.rs index 792d7468aa7..6536f19f739 100644 --- a/ecmascript/transforms/src/fixer.rs +++ b/ecmascript/transforms/src/fixer.rs @@ -1,18 +1,29 @@ -use crate::{pass::Pass, util::ExprFactory}; +use crate::{ + pass::Pass, + util::{ExprFactory, COMMENTS}, +}; use ast::*; +use fxhash::FxHashMap; use swc_common::{ util::{map::Map, move_map::MoveMap}, - Fold, FoldWith, + Fold, FoldWith, Span, Spanned, }; pub fn fixer() -> impl Pass { Fixer { ctx: Default::default(), + span_map: Default::default(), } } +#[derive(Debug)] struct Fixer { ctx: Context, + /// A hash map to preserve original span. + /// + /// Key is span of inner expression, and value is span of the paren + /// expression. + span_map: FxHashMap, } #[repr(u8)] @@ -33,12 +44,32 @@ enum Context { is_var_decl: bool, }, } + impl Default for Context { fn default() -> Self { Context::Default } } +impl Fold for Fixer { + fn fold(&mut self, p: Program) -> Program { + debug_assert!(self.span_map.is_empty()); + self.span_map.clear(); + + let p = p.fold_children(self); + + COMMENTS.with(|c| { + for (to, from) in self.span_map.drain() { + let (from, to) = (from.data(), to.data()); + c.move_leading(from.lo, to.lo); + c.move_trailing(from.hi, to.hi); + } + }); + + p + } +} + impl Fold for Fixer { fn fold(&mut self, node: KeyValuePatProp) -> KeyValuePatProp { let old = self.ctx; @@ -84,7 +115,7 @@ impl Fold for Fixer { match body { BlockStmtOrExpr::Expr(box expr @ Expr::Object(..)) => { - BlockStmtOrExpr::Expr(box expr.wrap_with_paren()) + BlockStmtOrExpr::Expr(box self.wrap(expr)) } _ => body, @@ -108,7 +139,7 @@ impl Fold for Fixer { let stmt = match stmt { Stmt::Expr(ExprStmt { span, expr }) => Stmt::Expr(ExprStmt { span, - expr: expr.map(handle_expr_stmt), + expr: expr.map(|e| self.handle_expr_stmt(e)), }), _ => stmt, @@ -175,7 +206,7 @@ impl Fold for Fixer { match *prop.value { Expr::Seq(..) => KeyValueProp { - value: box (*prop.value).wrap_with_paren(), + value: box self.wrap(*prop.value), ..prop }, _ => prop, @@ -183,14 +214,86 @@ impl Fold for Fixer { } } -/// Removes paren -fn unwrap_expr(mut e: Expr) -> Expr { - match e { - Expr::Seq(SeqExpr { ref mut exprs, .. }) if exprs.len() == 1 => { - unwrap_expr(*exprs.pop().unwrap()) +impl Fixer { + fn wrap(&mut self, e: T) -> Expr + where + T: Into, + { + let expr = box e.into(); + let span = expr.span(); + + let span = if let Some(span) = self.span_map.remove(&span) { + span + } else { + span + }; + + Expr::Paren(ParenExpr { expr, span }) + } + + /// Removes paren + fn unwrap_expr(&mut self, mut e: Expr) -> Expr { + match e { + Expr::Seq(SeqExpr { ref mut exprs, .. }) if exprs.len() == 1 => { + self.unwrap_expr(*exprs.pop().unwrap()) + } + Expr::Paren(ParenExpr { + span: paren_span, + expr, + .. + }) => { + let e = self.unwrap_expr(*expr); + + self.span_map.insert(e.span(), paren_span); + e + } + _ => validate!(e), + } + } + + fn handle_expr_stmt(&mut self, expr: Expr) -> Expr { + match expr { + // It's important for arrow pass to work properly. + Expr::Object(..) | Expr::Class(..) | Expr::Fn(..) => self.wrap(expr), + + // ({ a } = foo) + Expr::Assign(AssignExpr { + span, + left: PatOrExpr::Pat(left @ box Pat::Object(..)), + op, + right, + }) => self.wrap(AssignExpr { + span, + left: PatOrExpr::Pat(left), + op, + right, + }), + + Expr::Seq(SeqExpr { span, exprs }) => { + debug_assert!( + exprs.len() != 1, + "SeqExpr should be unwrapped if exprs.len() == 1, but length is 1" + ); + + let mut i = 0; + let len = exprs.len(); + Expr::Seq(SeqExpr { + span, + exprs: exprs.move_map(|expr| { + i += 1; + let is_last = len == i; + + if !is_last { + expr.map(|e| self.handle_expr_stmt(e)) + } else { + expr + } + }), + }) + } + + _ => expr, } - Expr::Paren(ParenExpr { expr, .. }) => unwrap_expr(*expr), - _ => validate!(e), } } @@ -199,7 +302,7 @@ impl Fold for Fixer { let expr = validate!(expr); let expr = expr.fold_children(self); let expr = validate!(expr); - let expr = unwrap_expr(expr); + let expr = self.unwrap_expr(expr); match expr { Expr::Member(MemberExpr { @@ -301,7 +404,7 @@ impl Fold for Fixer { }) => validate!(MemberExpr { span, computed, - obj: obj.wrap_with_paren().as_obj(), + obj: self.wrap(*obj).as_obj(), prop, }) .into(), @@ -381,10 +484,10 @@ impl Fold for Fixer { | e @ Expr::Seq(..) | e @ Expr::Yield(..) | e @ Expr::Cond(..) - | e @ Expr::Arrow(..) => box validate!(e).wrap_with_paren(), + | e @ Expr::Arrow(..) => box self.wrap(e), Expr::Bin(BinExpr { op: op_of_rhs, .. }) => { if op_of_rhs.precedence() <= expr.op.precedence() { - box expr.right.wrap_with_paren() + box self.wrap(*expr.right) } else { validate!(expr.right) } @@ -398,7 +501,7 @@ impl Fold for Fixer { Expr::Bin(BinExpr { op: op_of_lhs, .. }) => { if op_of_lhs.precedence() < expr.op.precedence() { Expr::Bin(validate!(BinExpr { - left: box expr.left.wrap_with_paren(), + left: box self.wrap(*expr.left), ..expr })) } else { @@ -411,7 +514,7 @@ impl Fold for Fixer { | e @ Expr::Cond(..) | e @ Expr::Assign(..) | e @ Expr::Arrow(..) => validate!(Expr::Bin(BinExpr { - left: box e.wrap_with_paren(), + left: box self.wrap(e), ..expr })), _ => validate!(Expr::Bin(expr)), @@ -423,11 +526,11 @@ impl Fold for Fixer { e @ Expr::Seq(..) | e @ Expr::Assign(..) | e @ Expr::Cond(..) - | e @ Expr::Arrow(..) => box e.wrap_with_paren(), + | e @ Expr::Arrow(..) => box self.wrap(e), e @ Expr::Object(..) | e @ Expr::Fn(..) | e @ Expr::Class(..) => { if self.ctx == Context::Default { - box e.wrap_with_paren() + box self.wrap(e) } else { box e } @@ -436,12 +539,12 @@ impl Fold for Fixer { }; let cons = match *expr.cons { - e @ Expr::Seq(..) => box e.wrap_with_paren(), + e @ Expr::Seq(..) => box self.wrap(e), _ => expr.cons, }; let alt = match *expr.alt { - e @ Expr::Seq(..) => box e.wrap_with_paren(), + e @ Expr::Seq(..) => box self.wrap(e), _ => expr.alt, }; validate!(Expr::Cond(CondExpr { @@ -459,7 +562,7 @@ impl Fold for Fixer { | e @ Expr::Seq(..) | e @ Expr::Cond(..) | e @ Expr::Arrow(..) - | e @ Expr::Yield(..) => box e.wrap_with_paren(), + | e @ Expr::Yield(..) => box self.wrap(e), _ => expr.arg, }; @@ -479,7 +582,7 @@ impl Fold for Fixer { }) => expr.right, // Handle `foo = bar = init() - Expr::Seq(right) => box right.wrap_with_paren(), + Expr::Seq(right) => box self.wrap(right), _ => expr.right, }; @@ -493,7 +596,7 @@ impl Fold for Fixer { type_args, }) => validate!(Expr::Call(CallExpr { span, - callee: callee.wrap_with_paren().as_callee(), + callee: self.wrap(*callee).as_callee(), args, type_args, })), @@ -512,17 +615,16 @@ impl Fold for Fixer { type_args, })), - Context::Callee { is_new: true } => validate!(Expr::Call(CallExpr { + Context::Callee { is_new: true } => self.wrap(CallExpr { span, callee: callee.as_callee(), args, type_args, - })) - .wrap_with_paren(), + }), _ => validate!(Expr::Call(CallExpr { span, - callee: callee.wrap_with_paren().as_callee(), + callee: self.wrap(*callee).as_callee(), args, type_args, })), @@ -534,7 +636,7 @@ impl Fold for Fixer { type_args, }) => validate!(Expr::Call(CallExpr { span, - callee: callee.wrap_with_paren().as_callee(), + callee: self.wrap(*callee).as_callee(), args, type_args, })), @@ -552,7 +654,7 @@ impl Fold for Fixer { Expr::Yield(..) => { return ExprOrSpread { spread: None, - expr: box e.expr.wrap_with_paren(), + expr: box self.wrap(*e.expr), } } _ => {} @@ -569,7 +671,7 @@ impl Fold for Fixer { self.ctx = Context::Default; let mut node = node.fold_children(self); node.expr = match *node.expr { - Expr::Arrow(..) | Expr::Seq(..) => box node.expr.wrap_with_paren(), + Expr::Arrow(..) | Expr::Seq(..) => box self.wrap(*node.expr), _ => node.expr, }; self.ctx = old; @@ -584,7 +686,7 @@ impl Fold for Fixer { let mut node = node.fold_children(self); node.body = match node.body { BlockStmtOrExpr::Expr(e @ box Expr::Seq(..)) => { - BlockStmtOrExpr::Expr(box e.wrap_with_paren()) + BlockStmtOrExpr::Expr(box self.wrap(*e)) } _ => node.body, }; @@ -599,7 +701,7 @@ impl Fold for Fixer { self.ctx = Context::Default; let mut node = node.fold_children(self); node.super_class = match node.super_class { - Some(e @ box Expr::Seq(..)) => Some(box e.wrap_with_paren()), + Some(e @ box Expr::Seq(..)) => Some(box self.wrap(*e)), _ => node.super_class, }; self.ctx = old; @@ -619,52 +721,6 @@ fn ignore_return_value(expr: Box) -> Option> { } } -fn handle_expr_stmt(expr: Expr) -> Expr { - match expr { - // It's important for arrow pass to work properly. - Expr::Object(..) | Expr::Class(..) | Expr::Fn(..) => expr.wrap_with_paren(), - - // ({ a } = foo) - Expr::Assign(AssignExpr { - span, - left: PatOrExpr::Pat(left @ box Pat::Object(..)), - op, - right, - }) => AssignExpr { - span, - left: PatOrExpr::Pat(left), - op, - right, - } - .wrap_with_paren(), - - Expr::Seq(SeqExpr { span, exprs }) => { - debug_assert!( - exprs.len() != 1, - "SeqExpr should be unwrapped if exprs.len() == 1, but length is 1" - ); - - let mut i = 0; - let len = exprs.len(); - Expr::Seq(SeqExpr { - span, - exprs: exprs.move_map(|expr| { - i += 1; - let is_last = len == i; - - if !is_last { - expr.map(handle_expr_stmt) - } else { - expr - } - }), - }) - } - - _ => expr, - } -} - #[cfg(test)] mod tests { struct Noop; diff --git a/ecmascript/transforms/src/optimization/simplify/expr.rs b/ecmascript/transforms/src/optimization/simplify/expr.rs index 59110dd0804..fd00b742a54 100644 --- a/ecmascript/transforms/src/optimization/simplify/expr.rs +++ b/ecmascript/transforms/src/optimization/simplify/expr.rs @@ -993,7 +993,7 @@ where | Expr::TsTypeCast(TsTypeCastExpr { expr, .. }) | Expr::TsAs(TsAsExpr { expr, .. }) | Expr::TsConstAssertion(TsConstAssertion { expr, .. }) => add_effects(v, expr), - Expr::TsOptChain(e) => add_effects(v, e.expr), + Expr::OptChain(e) => add_effects(v, e.expr), Expr::Invalid(..) => unreachable!(), } diff --git a/ecmascript/transforms/src/proposals.rs b/ecmascript/transforms/src/proposals.rs index c0ccdc296c6..c584e23e28d 100644 --- a/ecmascript/transforms/src/proposals.rs +++ b/ecmascript/transforms/src/proposals.rs @@ -1,5 +1,9 @@ -pub use self::{class_properties::class_properties, decorators::decorators, export::export}; +pub use self::{ + class_properties::class_properties, decorators::decorators, export::export, + opt_chaining::optional_chaining, +}; mod class_properties; pub mod decorators; mod export; +mod opt_chaining; diff --git a/ecmascript/transforms/src/typescript/opt_chaining.rs b/ecmascript/transforms/src/proposals/opt_chaining.rs similarity index 94% rename from ecmascript/transforms/src/typescript/opt_chaining.rs rename to ecmascript/transforms/src/proposals/opt_chaining.rs index 0ad68771820..c7bd1492d0d 100644 --- a/ecmascript/transforms/src/typescript/opt_chaining.rs +++ b/ecmascript/transforms/src/proposals/opt_chaining.rs @@ -48,7 +48,7 @@ where impl Fold for OptChaining { fn fold(&mut self, e: Expr) -> Expr { let e = match e { - Expr::TsOptChain(e) => Expr::Cond(validate!(self.unwrap(e))), + Expr::OptChain(e) => Expr::Cond(validate!(self.unwrap(e))), Expr::Unary(e) => validate!(self.handle_unary(e)), Expr::Member(e) => validate!(self.handle_member(e)), Expr::Call(e) => validate!(self.handle_call(e)), @@ -66,7 +66,7 @@ impl OptChaining { if let op!("delete") = e.op { match *e.arg { - Expr::TsOptChain(o) => { + Expr::OptChain(o) => { let expr = self.unwrap(o); return CondExpr { @@ -83,7 +83,7 @@ impl OptChaining { Expr::Member(MemberExpr { span, - obj: ExprOrSuper::Expr(box Expr::TsOptChain(o)), + obj: ExprOrSuper::Expr(box Expr::OptChain(o)), prop, computed, }) => { @@ -114,7 +114,7 @@ impl OptChaining { /// Only called from [Fold]. fn handle_call(&mut self, e: CallExpr) -> Expr { - if let ExprOrSuper::Expr(box Expr::TsOptChain(o)) = e.callee { + if let ExprOrSuper::Expr(box Expr::OptChain(o)) = e.callee { let expr = self.unwrap(o); return CondExpr { @@ -133,7 +133,7 @@ impl OptChaining { /// Only called from `[Fold]. fn handle_member(&mut self, e: MemberExpr) -> Expr { - if let ExprOrSuper::Expr(box Expr::TsOptChain(o)) = e.obj { + if let ExprOrSuper::Expr(box Expr::OptChain(o)) = e.obj { let expr = self.unwrap(o); return CondExpr { @@ -150,13 +150,13 @@ impl OptChaining { Expr::Member(e) } - fn unwrap(&mut self, e: TsOptChain) -> CondExpr { + fn unwrap(&mut self, e: OptChainExpr) -> CondExpr { let span = e.span; let cons = undefined(span); match *e.expr { Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(box Expr::TsOptChain(o)), + obj: ExprOrSuper::Expr(box Expr::OptChain(o)), prop, computed, span: m_span, @@ -170,7 +170,7 @@ impl OptChaining { prop, computed, }); - let alt = box Expr::TsOptChain(TsOptChain { + let alt = box Expr::OptChain(OptChainExpr { span: o_span, expr: alt, }); @@ -180,7 +180,7 @@ impl OptChaining { Expr::Call(CallExpr { span, - callee: ExprOrSuper::Expr(box Expr::TsOptChain(o)), + callee: ExprOrSuper::Expr(box Expr::OptChain(o)), args, type_args, }) => { @@ -192,7 +192,7 @@ impl OptChaining { args, type_args, }); - let alt = box Expr::TsOptChain(TsOptChain { span, expr: alt }); + let alt = box Expr::OptChain(OptChainExpr { span, expr: alt }); return validate!(CondExpr { span: DUMMY_SP, diff --git a/ecmascript/transforms/src/typescript/opt_chaining/tests.rs b/ecmascript/transforms/src/proposals/opt_chaining/tests.rs similarity index 100% rename from ecmascript/transforms/src/typescript/opt_chaining/tests.rs rename to ecmascript/transforms/src/proposals/opt_chaining/tests.rs diff --git a/ecmascript/transforms/src/typescript.rs b/ecmascript/transforms/src/typescript.rs index 9e637fda07d..37f22a469e0 100644 --- a/ecmascript/transforms/src/typescript.rs +++ b/ecmascript/transforms/src/typescript.rs @@ -1,4 +1,3 @@ -pub use self::opt_chaining::optional_chaining; use crate::{ pass::Pass, util::{prepend_stmts, var::VarCollector, ExprFactory}, @@ -10,7 +9,6 @@ use swc_common::{ util::move_map::MoveMap, Fold, FoldWith, Spanned, SyntaxContext, Visit, VisitWith, DUMMY_SP, }; -mod opt_chaining; #[cfg(test)] mod tests; diff --git a/ecmascript/transforms/src/util.rs b/ecmascript/transforms/src/util.rs index e6bdf1779c5..7f5740fc517 100644 --- a/ecmascript/transforms/src/util.rs +++ b/ecmascript/transforms/src/util.rs @@ -19,7 +19,8 @@ use std::{ }; use swc_atoms::{js_word, JsWord}; use swc_common::{ - errors::Handler, Fold, FoldWith, Mark, Span, Spanned, Visit, VisitWith, DUMMY_SP, + comments::Comments, errors::Handler, Fold, FoldWith, Mark, Span, Spanned, Visit, VisitWith, + DUMMY_SP, }; use unicode_xid::UnicodeXID; @@ -693,7 +694,7 @@ pub trait ExprExt { | Expr::TsNonNull(TsNonNullExpr { ref expr, .. }) | Expr::TsTypeAssertion(TsTypeAssertion { ref expr, .. }) | Expr::TsTypeCast(TsTypeCastExpr { ref expr, .. }) => expr.may_have_side_effects(), - Expr::TsOptChain(ref e) => e.expr.may_have_side_effects(), + Expr::OptChain(ref e) => e.expr.may_have_side_effects(), Expr::Invalid(..) => unreachable!(), } @@ -923,7 +924,7 @@ not_lit!(TsTypeAssertion); not_lit!(TsConstAssertion); not_lit!(PrivateName); -not_lit!(TsOptChain); +not_lit!(OptChainExpr); not_lit!(SpreadElement); not_lit!(Invalid); @@ -1283,3 +1284,4 @@ impl<'a> UsageFinder<'a> { } scoped_thread_local!(pub static HANDLER: Handler); +scoped_thread_local!(pub static COMMENTS: Comments); diff --git a/src/config.rs b/src/config.rs index 5b87bbf9197..ea10b34860e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use ecmascript::{ chain_at, const_modules, modules, optimization::{simplifier, InlineGlobals, JsonParse}, pass::{noop, Optional, Pass}, - proposals::{class_properties, decorators, export}, + proposals::{class_properties, decorators, export, optional_chaining}, react, resolver, typescript, }, }; @@ -181,7 +181,7 @@ impl Options { // handle jsx Optional::new(react::react(cm.clone(), transform.react), syntax.jsx()), Optional::new(typescript::strip(), syntax.typescript()), - Optional::new(typescript::optional_chaining(), syntax.typescript()), + Optional::new(optional_chaining(), syntax.typescript()), Optional::new(class_properties(), syntax.typescript()), resolver(), const_modules, diff --git a/src/lib.rs b/src/lib.rs index eaf50e559dd..5b339d1310d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ use ecmascript::{ transforms::{ helpers::{self, Helpers}, util, + util::COMMENTS, }, }; pub use ecmascript::{ @@ -43,6 +44,7 @@ pub struct Compiler { /// CodeMap pub cm: Arc, pub handler: Handler, + comments: Comments, } #[derive(Serialize)] @@ -54,6 +56,10 @@ pub struct TransformOutput { /// These are **low-level** apis. impl Compiler { + pub fn comments(&self) -> &Comments { + &self.comments + } + /// Runs `op` in current compiler's context. /// /// Note: Other methods of `Compiler` already uses this internally. @@ -61,7 +67,13 @@ impl Compiler { where F: FnOnce() -> R, { - GLOBALS.set(&self.globals, || op()) + GLOBALS.set(&self.globals, || { + // + COMMENTS.set(&self.comments, || { + // + op() + }) + }) } /// This method parses a javascript / typescript file @@ -71,7 +83,7 @@ impl Compiler { target: JscTarget, syntax: Syntax, is_module: bool, - comments: Option<&Comments>, + parse_comments: bool, ) -> Result { self.run(|| { let session = ParseSess { @@ -82,7 +94,11 @@ impl Compiler { syntax, target, SourceFileInput::from(&*fm), - comments, + if parse_comments { + Some(&self.comments) + } else { + None + }, ); let mut parser = Parser::new_from(session, lexer); let program = if is_module { @@ -181,6 +197,7 @@ impl Compiler { cm, handler, globals: Globals::new(), + comments: Default::default(), } } @@ -290,13 +307,12 @@ impl Compiler { eprintln!("processing js file: {:?}", fm) } - let comments = Default::default(); let module = self.parse_js( fm.clone(), config.target, config.syntax, config.is_module, - if config.minify { None } else { Some(&comments) }, + !config.minify, )?; let mut pass = config.pass; let module = helpers::HELPERS.set(&Helpers::new(config.external_helpers), || { @@ -306,7 +322,13 @@ impl Compiler { }) }); - self.print(&module, fm, &comments, config.source_maps, config.minify) + self.print( + &module, + fm, + &self.comments, + config.source_maps, + config.minify, + ) }) } } diff --git a/tests/projects.rs b/tests/projects.rs index a746bf5449f..800624036c5 100644 --- a/tests/projects.rs +++ b/tests/projects.rs @@ -228,3 +228,25 @@ fn issue_467() { fn issue_468() { file("tests/projects/issue-468/input.ts").expect("failed to parse typescript"); } + +#[test] +fn issue_528() { + let f = file("tests/projects/issue-528/input.js") + .unwrap() + .replace(" ", ""); + let f = f.trim(); + + println!("{}", f); + assert_eq!( + f, + "\ +//bar +[ +//foo +a, +//baz +//bar +b +];" + ); +} diff --git a/tests/projects/issue-528/input.js b/tests/projects/issue-528/input.js new file mode 100644 index 00000000000..1dec4a36105 --- /dev/null +++ b/tests/projects/issue-528/input.js @@ -0,0 +1,11 @@ +// bar +[ + // foo + a, + + //bar + ( + //baz + b + ) +] \ No newline at end of file