diff --git a/crates/swc_ecma_compat_es2020/src/lib.rs b/crates/swc_ecma_compat_es2020/src/lib.rs index 743ead4312b2..7e7cf05de528 100644 --- a/crates/swc_ecma_compat_es2020/src/lib.rs +++ b/crates/swc_ecma_compat_es2020/src/lib.rs @@ -18,10 +18,9 @@ pub fn es2020(config: Config, unresolved_mark: Mark) -> impl Pass { assumptions.no_document_all = config.nullish_coalescing.no_document_all; ( - nullish_coalescing(config.nullish_coalescing), optional_chaining(config.optional_chaining, unresolved_mark), Compiler::new(swc_ecma_compiler::Config { - includes: Features::EXPORT_NAMESPACE_FROM, + includes: Features::EXPORT_NAMESPACE_FROM | Features::NULLISH_COALESCING, assumptions, ..Default::default() }), diff --git a/crates/swc_ecma_compat_es2020/src/nullish_coalescing.rs b/crates/swc_ecma_compat_es2020/src/nullish_coalescing.rs index d78cc6de7327..87f456cba4da 100644 --- a/crates/swc_ecma_compat_es2020/src/nullish_coalescing.rs +++ b/crates/swc_ecma_compat_es2020/src/nullish_coalescing.rs @@ -1,24 +1,9 @@ -use std::mem::take; - use serde::Deserialize; -use swc_common::{util::take::Take, Span, DUMMY_SP}; -use swc_ecma_ast::*; -use swc_ecma_utils::{alias_ident_for_simple_assign_tatget, alias_if_required, StmtLike}; -use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith}; - -pub fn nullish_coalescing(c: Config) -> impl Pass + 'static { - visit_mut_pass(NullishCoalescing { - c, - ..Default::default() - }) -} - -#[derive(Debug, Default)] -struct NullishCoalescing { - vars: Vec, - c: Config, -} +use swc_ecma_ast::Pass; +use swc_ecma_compiler::{Compiler, Features}; +use swc_ecma_transforms_base::assumptions::Assumptions; +/// Configuration for nullish coalescing transformation #[derive(Debug, Clone, Copy, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Config { @@ -26,239 +11,16 @@ pub struct Config { pub no_document_all: bool, } -impl NullishCoalescing { - fn visit_mut_stmt_like(&mut self, stmts: &mut Vec) - where - T: VisitMutWith + StmtLike, - { - let mut buf = Vec::with_capacity(stmts.len() + 2); - - for mut stmt in stmts.take() { - stmt.visit_mut_with(self); - - if !self.vars.is_empty() { - buf.push(T::from( - VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - decls: take(&mut self.vars), - declare: false, - ..Default::default() - } - .into(), - )); - } - - buf.push(stmt); - } - - *stmts = buf - } -} - -impl VisitMut for NullishCoalescing { - noop_visit_mut_type!(fail); - - /// Prevents #1123 - fn visit_mut_block_stmt(&mut self, s: &mut BlockStmt) { - let old_vars = self.vars.take(); - s.visit_mut_children_with(self); - self.vars = old_vars; - } - - /// Prevents #1123 - fn visit_mut_switch_case(&mut self, s: &mut SwitchCase) { - // Prevents #6328 - s.test.visit_mut_with(self); - let old_vars = self.vars.take(); - s.cons.visit_mut_with(self); - self.vars = old_vars; - } - - fn visit_mut_module_items(&mut self, n: &mut Vec) { - self.visit_mut_stmt_like(n) - } - - fn visit_mut_stmts(&mut self, n: &mut Vec) { - self.visit_mut_stmt_like(n) - } - - fn visit_mut_expr(&mut self, e: &mut Expr) { - e.visit_mut_children_with(self); - - match e { - Expr::Bin(BinExpr { - span, - left, - op: op!("??"), - right, - }) => { - // - let (l, aliased) = alias_if_required(left, "ref"); - - if aliased { - self.vars.push(VarDeclarator { - span: DUMMY_SP, - name: l.clone().into(), - init: None, - definite: false, - }); - } - - let var_expr = if aliased { - AssignExpr { - span: DUMMY_SP, - op: op!("="), - left: l.clone().into(), - right: left.take(), - } - .into() - } else { - l.clone().into() - }; - - *e = make_cond(self.c, *span, &l, var_expr, right.take()); - } - - Expr::Assign(ref mut assign @ AssignExpr { op: op!("??="), .. }) => { - match &mut assign.left { - AssignTarget::Simple(SimpleAssignTarget::Ident(i)) => { - *e = AssignExpr { - span: assign.span, - op: op!("="), - left: i.clone().into(), - right: Box::new(make_cond( - self.c, - assign.span, - &Ident::from(&*i), - Expr::Ident(Ident::from(&*i)), - assign.right.take(), - )), - } - .into(); - } - - AssignTarget::Simple(left) => { - let alias = alias_ident_for_simple_assign_tatget(left, "refs"); - self.vars.push(VarDeclarator { - span: DUMMY_SP, - name: alias.clone().into(), - init: None, - definite: false, - }); - - // TODO: Check for computed. - let right_expr = AssignExpr { - span: assign.span, - left: left.clone().into(), - op: op!("="), - right: assign.right.take(), - } - .into(); - - let var_expr = AssignExpr { - span: DUMMY_SP, - op: op!("="), - left: alias.clone().into(), - right: left.take().into(), - } - .into(); - - *e = AssignExpr { - span: assign.span, - op: op!("="), - left: alias.clone().into(), - right: Box::new(make_cond( - self.c, - assign.span, - &alias, - var_expr, - right_expr, - )), - } - .into(); - } - - _ => {} - } - } - - _ => {} - } - } - - fn visit_mut_block_stmt_or_expr(&mut self, n: &mut BlockStmtOrExpr) { - let vars = self.vars.take(); - n.visit_mut_children_with(self); - - if !self.vars.is_empty() { - if let BlockStmtOrExpr::Expr(expr) = n { - // expr - // { var decl = init; return expr; } - let stmts = vec![ - VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - decls: self.vars.take(), - declare: false, - ..Default::default() - } - .into(), - Stmt::Return(ReturnStmt { - span: DUMMY_SP, - arg: Some(expr.take()), - }), - ]; - *n = BlockStmtOrExpr::BlockStmt(BlockStmt { - span: DUMMY_SP, - stmts, - ..Default::default() - }); - } - } - - self.vars = vars; - } -} +/// Creates a nullish coalescing transformation pass +/// +/// This is now a thin wrapper around the Compiler implementation. +pub fn nullish_coalescing(c: Config) -> impl Pass + 'static { + let mut assumptions = Assumptions::default(); + assumptions.no_document_all = c.no_document_all; -#[tracing::instrument(level = "debug", skip_all)] -fn make_cond(c: Config, span: Span, alias: &Ident, var_expr: Expr, init: Box) -> Expr { - if c.no_document_all { - CondExpr { - span, - test: BinExpr { - span: DUMMY_SP, - left: Box::new(var_expr), - op: op!("!="), - right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), - } - .into(), - cons: alias.clone().into(), - alt: init, - } - } else { - CondExpr { - span, - test: BinExpr { - span: DUMMY_SP, - left: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - left: Box::new(var_expr), - op: op!("!=="), - right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), - })), - op: op!("&&"), - right: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - left: Box::new(Expr::Ident(alias.clone())), - op: op!("!=="), - right: Expr::undefined(DUMMY_SP), - })), - } - .into(), - cons: alias.clone().into(), - alt: init, - } - } - .into() + Compiler::new(swc_ecma_compiler::Config { + includes: Features::NULLISH_COALESCING, + assumptions, + ..Default::default() + }) } diff --git a/crates/swc_ecma_compiler/src/es2020/mod.rs b/crates/swc_ecma_compiler/src/es2020/mod.rs index bfe80ea8cf01..efabbc846a93 100644 --- a/crates/swc_ecma_compiler/src/es2020/mod.rs +++ b/crates/swc_ecma_compiler/src/es2020/mod.rs @@ -1 +1,2 @@ pub(crate) mod export_namespace_from; +pub(crate) mod nullish_coalescing; diff --git a/crates/swc_ecma_compiler/src/es2020/nullish_coalescing.rs b/crates/swc_ecma_compiler/src/es2020/nullish_coalescing.rs new file mode 100644 index 000000000000..bac9dc869e7d --- /dev/null +++ b/crates/swc_ecma_compiler/src/es2020/nullish_coalescing.rs @@ -0,0 +1,168 @@ +use swc_common::{util::take::Take, Span, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_utils::alias_ident_for_simple_assign_tatget; + +use crate::CompilerImpl; + +impl<'a> CompilerImpl<'a> { + /// Transform nullish coalescing operator (??) and assignment (??=) + pub(crate) fn transform_nullish_coalescing(&mut self, e: &mut Expr) -> bool { + match e { + Expr::Bin(BinExpr { + span, + left, + op: op!("??"), + right, + }) => { + // Alias the left side if needed + let (l, aliased) = swc_ecma_utils::alias_if_required(left, "ref"); + + if aliased { + self.es2020_nullish_coalescing_vars.push(VarDeclarator { + span: DUMMY_SP, + name: l.clone().into(), + init: None, + definite: false, + }); + } + + let var_expr = if aliased { + AssignExpr { + span: DUMMY_SP, + op: op!("="), + left: l.clone().into(), + right: left.take(), + } + .into() + } else { + l.clone().into() + }; + + *e = make_cond( + self.config.assumptions.no_document_all, + *span, + &l, + var_expr, + right.take(), + ); + true + } + + Expr::Assign(ref mut assign @ AssignExpr { op: op!("??="), .. }) => { + match &mut assign.left { + AssignTarget::Simple(SimpleAssignTarget::Ident(i)) => { + *e = AssignExpr { + span: assign.span, + op: op!("="), + left: i.clone().into(), + right: Box::new(make_cond( + self.config.assumptions.no_document_all, + assign.span, + &Ident::from(&*i), + Expr::Ident(Ident::from(&*i)), + assign.right.take(), + )), + } + .into(); + true + } + + AssignTarget::Simple(left) => { + let alias = alias_ident_for_simple_assign_tatget(left, "refs"); + self.es2020_nullish_coalescing_vars.push(VarDeclarator { + span: DUMMY_SP, + name: alias.clone().into(), + init: None, + definite: false, + }); + + // TODO: Check for computed. + let right_expr = AssignExpr { + span: assign.span, + left: left.clone().into(), + op: op!("="), + right: assign.right.take(), + } + .into(); + + let var_expr = AssignExpr { + span: DUMMY_SP, + op: op!("="), + left: alias.clone().into(), + right: left.take().into(), + } + .into(); + + *e = AssignExpr { + span: assign.span, + op: op!("="), + left: alias.clone().into(), + right: Box::new(make_cond( + self.config.assumptions.no_document_all, + assign.span, + &alias, + var_expr, + right_expr, + )), + } + .into(); + true + } + + _ => false, + } + } + + _ => false, + } + } +} + +/// Generate the conditional expression for nullish coalescing +#[tracing::instrument(level = "debug", skip_all)] +fn make_cond( + no_document_all: bool, + span: Span, + alias: &Ident, + var_expr: Expr, + init: Box, +) -> Expr { + if no_document_all { + CondExpr { + span, + test: BinExpr { + span: DUMMY_SP, + left: Box::new(var_expr), + op: op!("!="), + right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), + } + .into(), + cons: alias.clone().into(), + alt: init, + } + } else { + CondExpr { + span, + test: BinExpr { + span: DUMMY_SP, + left: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + left: Box::new(var_expr), + op: op!("!=="), + right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), + })), + op: op!("&&"), + right: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + left: Box::new(Expr::Ident(alias.clone())), + op: op!("!=="), + right: Expr::undefined(DUMMY_SP), + })), + } + .into(), + cons: alias.clone().into(), + alt: init, + } + } + .into() +} diff --git a/crates/swc_ecma_compiler/src/features.rs b/crates/swc_ecma_compiler/src/features.rs index b0bf26f438c0..9f519ab53ee6 100644 --- a/crates/swc_ecma_compiler/src/features.rs +++ b/crates/swc_ecma_compiler/src/features.rs @@ -8,6 +8,7 @@ bitflags! { const PRIVATE_IN_OBJECT = 1 << 2; const LOGICAL_ASSIGNMENTS = 1 << 3; const EXPORT_NAMESPACE_FROM = 1 << 4; + const NULLISH_COALESCING = 1 << 5; } } diff --git a/crates/swc_ecma_compiler/src/lib.rs b/crates/swc_ecma_compiler/src/lib.rs index 968abf8bbe9e..c08862af62fe 100644 --- a/crates/swc_ecma_compiler/src/lib.rs +++ b/crates/swc_ecma_compiler/src/lib.rs @@ -58,8 +58,11 @@ struct CompilerImpl<'a> { es2022_injected_weakset_vars: FxHashSet, es2022_current_class_data: ClassData, - // Logical assignments transformation state + // ES2021: Logical assignments transformation state es2021_logical_assignment_vars: Vec, + + // ES2020: Nullish coalescing transformation state + es2020_nullish_coalescing_vars: Vec, } #[swc_trace] @@ -72,6 +75,7 @@ impl<'a> CompilerImpl<'a> { es2022_injected_weakset_vars: FxHashSet::default(), es2022_current_class_data: ClassData::default(), es2021_logical_assignment_vars: Vec::new(), + es2020_nullish_coalescing_vars: Vec::new(), } } @@ -465,13 +469,43 @@ impl<'a> VisitMut for CompilerImpl<'a> { /// Prevents #1123 for nullish coalescing fn visit_mut_block_stmt(&mut self, s: &mut BlockStmt) { + // Setup phase: Save nullish coalescing vars + let old_es2020_nullish_coalescing_vars = + if self.config.includes.contains(Features::NULLISH_COALESCING) { + Some(self.es2020_nullish_coalescing_vars.take()) + } else { + None + }; + // Single recursive visit s.visit_mut_children_with(self); + + // Cleanup phase: Restore nullish coalescing vars + if let Some(old_vars) = old_es2020_nullish_coalescing_vars { + self.es2020_nullish_coalescing_vars = old_vars; + } } /// Prevents #1123 and #6328 for nullish coalescing fn visit_mut_switch_case(&mut self, s: &mut SwitchCase) { - s.visit_mut_children_with(self); + // Prevents #6328 + s.test.visit_mut_with(self); + + // Setup phase: Save nullish coalescing vars + let old_es2020_nullish_coalescing_vars = + if self.config.includes.contains(Features::NULLISH_COALESCING) { + Some(self.es2020_nullish_coalescing_vars.take()) + } else { + None + }; + + // Visit consequents + s.cons.visit_mut_with(self); + + // Cleanup phase: Restore nullish coalescing vars + if let Some(old_vars) = old_es2020_nullish_coalescing_vars { + self.es2020_nullish_coalescing_vars = old_vars; + } } fn visit_mut_assign_pat(&mut self, p: &mut AssignPat) { @@ -522,22 +556,26 @@ impl<'a> VisitMut for CompilerImpl<'a> { } fn visit_mut_expr(&mut self, e: &mut Expr) { - // Phase 1: Pre-processing - Check and apply transformations that replace the - // expression - let logical_transformed = self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS) - && self.transform_logical_assignment(e); - - // Phase 2: Setup for private field expressions + // Phase 1: Setup for private field expressions let prev_prepend_exprs = if self.config.includes.contains(Features::PRIVATE_IN_OBJECT) { Some(take(&mut self.es2022_private_field_init_exprs)) } else { None }; - // Phase 3: Single recursive visit + // Phase 2: Single recursive visit - Visit children first e.visit_mut_children_with(self); - // Phase 4: Post-processing transformations + // Phase 3: Post-processing transformations + // Apply transformations after visiting children (this matches the original + // order) + let logical_transformed = self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS) + && self.transform_logical_assignment(e); + + let nullish_transformed = !logical_transformed + && self.config.includes.contains(Features::NULLISH_COALESCING) + && self.transform_nullish_coalescing(e); + // Handle private field expressions if let Some(prev_prepend_exprs) = prev_prepend_exprs { let mut prepend_exprs = std::mem::replace( @@ -559,7 +597,7 @@ impl<'a> VisitMut for CompilerImpl<'a> { .into(); } } - } else if !logical_transformed { + } else if !logical_transformed && !nullish_transformed { // Transform private in expressions only if no other transformation occurred self.es2022_transform_private_in_to_weakset_has(e); } @@ -577,39 +615,91 @@ impl<'a> VisitMut for CompilerImpl<'a> { } // Setup for variable hoisting - let need_var_hoisting = self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS); + let need_logical_var_hoisting = + self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS); + let need_nullish_var_hoisting = self.config.includes.contains(Features::NULLISH_COALESCING); - let saved_logical_vars = if need_var_hoisting { + let saved_logical_vars = if need_logical_var_hoisting { self.es2021_logical_assignment_vars.take() } else { vec![] }; - // Single recursive visit - ns.visit_mut_children_with(self); + let saved_nullish_vars = if need_nullish_var_hoisting { + self.es2020_nullish_coalescing_vars.take() + } else { + vec![] + }; + + // Process statements with different hoisting strategies + if need_nullish_var_hoisting { + // Nullish coalescing: Insert vars before each statement that generates them + let mut buf = Vec::with_capacity(ns.len() + 2); + + for mut item in ns.take() { + item.visit_mut_with(self); + + // Insert nullish vars before the statement + if !self.es2020_nullish_coalescing_vars.is_empty() { + buf.push(ModuleItem::Stmt( + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2020_nullish_coalescing_vars.take(), + ..Default::default() + } + .into(), + )); + } - // Post-processing: Handle variable hoisting - if need_var_hoisting { - let logical_vars = - std::mem::replace(&mut self.es2021_logical_assignment_vars, saved_logical_vars); + // Collect logical vars but don't insert yet + buf.push(item); + } - let mut all_vars = Vec::new(); - all_vars.extend(logical_vars); + *ns = buf; - if !all_vars.is_empty() { + // Logical assignments: Hoist all vars to the top + if need_logical_var_hoisting && !self.es2021_logical_assignment_vars.is_empty() { prepend_stmt( ns, - VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - decls: all_vars, - ..Default::default() - } - .into(), + ModuleItem::Stmt( + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2021_logical_assignment_vars.take(), + ..Default::default() + } + .into(), + ), + ); + } + } else if need_logical_var_hoisting { + // Only logical assignments: Hoist all vars to the top + ns.visit_mut_children_with(self); + + if !self.es2021_logical_assignment_vars.is_empty() { + prepend_stmt( + ns, + ModuleItem::Stmt( + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2021_logical_assignment_vars.take(), + ..Default::default() + } + .into(), + ), ); } + } else { + // Single recursive visit + ns.visit_mut_children_with(self); } + // Restore saved vars + self.es2021_logical_assignment_vars = saved_logical_vars; + self.es2020_nullish_coalescing_vars = saved_nullish_vars; + // Post-processing: Private field variables if self.config.includes.contains(Features::PRIVATE_IN_OBJECT) && !self.es2022_private_field_helper_vars.is_empty() @@ -636,39 +726,87 @@ impl<'a> VisitMut for CompilerImpl<'a> { fn visit_mut_stmts(&mut self, s: &mut Vec) { // Setup for variable hoisting - let need_var_hoisting = self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS); + let need_logical_var_hoisting = + self.config.includes.contains(Features::LOGICAL_ASSIGNMENTS); + let need_nullish_var_hoisting = self.config.includes.contains(Features::NULLISH_COALESCING); - let saved_logical_vars = if need_var_hoisting { + let saved_logical_vars = if need_logical_var_hoisting { self.es2021_logical_assignment_vars.take() } else { vec![] }; - // Single recursive visit - s.visit_mut_children_with(self); + let saved_nullish_vars = if need_nullish_var_hoisting { + self.es2020_nullish_coalescing_vars.take() + } else { + vec![] + }; + + // Process statements with different hoisting strategies + if need_nullish_var_hoisting { + // Nullish coalescing: Insert vars before each statement that generates them + let mut buf = Vec::with_capacity(s.len() + 2); + + for mut stmt in s.take() { + stmt.visit_mut_with(self); + + // Insert nullish vars before the statement + if !self.es2020_nullish_coalescing_vars.is_empty() { + buf.push( + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2020_nullish_coalescing_vars.take(), + ..Default::default() + } + .into(), + ); + } - // Post-processing: Handle variable hoisting - if need_var_hoisting { - let logical_vars = - std::mem::replace(&mut self.es2021_logical_assignment_vars, saved_logical_vars); + // Collect logical vars but don't insert yet + buf.push(stmt); + } - let mut all_vars = Vec::new(); - all_vars.extend(logical_vars); + *s = buf; - if !all_vars.is_empty() { + // Logical assignments: Hoist all vars to the top + if need_logical_var_hoisting && !self.es2021_logical_assignment_vars.is_empty() { prepend_stmt( s, VarDecl { span: DUMMY_SP, kind: VarDeclKind::Var, - decls: all_vars, + decls: self.es2021_logical_assignment_vars.take(), ..Default::default() } .into(), ); } + } else if need_logical_var_hoisting { + // Only logical assignments: Hoist all vars to the top + s.visit_mut_children_with(self); + + if !self.es2021_logical_assignment_vars.is_empty() { + prepend_stmt( + s, + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2021_logical_assignment_vars.take(), + ..Default::default() + } + .into(), + ); + } + } else { + // Single recursive visit + s.visit_mut_children_with(self); } + // Restore saved vars + self.es2021_logical_assignment_vars = saved_logical_vars; + self.es2020_nullish_coalescing_vars = saved_nullish_vars; + // Post-processing: Private field variables if self.config.includes.contains(Features::PRIVATE_IN_OBJECT) && !self.es2022_private_field_helper_vars.is_empty() @@ -676,4 +814,42 @@ impl<'a> VisitMut for CompilerImpl<'a> { self.es2022_prepend_private_field_vars(s); } } + + fn visit_mut_block_stmt_or_expr(&mut self, n: &mut BlockStmtOrExpr) { + if !self.config.includes.contains(Features::NULLISH_COALESCING) { + n.visit_mut_children_with(self); + return; + } + + let vars = self.es2020_nullish_coalescing_vars.take(); + n.visit_mut_children_with(self); + + if !self.es2020_nullish_coalescing_vars.is_empty() { + if let BlockStmtOrExpr::Expr(expr) = n { + // expr + // { var decl = init; return expr; } + let stmts = vec![ + VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: self.es2020_nullish_coalescing_vars.take(), + declare: false, + ..Default::default() + } + .into(), + Stmt::Return(ReturnStmt { + span: DUMMY_SP, + arg: Some(expr.take()), + }), + ]; + *n = BlockStmtOrExpr::BlockStmt(BlockStmt { + span: DUMMY_SP, + stmts, + ..Default::default() + }); + } + } + + self.es2020_nullish_coalescing_vars = vars; + } }