diff --git a/src/ast/expr.rs b/src/ast/expr.rs index cfedeb4f9bf8..59cfea86efcf 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -1247,7 +1247,7 @@ impl Expr { // will potentially be simplified to avoid generating unnecessary extra "!" // operators. For example, calling this with "!!x" will return "!x" instead // of returning "!!!x". - pub(crate) fn not(&self, bump: &Bump) -> Expr { + pub fn not(&self, bump: &Bump) -> Expr { self.maybe_simplify_not(bump).unwrap_or_else(|| { Expr::init( E::Unary { @@ -2491,6 +2491,15 @@ impl Data { break 'brk PrimitiveType::Mixed; // Can be number or bigint or string (or an exception) } + crate::OpCode::BinAddAssign => { + if binary.right.data.known_primitive_with_check(stack_check) + == PrimitiveType::String + { + break 'brk PrimitiveType::String; + } + break 'brk PrimitiveType::Mixed; // Can be number or bigint or string (or an exception) + } + crate::OpCode::BinSub | crate::OpCode::BinSubAssign | crate::OpCode::BinMul @@ -2629,7 +2638,7 @@ impl Data { } } - pub(crate) fn extract_numeric_value(&self) -> Option { + pub fn extract_numeric_value(&self) -> Option { match self { Data::ENumber(n) => Some(n.value()), Data::EInlinedEnum(inlined) => match &inlined.value.data { diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index bf59458d4fd6..6b4ad9243bc3 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2517,6 +2517,8 @@ pub mod parse_worker { options::Format::InternalBakeDev => js_parser::options::Format::InternalBakeDev, }; opts.features.minify_syntax = topts.minify_syntax; + // A bundle is a build: it gets every syntax pass. + opts.features.minify_syntax_statements = topts.minify_syntax; opts.features.minify_identifiers = topts.minify_identifiers; opts.features.minify_keep_names = topts.keep_names; opts.features.minify_whitespace = topts.minify_whitespace; diff --git a/src/bundler/options.rs b/src/bundler/options.rs index 0197ab08af0d..51af96544ef2 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -1308,6 +1308,10 @@ pub struct BundleOptions<'a> { pub inline_entrypoint_import_meta_main: bool, pub minify_whitespace: bool, pub minify_syntax: bool, + /// Statement restructuring on top of `minify_syntax`. Set by `bun build` + /// only (the bundler's parse task derives it from `minify_syntax` itself). + /// See `bun_js_parser::RuntimeFeatures::minify_syntax_statements`. + pub minify_syntax_statements: bool, pub minify_identifiers: bool, pub keep_names: bool, pub dead_code_elimination: bool, @@ -1519,6 +1523,7 @@ impl<'a> BundleOptions<'a> { inline_entrypoint_import_meta_main: self.inline_entrypoint_import_meta_main, minify_whitespace: self.minify_whitespace, minify_syntax: self.minify_syntax, + minify_syntax_statements: self.minify_syntax_statements, minify_identifiers: self.minify_identifiers, keep_names: self.keep_names, dead_code_elimination: self.dead_code_elimination, @@ -1768,6 +1773,7 @@ impl<'a> BundleOptions<'a> { inline_entrypoint_import_meta_main: false, minify_whitespace: false, minify_syntax: false, + minify_syntax_statements: false, minify_identifiers: false, keep_names: false, dead_code_elimination: true, diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 9c1b3090ee64..9c3713fd9d40 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1644,6 +1644,7 @@ impl<'a> Transpiler<'a> { opts.features.inject_jest_globals = this_parse.inject_jest_globals; opts.features.minify_syntax = self.options.minify_syntax; + opts.features.minify_syntax_statements = self.options.minify_syntax_statements; opts.features.minify_identifiers = self.options.minify_identifiers; opts.features.dead_code_elimination = self.options.dead_code_elimination; opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper; diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index a96df3d43031..df565f4a2131 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -442,6 +442,12 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { /// See [`bun_react_compiler::PendingCompile`]. Set by `visit_func` / /// arrow-visit, consumed inside `visit_stmts` between visit and mangle. pub(crate) react_compiler_pending: Option, + /// True while visiting the body of a function the React Compiler may + /// compile. See `full_minify_syntax`. + pub(crate) in_react_compiler_candidate: bool, + /// Nesting of the `if (x) return; rest` to `if (!x) { rest }` rewrite in + /// `mangle_stmts`, bounded by `MAX_MANGLE_NESTING`. + pub(crate) mangle_jump_depth: u32, /// Compiled args/flags written by the `visit_stmts` hook for `visit_func` / /// arrow-visit to apply to the original `G::Fn` / `E::Arrow`. pub(crate) react_compiler_result: Option, @@ -508,6 +514,10 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub(crate) stmt_expr_value: js_ast::ExprData, pub(crate) call_target: js_ast::ExprData, pub(crate) delete_target: js_ast::ExprData, + /// The tag of the tagged template being visited. A tag call binds `this` + /// like a method call, so a rewrite that turns a tag into a property access + /// must keep `this` undefined, as `call_target` does for a call. + pub(crate) template_tag: js_ast::ExprData, pub(crate) loop_body: js_ast::StmtData, pub(crate) module_scope: js_ast::StoreRef, pub(crate) module_scope_directive_loc: bun_ast::Loc, @@ -5727,6 +5737,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O true } + /// `minify_syntax_statements` (see `RuntimeFeatures`): every pass that + /// merges or restructures statements (comma joins, `if` to `&&`/`?:`, + /// return and throw chains, `while` to `for`, arrow bodies to expressions) + /// and every expression rewrite that changes how a function prints checks + /// this instead of `minify_syntax`. + /// + /// It is off inside a function body the React Compiler may compile: the + /// compiler sees the body after its statements were visited, and it does + /// not lower some shapes the statement mangler produces (a `for (;;)` loop + /// made from a `while`, `ref.current == null && (ref.current = x)` made + /// from an `if`). + pub(crate) fn full_minify_syntax(&self) -> bool { + self.options.features.minify_syntax + && self.options.features.minify_syntax_statements + && !self.in_react_compiler_candidate + } + // TODO: // When React Fast Refresh is enabled, anything that's a JSX component should not be removable // This is to improve the reliability of fast refresh between page loads. @@ -6346,43 +6373,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - pub(crate) fn append_if_body_preserving_scope( - &mut self, - stmts: &mut ListManaged<'a, Stmt>, - body: Stmt, - ) -> Result<(), crate::Error> { - if let js_ast::StmtData::SBlock(block) = &body.data { - // `S::Block.stmts` is `StoreSlice` arena-owned for parser 'a; no aliasing &mut. - let block_stmts: &[Stmt] = block.stmts.slice(); - let mut keep_block = false; - for stmt in block_stmts { - if statement_cares_about_scope(stmt) { - keep_block = true; - break; - } - } - if !keep_block && !block_stmts.is_empty() { - stmts.extend_from_slice(block_stmts); - return Ok(()); - } - } - - if statement_cares_about_scope(&body) { - let block_stmts = self.arena.alloc_slice_copy(&[body]); - stmts.push(self.s( - S::Block { - stmts: block_stmts.into(), - close_brace_loc: bun_ast::Loc::EMPTY, - }, - body.loc, - )); - return Ok(()); - } - - stmts.push(body); - Ok(()) - } - #[cold] #[inline(never)] fn mark_exported_binding_inside_namespace(&mut self, r#ref: Ref, binding: BindingNodeIndex) { @@ -9174,6 +9164,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O call_target: null_expr_data(), delete_target: null_expr_data(), + template_tag: null_expr_data(), stmt_expr_value: null_expr_data(), loop_body: null_stmt_data(), define, @@ -9265,6 +9256,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O react_compiler_candidate_name: None, react_compiler_in_react_hoc: false, react_compiler_pending: None, + in_react_compiler_candidate: false, + mangle_jump_depth: 0, react_compiler_result: None, server_components_wrap_ref: Ref::NONE, jest: Jest::default(), diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index d0edbf4f2846..a08192e7bb68 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -194,6 +194,7 @@ impl<'a> Options<'a> { no_macros: f.no_macros, commonjs_named_exports: f.commonjs_named_exports, minify_syntax: f.minify_syntax, + minify_syntax_statements: f.minify_syntax_statements, minify_identifiers: f.minify_identifiers, minify_keep_names: f.minify_keep_names, minify_whitespace: f.minify_whitespace, @@ -268,12 +269,6 @@ impl<'a> Options<'a> { self.features.hash_for_runtime_transpiler(hasher); } - // Used to determine if `joinWithComma` should be called in `visitStmts`. We do this - // to avoid changing line numbers too much to make source mapping more readable - pub(crate) fn runtime_merge_adjacent_expression_statements(&self) -> bool { - self.bundle - } - pub fn init(jsx: options::JSX::Pragma, loader: options::Loader) -> Options<'static> { // `macro_context` is `None` // (see field comment); caller overwrites before use. diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 3937affd4b9f..2cd2242d6488 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -199,6 +199,14 @@ pub mod Runtime { pub commonjs_named_exports: bool, pub minify_syntax: bool, + /// The rest of esbuild's `--minify-syntax`: statement restructuring + /// (`if` chains to `?:`/`&&`/`||`, `while` to `for`, merged returns) + /// and the expression rewrites that change how a function prints + /// (`===` to `==`, `a === null || a === void 0` to `a == null`). + /// Only `bun build` sets it. `minify_syntax` alone keeps one output + /// statement per source statement, which `bun run` relies on for + /// source maps, coverage and `Function.prototype.toString()`. + pub minify_syntax_statements: bool, pub minify_identifiers: bool, /// Preserve function/class names during minification (CLI: --keep-names) pub minify_keep_names: bool, @@ -289,6 +297,7 @@ pub mod Runtime { no_macros: false, commonjs_named_exports: true, minify_syntax: false, + minify_syntax_statements: false, minify_identifiers: false, minify_keep_names: false, minify_whitespace: false, @@ -359,13 +368,14 @@ pub mod Runtime { pub(crate) fn hash_for_runtime_transpiler(&self, hasher: &mut Wyhash) { debug_assert!(self.runtime_transpiler_cache.is_some()); - let bools: [bool; 17] = [ + let bools: [bool; 18] = [ self.top_level_await, self.auto_import_jsx, self.allow_runtime, self.inlining, self.commonjs_named_exports, self.minify_syntax, + self.minify_syntax_statements, self.minify_identifiers, self.minify_keep_names, self.dead_code_elimination, diff --git a/src/js_parser/scan/scan_side_effects.rs b/src/js_parser/scan/scan_side_effects.rs index f541f38bad57..ffbb16dc886b 100644 --- a/src/js_parser/scan/scan_side_effects.rs +++ b/src/js_parser/scan/scan_side_effects.rs @@ -35,62 +35,148 @@ impl SideEffects { && left != bun_ast::expr::PrimitiveType::Mixed } + /// Simplify an expression that is only tested for truthiness, such as the + /// test of an `if`. Port of esbuild's `SimplifyBooleanExpr`. + /// + /// Some callers run this without `minify_syntax`, so the rules that + /// rewrite operands or comparisons only run for a minified build. The + /// rest is the behavior those callers had before. pub(crate) fn simplify_boolean<'a, const TS: bool, const SCAN: bool>( - p: &P<'a, TS, SCAN>, + p: &mut P<'a, TS, SCAN>, expr: Expr, ) -> Expr { - if !p.options.features.dead_code_elimination { + if !p.options.features.dead_code_elimination || !p.stack_check.is_safe_to_recurse() { return expr; } - let mut result: Expr = expr; - Self::_simplify_boolean(p, &mut result); - result - } + let minify = p.full_minify_syntax(); - fn _simplify_boolean<'a, const TS: bool, const SCAN: bool>( - p: &P<'a, TS, SCAN>, - expr: &mut Expr, - ) { - loop { - match &mut expr.data { - ExprData::EUnary(e) => { - if e.op == Op::Code::UnNot { - // "!!a" => "a" - if let ExprData::EUnary(inner) = &e.value.data { - if inner.op == Op::Code::UnNot { - *expr = inner.value; - continue; - } + match expr.data { + ExprData::EUnary(mut e) if e.op == Op::Code::UnNot => { + // "!!a" => "a" + if let ExprData::EUnary(inner) = e.value.data + && inner.op == Op::Code::UnNot + { + return Self::simplify_boolean(p, inner.value); + } + + // "!!!a" => "!a" + e.value = Self::simplify_boolean(p, e.value); + } + + ExprData::EBinary(mut e) => match e.op { + Op::Code::BinStrictEq + | Op::Code::BinStrictNe + | Op::Code::BinLooseEq + | Op::Code::BinLooseNe => { + if minify + && let Some(r) = e.right.data.extract_numeric_value() + && r == 0.0 + && is_int32_or_uint32(&e.left.data, p.stack_check) + { + // If the left is guaranteed to be an integer (e.g. not NaN, + // Infinity, or a non-numeric value) then a test against zero + // in a boolean context is unnecessary because the value is + // only truthy if it's not zero. + if e.op == Op::Code::BinStrictNe || e.op == Op::Code::BinLooseNe { + // "if ((a >>> b) !== 0)" => "if (a >>> b)" + return e.left; } - Self::_simplify_boolean(p, &mut e.value); + // "if ((a >>> b) === 0)" => "if (!(a >>> b))" + return e.left.not(p.arena); } } - ExprData::EBinary(e) => match e.op { - Op::Code::BinLogicalAnd => { - if let Some(effects) = SideEffects::to_boolean(p, &e.right.data) { - if effects.value && effects.side_effects == SideEffects::NoSideEffects { - // "if (anything && truthyNoSideEffects)" => "if (anything)" - *expr = e.left; - continue; - } - } + + Op::Code::BinLogicalAnd => { + // "if (!!a && !!b)" => "if (a && b)" + if minify { + e.left = Self::simplify_boolean(p, e.left); + e.right = Self::simplify_boolean(p, e.right); } - Op::Code::BinLogicalOr => { - if let Some(effects) = SideEffects::to_boolean(p, &e.right.data) { - if !effects.value && effects.side_effects == SideEffects::NoSideEffects - { - // "if (anything || falsyNoSideEffects)" => "if (anything)" - *expr = e.left; - continue; - } - } + + if let Some(effects) = SideEffects::to_boolean(p, &e.right.data) + && effects.value + && effects.side_effects == SideEffects::NoSideEffects + { + // "if (anything && truthyNoSideEffects)" => "if (anything)" + return if minify { + e.left + } else { + Self::simplify_boolean(p, e.left) + }; } - _ => {} - }, + } + + Op::Code::BinLogicalOr => { + // "if (!!a || !!b)" => "if (a || b)" + if minify { + e.left = Self::simplify_boolean(p, e.left); + e.right = Self::simplify_boolean(p, e.right); + } + + if let Some(effects) = SideEffects::to_boolean(p, &e.right.data) + && !effects.value + && effects.side_effects == SideEffects::NoSideEffects + { + // "if (anything || falsyNoSideEffects)" => "if (anything)" + return if minify { + e.left + } else { + Self::simplify_boolean(p, e.left) + }; + } + } + _ => {} + }, + + ExprData::EIf(mut e) if minify => { + // "if (a ? !!b : !!c)" => "if (a ? b : c)" + e.yes = Self::simplify_boolean(p, e.yes); + e.no = Self::simplify_boolean(p, e.no); + + if let Some(effects) = SideEffects::to_boolean(p, &e.yes.data) + && effects.side_effects == SideEffects::NoSideEffects + { + if effects.value { + // "if (anything1 ? truthyNoSideEffects : anything2)" => "if (anything1 || anything2)" + return Expr::join_with_left_associative_op( + Op::Code::BinLogicalOr, + e.test, + e.no, + ); + } + // "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 && anything2)" + return Expr::join_with_left_associative_op( + Op::Code::BinLogicalAnd, + e.test.not(p.arena), + e.no, + ); + } + + if let Some(effects) = SideEffects::to_boolean(p, &e.no.data) + && effects.side_effects == SideEffects::NoSideEffects + { + if effects.value { + // "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)" + return Expr::join_with_left_associative_op( + Op::Code::BinLogicalOr, + e.test.not(p.arena), + e.yes, + ); + } + // "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)" + return Expr::join_with_left_associative_op( + Op::Code::BinLogicalAnd, + e.test, + e.yes, + ); + } } - break; + + _ => {} } + + expr } // Re-exports of ExprData methods. @@ -327,6 +413,13 @@ impl SideEffects { Op::Code::BinLogicalAnd | Op::Code::BinLogicalOr | Op::Code::BinNullishCoalescing => { + // If this is a boolean logical operation and the result is unused, then + // we know the left operand will only be used for its boolean value and + // can be simplified under that assumption + if bin.op != Op::Code::BinNullishCoalescing && p.full_minify_syntax() { + bin.left = Self::simplify_boolean(p, bin.left); + } + let right = bin.right; bin.right = Self::simplify_unused_expr(p, right) .unwrap_or_else(|| right.to_empty()); @@ -337,6 +430,39 @@ impl SideEffects { if bin.right.is_empty() { return Self::simplify_unused_expr(p, bin.left); } + + // Try to take advantage of the optional chain operator to shorten code + if p.full_minify_syntax() + && let ExprData::EBinary(binary) = bin.left.data + // "a != null && a.b()" => "a?.b()" + // "a == null || a.b()" => "a?.b()" + && ((binary.op == Op::Code::BinLooseNe + && bin.op == Op::Code::BinLogicalAnd) + || (binary.op == Op::Code::BinLooseEq + && bin.op == Op::Code::BinLogicalOr)) + { + let test = if matches!(binary.right.data, ExprData::ENull(_)) { + Some(binary.left) + } else if matches!(binary.left.data, ExprData::ENull(_)) { + Some(binary.right) + } else { + None + }; + + // Note: Technically unbound identifiers can refer to a getter on + // the global object and that getter can have side effects that can + // be observed if we run that getter once instead of twice. But this + // seems like terrible coding practice and very unlikely to come up + // in real software, so we deliberately ignore this possibility and + // optimize for size instead of for this obscure edge case. + if let Some(test) = test + && let ExprData::EIdentifier(id) = test.data + && !id.must_keep_due_to_with_stmt() + && p.try_to_insert_optional_chain(test, bin.right) + { + return Some(bin.right); + } + } } _ => {} @@ -1001,3 +1127,27 @@ impl SideEffects { } } } + +/// True when the expression always evaluates to an int32 or uint32, so that it +/// is falsy exactly when it is zero. +fn is_int32_or_uint32(data: &ExprData, stack_check: bun_core::StackCheck) -> bool { + if !stack_check.is_safe_to_recurse() { + return false; + } + match data { + ExprData::EBinary(e) => match e.op { + // This is the only bitwise operator that can't return a bigint (because it throws instead) + Op::Code::BinUShr => true, + Op::Code::BinLogicalOr | Op::Code::BinLogicalAnd => { + is_int32_or_uint32(&e.left.data, stack_check) + && is_int32_or_uint32(&e.right.data, stack_check) + } + _ => false, + }, + ExprData::EIf(e) => { + is_int32_or_uint32(&e.yes.data, stack_check) + && is_int32_or_uint32(&e.no.data, stack_check) + } + _ => false, + } +} diff --git a/src/js_parser/visit/mangle.rs b/src/js_parser/visit/mangle.rs new file mode 100644 index 000000000000..da4146e7429a --- /dev/null +++ b/src/js_parser/visit/mangle.rs @@ -0,0 +1,1574 @@ +//! Statement-level syntax minification. Ports of esbuild's `mangleStmts`, +//! `mangleIf`, `mangleFor`, and of the conditional-expression folding +//! (`MangleIfExpr`) the statement passes build on. +//! +//! `mangle_stmts` is the tail of `visit_stmts`: it runs once every statement in +//! a list has been visited, so every branch and loop body it looks at has +//! already been mangled. The passes ported here check +//! `P::full_minify_syntax()`. The merges that predate the port (adjacent +//! declarations, `var f; f = 1`, single-use `let` inlining) and the constant +//! folding keep running whenever `minify_syntax` is on, as they did before. + +use crate::p::P; +use crate::parser::{StmtsKind, statement_cares_about_scope}; +use crate::scan::scan_side_effects::SideEffects; +use bun_alloc::{Arena as Bump, ArenaVec as BumpVec, ArenaVecExt as _}; +use bun_ast::b::B as BData; +use bun_ast::expr::{Equality, StrictEql}; +use bun_ast::s::{Kind as LocalKind, LocalOrigin}; +use bun_ast::{ + E, Expr, ExprData, G, Loc, OpCode, OptionalChain, Ref, S, Stmt, StmtData, StoreRef, StoreSlice, + Symbol, +}; +use bun_collections::VecExt; + +type ListManaged<'bump, T> = BumpVec<'bump, T>; + +/// How many `if (x) return;` statements in one list become one nested `&&` +/// chain, and how many `if (a) return b;` statements become one `?:` chain. +/// Past this the rest of the list stays as it is. esbuild has no bound and +/// goes quadratic on such lists (11 s and 11 GB for 16,000 statements), and +/// the chains it builds are as deep as the list, which the recursive walkers +/// and the printer here cannot take. +const MAX_MANGLE_NESTING: u32 = 128; + +impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { + /// Merge and simplify a list of visited statements. Port of esbuild's + /// `mangleStmts`. The caller checks `minify_syntax` and + /// `dead_code_elimination`. + pub(crate) fn mangle_stmts( + &mut self, + mut stmts: ListManaged<'a, Stmt>, + kind: StmtsKind, + ) -> ListManaged<'a, Stmt> { + let p = self; + + // SAFETY: current_scope is a valid arena ptr for the parse. + if p.current_scope().parent.is_some() && !p.current_scope().contains_direct_eval { + // Remove inlined constants now that we know whether any of these statements + // contained a direct eval() or not. This can't be done earlier when we + // encounter the constant because we haven't encountered the eval() yet. + // Inlined constants are not removed if they are in a top-level scope or + // if they are exported (which could be in a nested TypeScript namespace). + if p.const_values.count() > 0 { + let items: &mut [Stmt] = stmts.as_mut_slice(); + for stmt in items.iter_mut() { + match stmt.data { + StmtData::SEmpty(_) + | StmtData::SComment(_) + | StmtData::SDirective(_) + | StmtData::SDebugger(_) + | StmtData::STypeScript(_) => continue, + StmtData::SLocal(mut local) => { + // "using" / "await using" declarations have disposal + // side-effects on scope exit. Their refs can end up in + // `const_values` via the macro path in `visitDecl` + // (`could_be_macro`), so skip them here to avoid + // silently dropping the declaration. + if local.kind.is_using() { + continue; + } + if !local.is_export && !local.origin.is_commonjs_export() { + let mut any_decl_in_const_values = local.kind == LocalKind::KConst; + let decls: &mut [G::Decl] = local.decls.slice_mut(); + let mut end: usize = 0; + for idx in 0..decls.len() { + if let BData::BIdentifier(id_ptr) = decls[idx].binding.data { + let id_ref = id_ptr.r#ref; + if p.const_values.contains(&id_ref) { + any_decl_in_const_values = true; + let symbol = &p.symbols[id_ref.inner_index() as usize]; + if symbol.use_count_estimate == 0 { + // Skip declarations that are constants with zero usage + continue; + } + } + } + // `Decl` is field-wise `Copy` but lacks the + // derive; `swap` compacts in place (idx >= end always). + decls.swap(end, idx); + end += 1; + } + local.decls.truncate(end); + if any_decl_in_const_values { + if end == 0 { + *stmt = stmt.to_empty(); + } + continue; + } + } + } + _ => {} + } + + // Break after processing relevant statements + break; + } + } + } + + let full = p.full_minify_syntax(); + let mut is_control_flow_dead = false; + + let mut output: ListManaged<'a, Stmt> = ListManaged::with_capacity_in(stmts.len(), p.arena); + + for i in 0..stmts.len() { + let stmt = stmts[i]; + if is_control_flow_dead + && !SideEffects::should_keep_stmt_in_dead_control_flow(stmt, p.arena) + { + // Strip unnecessary statements if the control flow is dead here + continue; + } + + // Inline single-use variable declarations where possible: + // + // // Before + // let x = fn(); + // return x.y(); + // + // // After + // return fn().y(); + // + // The declaration must not be exported. We can't just check for the + // "export" keyword because something might do "export {id};" later on. + // Instead we just ignore all top-level declarations for now. That means + // this optimization currently only applies in nested scopes. + // + // Ignore declarations if the scope is shadowed by a direct "eval" call. + // The eval'd code may indirectly reference this symbol and the actual + // use count may be greater than 1. + // SAFETY: current_scope is a valid arena ptr for the parse. + if p.current_scope != p.module_scope && !p.current_scope().contains_direct_eval { + // Keep inlining variables until a failure or until there are none left. + // That handles cases like this: + // + // // Before + // let x = fn(); + // let y = x.prop; + // return y; + // + // // After + // return fn().prop; + // + 'inner: while output.len() > 0 { + // Ignore "var" declarations since those have function-level scope and + // we may not have visited all of their uses yet by this point. We + // should have visited all the uses of "let" and "const" declarations + // by now since they are scoped to this block which we just finished + // visiting. + let prev_idx = output.len() - 1; + // borrowck: read the `StoreRef` (Copy) first, then re-borrow + // `output` only when truncating. + let StmtData::SLocal(mut local) = output[prev_idx].data else { + break; + }; + // "using" / "await using" declarations have disposal + // side-effects on scope exit, so they must not be + // removed by inlining their initializer into the use. + if local.decls.len_u32() == 0 + || local.kind == LocalKind::KVar + || local.kind.is_using() + || local.is_export + { + break; + } + + // The variable must be initialized, since we will be substituting + // the value into the usage. + let last_idx = (local.decls.len_u32() - 1) as usize; + let last: &mut G::Decl = &mut local.decls.slice_mut()[last_idx]; + let Some(replacement) = last.value else { break }; + + // The binding must be an identifier that is only used once. + // Ignore destructuring bindings since that's not the simple case. + // Destructuring bindings could potentially execute side-effecting + // code which would invalidate reordering. + let BData::BIdentifier(ident_ptr) = last.binding.data else { + break; + }; + let id = ident_ptr.r#ref; + + let symbol: &Symbol = &p.symbols[id.inner_index() as usize]; + + // Try to substitute the identifier with the initializer. This will + // fail if something with side effects is in between the declaration + // and the usage. + if symbol.use_count_estimate == 1 + && p.substitute_single_use_symbol_in_stmt(stmt, id, replacement) + { + // `const ns = await import(x); return ns` — the single use just + // moved into `replacement`; unless it was an accounted-for read + // (`f(ns.a)`), the namespace escapes there. + if p.dynamic_import_namespace_locals.contains_key(&id) + && p.namespace_tracked_uses.get(&id).copied().unwrap_or(0) == 0 + { + // Read as "more uses than accounted for" when finalizing. + p.namespace_tracked_uses.insert(id, u32::MAX); + } + match local.decls.len_u32() { + 1 => { + local.decls.clear(); + let new_len = output.len() - 1; + output.truncate(new_len); + continue 'inner; + } + _ => { + let n = local.decls.len() - 1; + local.decls.truncate(n); + continue 'inner; + } + } + } + break; + } + } + + // don't merge super calls to ensure they are called before "this" is accessed + if stmt.is_super_call() { + output.push(stmt); + continue; + } + + match stmt.data { + StmtData::SEmpty(_) => continue, + + // skip directives for now + StmtData::SDirective(_) => continue, + + StmtData::SLocal(local) => { + // Merge adjacent local statements + if output.len() > 0 { + let prev_idx = output.len() - 1; + let prev_stmt = &mut output[prev_idx]; + if let StmtData::SLocal(mut prev_local) = prev_stmt.data { + if local.can_merge_with(&prev_local) { + append_decls(&mut prev_local.decls, local.decls.slice()); + continue; + } + } + } + } + + StmtData::SExpr(s_expr) => { + // Merge adjacent expression statements + if output.len() > 0 { + let prev_idx = output.len() - 1; + let prev_stmt = &mut output[prev_idx]; + if let StmtData::SExpr(mut prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() && full { + prev_expr.does_not_affect_tree_shaking = prev_expr + .does_not_affect_tree_shaking + && s_expr.does_not_affect_tree_shaking; + prev_expr.value = + Expr::join_with_comma(prev_expr.value, s_expr.value); + continue; + } + } else if let StmtData::SLocal(prev_local) = prev_stmt.data { + // + // Input: + // var f; + // f = 123; + // Output: + // var f = 123; + // + // This doesn't handle every case. Only the very simple one. + if let ExprData::EBinary(bin_assign) = s_expr.value.data { + if prev_local.decls.len_u32() == 1 + && bin_assign.op == OpCode::BinAssign + // we can only do this with var because var is hoisted + // the statement we are merging into may use the statement before its defined. + && prev_local.kind == LocalKind::KVar + { + if let ExprData::EIdentifier(left_id) = bin_assign.left.data { + // `prev_local` is a `StoreRef` (Copy) so + // re-slicing here writes through to the arena slot. + let mut prev_local = prev_local; + let decl = &mut prev_local.decls.slice_mut()[0]; + if let BData::BIdentifier(bid_ptr) = decl.binding.data { + let bid_ref = bid_ptr.r#ref; + if bid_ref.eql(left_id.ref_) + // If the value was assigned, we shouldn't merge it incase it was used in the current statement + // https://github.com/oven-sh/bun/issues/2948 + // We don't have a more granular way to check symbol usage so this is the best we can do + && decl.value.is_none() + { + decl.value = Some(bin_assign.right); + p.ignore_usage(left_id.ref_); + continue; + } + } + } + } + } + } + } + } + StmtData::SSwitch(mut s_switch) => { + // Absorb a previous expression statement + if output.len() > 0 && full { + let prev_idx = output.len() - 1; + let prev_stmt = output[prev_idx]; + if let StmtData::SExpr(prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() { + s_switch.test = + Expr::join_with_comma(prev_expr.value, s_switch.test); + output.truncate(prev_idx); + } + } + } + } + StmtData::SIf(mut s_if) => { + // Absorb a previous expression statement + if output.len() > 0 && full { + let prev_idx = output.len() - 1; + let prev_stmt = output[prev_idx]; + if let StmtData::SExpr(prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() { + s_if.test = Expr::join_with_comma(prev_expr.value, s_if.test); + output.truncate(prev_idx); + } + } + } + + if full && is_jump_statement(s_if.yes.data) { + // Absorb a previous if statement + if let Some(&prev_stmt) = output.last() { + if let StmtData::SIf(prev_if) = prev_stmt.data { + if prev_if.no.is_none() + && p.jump_stmts_look_the_same(prev_if.yes.data, s_if.yes.data) + { + // "if (a) break c; if (b) break c;" => "if (a || b) break c;" + // "if (a) continue c; if (b) continue c;" => "if (a || b) continue c;" + // "if (a) return c; if (b) return c;" => "if (a || b) return c;" + // "if (a) throw c; if (b) throw c;" => "if (a || b) throw c;" + s_if.test = Expr::join_with_left_associative_op( + OpCode::BinLogicalOr, + prev_if.test, + s_if.test, + ); + let new_len = output.len() - 1; + output.truncate(new_len); + } + } + } + + // "while (x) { if (y) continue; z(); }" => "while (x) { if (!y) z(); }" + // "while (x) { if (y) continue; else z(); w(); }" => "while (x) { if (!y) { z(); w(); } }" => "for (; x;) !y && (z(), w());" + // + // "let x = () => { if (y) return; z(); };" => "let x = () => { if (!y) z(); };" + // "let x = () => { if (y) return; else z(); w(); };" => "let x = () => { if (!y) { z(); w(); } };" => "let x = () => { !y && (z(), w()); };" + let optimize_implicit_jump = match (kind, s_if.yes.data) { + (StmtsKind::LoopBody, StmtData::SContinue(c)) => c.label.is_none(), + (StmtsKind::FnBody, StmtData::SReturn(r)) => r.value.is_none(), + _ => false, + }; + + if optimize_implicit_jump + && p.mangle_jump_depth < MAX_MANGLE_NESTING + && p.stack_check.is_safe_to_recurse() + { + let mut body: ListManaged<'a, Stmt> = + ListManaged::with_capacity_in(1 + stmts.len() - i, p.arena); + if let Some(no) = s_if.no { + body.push(no); + } + body.extend_from_slice(&stmts[i + 1..]); + + // Don't do this transformation if the branch condition could + // potentially access symbols declared later on this scope below. + // If so, inverting the branch condition and nesting statements after + // this in a block would break that access which is a behavior change. + // + // // This transformation is incorrect + // if (a()) return; function a() {} + // if (!a()) { function a() {} } + // + // // This transformation is incorrect + // if (a(() => b)) return; let b; + // if (a(() => b)) { let b; } + // + if !stmts_care_about_scope(&body) { + p.mangle_jump_depth += 1; + let body = p.mangle_stmts(body, kind); + p.mangle_jump_depth -= 1; + let body_loc = body.first().map_or(s_if.yes.loc, |s| s.loc); + let test = SideEffects::simplify_boolean(p, s_if.test.not(p.arena)); + let yes = + p.stmts_to_single_stmt(body_loc, body.into_bump_slice_mut()); + let new_if = p.s( + S::If { + test, + yes, + no: None, + }, + stmt.loc, + ); + let StmtData::SIf(new_if_ref) = new_if.data else { + unreachable!() + }; + p.mangle_if(&mut output, stmt.loc, new_if_ref); + return output; + } + } + + if s_if.no.is_some() { + // "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;" + let mut cur_stmt = stmt; + let mut cur_if = s_if; + loop { + output.push(cur_stmt); + cur_stmt = cur_if.no.take().expect("checked above"); + let StmtData::SIf(next_if) = cur_stmt.data else { + break; + }; + if !is_jump_statement(next_if.yes.data) || next_if.no.is_none() { + break; + } + cur_if = next_if; + } + p.append_if_body_preserving_scope(&mut output, cur_stmt); + if is_jump_statement(cur_stmt.data) { + is_control_flow_dead = true; + } + continue; + } + } + } + + StmtData::SReturn(mut ret) => { + // Merge return statements with the previous expression statement + if output.len() > 0 && ret.value.is_some() && full { + let prev_idx = output.len() - 1; + let prev_stmt = output[prev_idx]; + if let StmtData::SExpr(prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() { + ret.value = Some(Expr::join_with_comma( + prev_expr.value, + ret.value.unwrap(), + )); + output[prev_idx] = stmt; + continue; + } + } + } + + is_control_flow_dead = true; + } + + StmtData::SBreak(_) | StmtData::SContinue(_) => { + is_control_flow_dead = true; + } + + StmtData::SThrow(s_throw) => { + // Merge throw statements with the previous expression statement + if output.len() > 0 && full { + let prev_idx = output.len() - 1; + let prev_stmt = output[prev_idx]; + if let StmtData::SExpr(prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() { + output[prev_idx] = p.s( + S::Throw { + value: Expr::join_with_comma( + prev_expr.value, + s_throw.value, + ), + }, + stmt.loc, + ); + continue; + } + } + } + + is_control_flow_dead = true; + } + + StmtData::SFor(mut s_for) if full => { + if let Some(&prev_stmt) = output.last() { + let prev_idx = output.len() - 1; + if let StmtData::SExpr(prev_expr) = prev_stmt.data { + if !prev_stmt.is_super_call() { + // Insert the previous expression into the for loop initializer + match s_for.init { + None => { + s_for.init = Some(p.s( + S::SExpr { + value: prev_expr.value, + ..Default::default() + }, + prev_stmt.loc, + )); + output[prev_idx] = stmt; + continue; + } + Some(init) => { + if let StmtData::SExpr(init_expr) = init.data { + s_for.init = Some(p.s( + S::SExpr { + value: Expr::join_with_comma( + prev_expr.value, + init_expr.value, + ), + ..Default::default() + }, + prev_stmt.loc, + )); + output[prev_idx] = stmt; + continue; + } + } + } + } + } else if let StmtData::SLocal(mut prev_local) = prev_stmt.data { + // Insert the previous variable declaration into the for loop + // initializer if it's a "var" declaration, since the scope + // doesn't matter due to scope hoisting + if prev_local.kind == LocalKind::KVar + && !prev_local.is_export + && prev_local.origin == LocalOrigin::Normal + { + match s_for.init { + None => { + s_for.init = Some(prev_stmt); + output[prev_idx] = stmt; + continue; + } + Some(init) => { + if let StmtData::SLocal(init_local) = init.data + && init_local.kind == LocalKind::KVar + && init_local.origin == LocalOrigin::Normal + { + append_decls( + &mut prev_local.decls, + init_local.decls.slice(), + ); + s_for.init = Some(prev_stmt); + output[prev_idx] = stmt; + continue; + } + } + } + } + } + } + } + + StmtData::STry(mut s_try) if full => { + // Drop an unused identifier binding: "try { x() } catch (y) {}" => "try { x() } catch {}" + if let Some(catch) = &mut s_try.catch + && let Some(binding) = catch.binding + && let BData::BIdentifier(id) = binding.data + { + let symbol = &p.symbols[id.r#ref.inner_index() as usize]; + // We cannot transform "try { x() } catch (y) { var y = 1 }" into + // "try { x() } catch { var y = 1 }" even though "y" is never used + // because the hoisted variable "y" would have different values + // after the statement ends due to a strange JavaScript quirk: + // + // try { x() } catch (y) { var y = 1 } + // console.log(y) // undefined + // + // try { x() } catch { var y = 1 } + // console.log(y) // 1 + // + // We also cannot transform "try { x() } catch (y) { eval('z = y') }" + // into "try { x() } catch { eval('z = y') }" because the variable + // "y" is actually still used. + // SAFETY: current_scope is a valid arena ptr for the parse. + if symbol.use_count_estimate == 0 + && symbol.link.get() == Ref::NONE + && !p.current_scope().contains_direct_eval + { + catch.binding = None; + } + } + } + + _ => {} + } + + output.push(stmt); + } + + if !full { + return output; + } + + // Drop a trailing unconditional jump statement if applicable + if let Some(&last) = output.last() { + let last_idx = output.len() - 1; + match kind { + StmtsKind::LoopBody => { + // "while (x) { y(); continue; }" => "while (x) { y(); }" + if let StmtData::SContinue(c) = last.data + && c.label.is_none() + { + output.truncate(last_idx); + } + } + StmtsKind::FnBody => { + if let StmtData::SReturn(ret) = last.data { + match ret.value { + // "function f() { x(); return; }" => "function f() { x(); }" + None => output.truncate(last_idx), + // "function f() { return void x(); }" => "function f() { x(); }" + Some(value) => { + if let ExprData::EUnary(unary) = value.data + && unary.op == OpCode::UnVoid + { + output[last_idx] = p.s( + S::SExpr { + value: unary.value, + ..Default::default() + }, + last.loc, + ); + } + } + } + } + } + _ => {} + } + } + + // Merge certain statements in reverse order + if output.len() >= 2 { + let last_stmt = output[output.len() - 1]; + if let StmtData::SReturn(last_return) = last_stmt.data { + // "if (a) return b; if (c) return d; return e;" => "return a ? b : c ? d : e;" + let mut last_loc = last_stmt.loc; + let mut last_value = last_return.value; + let mut nesting = 0; + while output.len() >= 2 { + let prev_index = output.len() - 2; + let prev_stmt = output[prev_index]; + match prev_stmt.data { + StmtData::SExpr(prev_expr) => { + // This return statement must have a value + let Some(value) = last_value else { break }; + if prev_stmt.is_super_call() { + break; + } + // "a(); return b;" => "return a(), b;" + last_value = Some(Expr::join_with_comma(prev_expr.value, value)); + } + StmtData::SIf(mut prev_if) => { + // The previous statement must be an if statement with no else clause + if prev_if.no.is_some() || nesting >= MAX_MANGLE_NESTING { + break; + } + nesting += 1; + // The then clause must be a return + let StmtData::SReturn(prev_return) = prev_if.yes.data else { + break; + }; + + // Handle some or all of the values being undefined + // "if (a) return; return b;" => "return a ? void 0 : b;" + let mut left = prev_return + .value + .unwrap_or_else(|| p.new_expr(E::Undefined {}, prev_if.yes.loc)); + // "if (a) return a; return;" => "return a ? b : void 0;" + let mut right = + last_value.unwrap_or_else(|| p.new_expr(E::Undefined {}, last_loc)); + + // "if (!a) return b; return c;" => "return a ? c : b;" + if let ExprData::EUnary(not) = prev_if.test.data + && not.op == OpCode::UnNot + { + prev_if.test = not.value; + core::mem::swap(&mut left, &mut right); + } + + last_value = Some(p.mangle_test_to_if_expr(prev_if.test, left, right)); + } + _ => break, + } + + // Merge the last two statements + last_loc = prev_stmt.loc; + output[prev_index] = p.s(S::Return { value: last_value }, last_loc); + let new_len = output.len() - 1; + output.truncate(new_len); + } + } else if let StmtData::SThrow(last_throw) = last_stmt.data { + // "if (a) throw b; if (c) throw d; throw e;" => "throw a ? b : c ? d : e;" + let mut last_value = last_throw.value; + let mut nesting = 0; + while output.len() >= 2 { + let prev_index = output.len() - 2; + let prev_stmt = output[prev_index]; + match prev_stmt.data { + StmtData::SExpr(prev_expr) => { + if prev_stmt.is_super_call() { + break; + } + // "a(); throw b;" => "throw a(), b;" + last_value = Expr::join_with_comma(prev_expr.value, last_value); + } + StmtData::SIf(mut prev_if) => { + // The previous statement must be an if statement with no else clause + if prev_if.no.is_some() || nesting >= MAX_MANGLE_NESTING { + break; + } + nesting += 1; + // The then clause must be a throw + let StmtData::SThrow(prev_throw) = prev_if.yes.data else { + break; + }; + + let mut left = prev_throw.value; + let mut right = last_value; + + // "if (!a) throw b; throw c;" => "throw a ? c : b;" + if let ExprData::EUnary(not) = prev_if.test.data + && not.op == OpCode::UnNot + { + prev_if.test = not.value; + core::mem::swap(&mut left, &mut right); + } + + last_value = p.mangle_test_to_if_expr(prev_if.test, left, right); + } + _ => break, + } + + // Merge the last two statements + output[prev_index] = p.s(S::Throw { value: last_value }, prev_stmt.loc); + let new_len = output.len() - 1; + output.truncate(new_len); + } + } + } + + output + } + + /// `test ? yes : no` for the return and throw chains above. + /// "if (a, b) return c; return d;" => "return a, b ? c : d;" + fn mangle_test_to_if_expr(&mut self, test: Expr, yes: Expr, no: Expr) -> Expr { + if let ExprData::EBinary(comma) = test.data + && comma.op == OpCode::BinComma + { + let e_if = self.new_expr( + E::If { + test: comma.right, + yes, + no, + }, + comma.right.loc, + ); + let ExprData::EIf(e_if_ref) = e_if.data else { + unreachable!() + }; + let mangled = self.mangle_if_expr(comma.right.loc, e_if_ref); + return Expr::join_with_comma(comma.left, mangled); + } + let e_if = self.new_expr(E::If { test, yes, no }, test.loc); + let ExprData::EIf(e_if_ref) = e_if.data else { + unreachable!() + }; + self.mangle_if_expr(test.loc, e_if_ref) + } + + /// Append the body of an `if` branch or a label without changing what the + /// block scopes. Port of esbuild's `appendIfOrLabelBodyPreservingScope`. + pub(crate) fn append_if_body_preserving_scope( + &mut self, + stmts: &mut ListManaged<'a, Stmt>, + body: Stmt, + ) { + if let StmtData::SBlock(block) = body.data { + let block_stmts: &[Stmt] = block.stmts.slice(); + if !stmts_care_about_scope(block_stmts) { + stmts.extend_from_slice(block_stmts); + return; + } + } + + if statement_cares_about_scope(&body) { + let block_stmts = self.arena.alloc_slice_copy(&[body]); + stmts.push(self.s( + S::Block { + stmts: block_stmts.into(), + close_brace_loc: Loc::EMPTY, + }, + body.loc, + )); + return; + } + + stmts.push(body); + } + + /// Simplify a visited `if` statement and append the result. Port of + /// esbuild's `mangleIf`. The caller checks `minify_syntax`. + pub(crate) fn mangle_if( + &mut self, + stmts: &mut ListManaged<'a, Stmt>, + loc: Loc, + mut s: StoreRef, + ) { + let p = self; + let full = p.full_minify_syntax(); + + // Constant folding using the test expression + if let Some(known) = SideEffects::to_boolean(p, &s.test.data) { + if known.value { + // The test is truthy + if s.no.is_none_or(|no| { + !SideEffects::should_keep_stmt_in_dead_control_flow(no, p.arena) + }) { + // We can drop the "no" branch + if known.side_effects == SideEffects::CouldHaveSideEffects { + // Keep the condition if it could have side effects (but is still known to be truthy) + if let Some(test) = SideEffects::simplify_unused_expr(p, s.test) { + stmts.push(p.s( + S::SExpr { + value: test, + ..Default::default() + }, + test.loc, + )); + } + } + p.append_if_body_preserving_scope(stmts, s.yes); + return; + } + // We have to keep the "no" branch + } else { + // The test is falsy + if !SideEffects::should_keep_stmt_in_dead_control_flow(s.yes, p.arena) { + // We can drop the "yes" branch + if known.side_effects == SideEffects::CouldHaveSideEffects { + // Keep the condition if it could have side effects (but is still known to be falsy) + if let Some(test) = SideEffects::simplify_unused_expr(p, s.test) { + stmts.push(p.s( + S::SExpr { + value: test, + ..Default::default() + }, + test.loc, + )); + } + } + if let Some(no) = s.no { + p.append_if_body_preserving_scope(stmts, no); + } + return; + } + // We have to keep the "yes" branch + } + + // Use "1" and "0" instead of "true" and "false" to be shorter + if full && known.side_effects == SideEffects::NoSideEffects { + s.test = p.new_expr( + E::Number::new(if known.value { 1.0 } else { 0.0 }), + s.test.loc, + ); + } + } + + // `s_expr` drops an expression statement without side effects, so an + // emptied branch is an `SEmpty`. An `SExpr` with a missing value is the + // same thing to the passes below. + let yes_is_empty = matches!(s.yes.data, StmtData::SEmpty(_)) || s.yes.is_missing_expr(); + + if !full { + // "if (a) {}" => "" and "if (a) {} else {}" => "" when the test has no side effects + if yes_is_empty + && s.no.is_none_or(|no| no.is_missing_expr()) + && p.expr_can_be_removed_if_unused(&s.test) + { + return; + } + stmts.push(Stmt { + loc, + data: StmtData::SIf(s), + }); + return; + } + + let mut expr: Option = None; + + if let StmtData::SExpr(yes) = s.yes.data + && !yes_is_empty + { + // "yes" is an expression + match s.no { + None => { + if let ExprData::EUnary(not) = s.test.data + && not.op == OpCode::UnNot + { + // "if (!a) b();" => "a || b();" + expr = Some(Expr::join_with_left_associative_op( + OpCode::BinLogicalOr, + not.value, + yes.value, + )); + } else { + // "if (a) b();" => "a && b();" + expr = Some(Expr::join_with_left_associative_op( + OpCode::BinLogicalAnd, + s.test, + yes.value, + )); + } + } + Some(no) => { + if let StmtData::SExpr(no_expr) = no.data { + // "if (a) b(); else c();" => "a ? b() : c();" + let e_if = p.new_expr( + E::If { + test: s.test, + yes: yes.value, + no: no_expr.value, + }, + loc, + ); + let ExprData::EIf(e_if_ref) = e_if.data else { + unreachable!() + }; + expr = Some(p.mangle_if_expr(loc, e_if_ref)); + } + } + } + } else if yes_is_empty { + // "yes" is missing + match s.no { + None => { + // "yes" and "no" are both missing + if p.expr_can_be_removed_if_unused(&s.test) { + // "if (1) {}" => "" + return; + } + // "if (a) {}" => "a;" + expr = Some(s.test); + } + Some(no) => { + if let StmtData::SExpr(no_expr) = no.data { + if let ExprData::EUnary(not) = s.test.data + && not.op == OpCode::UnNot + { + // "if (!a) {} else b();" => "a && b();" + expr = Some(Expr::join_with_left_associative_op( + OpCode::BinLogicalAnd, + not.value, + no_expr.value, + )); + } else { + // "if (a) {} else b();" => "a || b();" + expr = Some(Expr::join_with_left_associative_op( + OpCode::BinLogicalOr, + s.test, + no_expr.value, + )); + } + } else { + // "yes" is missing and "no" is not missing (and is not an expression) + if let ExprData::EUnary(not) = s.test.data + && not.op == OpCode::UnNot + { + // "if (!a) {} else throw b;" => "if (a) throw b;" + s.test = not.value; + } else { + // "if (a) {} else throw b;" => "if (!a) throw b;" + s.test = s.test.not(p.arena); + } + s.yes = no; + s.no = None; + } + } + } + } else { + // "yes" is not missing (and is not an expression) + match s.no { + Some(no) => { + // "yes" is not missing (and is not an expression) and "no" is not missing + if let ExprData::EUnary(not) = s.test.data + && not.op == OpCode::UnNot + { + // "if (!a) return b; else return c;" => "if (a) return c; else return b;" + s.test = not.value; + s.no = Some(s.yes); + s.yes = no; + } + } + None => { + // "no" is missing + if let StmtData::SIf(s2) = s.yes.data + && s2.no.is_none() + { + // "if (a) if (b) return c;" => "if (a && b) return c;" + s.test = Expr::join_with_left_associative_op( + OpCode::BinLogicalAnd, + s.test, + s2.test, + ); + s.yes = s2.yes; + } + } + } + } + + // Return an expression if we replaced the if statement with an expression above + if let Some(expr) = expr { + if let Some(expr) = SideEffects::simplify_unused_expr(p, expr) { + stmts.push(p.s( + S::SExpr { + value: expr, + ..Default::default() + }, + loc, + )); + } + return; + } + + stmts.push(Stmt { + loc, + data: StmtData::SIf(s), + }); + } + + /// Rewrite a conditional expression into a shorter equivalent. Port of + /// esbuild's `MangleIfExpr`. `e` is updated in place when the result is + /// still a conditional. + pub(crate) fn mangle_if_expr(&mut self, loc: Loc, mut e: StoreRef) -> Expr { + if !self.stack_check.is_safe_to_recurse() { + return Expr { + loc, + data: ExprData::EIf(e), + }; + } + + let mut test = e.test; + let mut yes = e.yes; + let mut no = e.no; + + // "(a, b) ? c : d" => "a, b ? c : d" + if let ExprData::EBinary(comma) = test.data + && comma.op == OpCode::BinComma + { + e.test = comma.right; + let inner = self.mangle_if_expr(comma.right.loc, e); + return Expr::join_with_comma(comma.left, inner); + } + + // "!a ? b : c" => "a ? c : b" + if let ExprData::EUnary(not) = test.data + && not.op == OpCode::UnNot + { + test = not.value; + core::mem::swap(&mut yes, &mut no); + } + + if self.values_look_the_same(&yes.data, &no.data) { + // "/* @__PURE__ */ a() ? b : b" => "b" + if self.expr_can_be_removed_if_unused(&test) { + return yes; + } + + // "a ? b : b" => "a, b" + return Expr::join_with_comma(test, yes); + } + + // "a ? true : false" => "!!a" + // "a ? false : true" => "!a" + if let ( + ExprData::EBoolean(y) | ExprData::EBranchBoolean(y), + ExprData::EBoolean(n) | ExprData::EBranchBoolean(n), + ) = (yes.data, no.data) + { + if y.value && !n.value { + return test.not(self.arena).not(self.arena); + } + if !y.value && n.value { + return test.not(self.arena); + } + } + + if let ExprData::EIdentifier(id) = test.data { + // "a ? a : b" => "a || b" + if let ExprData::EIdentifier(id2) = yes.data + && id.ref_ == id2.ref_ + { + return Expr::join_with_left_associative_op(OpCode::BinLogicalOr, test, no); + } + + // "a ? b : a" => "a && b" + if let ExprData::EIdentifier(id2) = no.data + && id.ref_ == id2.ref_ + { + return Expr::join_with_left_associative_op(OpCode::BinLogicalAnd, test, yes); + } + } + + // "a ? b ? c : d : d" => "a && b ? c : d" + if let ExprData::EIf(yes_if) = yes.data + && self.values_look_the_same(&yes_if.no.data, &no.data) + { + e.test = Expr::join_with_left_associative_op(OpCode::BinLogicalAnd, test, yes_if.test); + e.yes = yes_if.yes; + e.no = no; + return Expr { + loc, + data: ExprData::EIf(e), + }; + } + + // "a ? b : c ? b : d" => "a || c ? b : d" + if let ExprData::EIf(no_if) = no.data + && self.values_look_the_same(&yes.data, &no_if.yes.data) + { + e.test = Expr::join_with_left_associative_op(OpCode::BinLogicalOr, test, no_if.test); + e.yes = yes; + e.no = no_if.no; + return Expr { + loc, + data: ExprData::EIf(e), + }; + } + + // "a ? c : (b, c)" => "(a || b), c" + if let ExprData::EBinary(comma) = no.data + && comma.op == OpCode::BinComma + && self.values_look_the_same(&yes.data, &comma.right.data) + { + return Expr::join_with_comma( + Expr::join_with_left_associative_op(OpCode::BinLogicalOr, test, comma.left), + comma.right, + ); + } + + // "a ? (b, c) : c" => "(a && b), c" + if let ExprData::EBinary(comma) = yes.data + && comma.op == OpCode::BinComma + && self.values_look_the_same(&comma.right.data, &no.data) + { + return Expr::join_with_comma( + Expr::join_with_left_associative_op(OpCode::BinLogicalAnd, test, comma.left), + comma.right, + ); + } + + // "a ? b || c : c" => "(a && b) || c" + if let ExprData::EBinary(binary) = yes.data + && binary.op == OpCode::BinLogicalOr + && self.values_look_the_same(&binary.right.data, &no.data) + { + return Expr::init( + E::Binary { + op: OpCode::BinLogicalOr, + left: Expr::join_with_left_associative_op( + OpCode::BinLogicalAnd, + test, + binary.left, + ), + right: binary.right, + }, + loc, + ); + } + + // "a ? c : b && c" => "(a || b) && c" + if let ExprData::EBinary(binary) = no.data + && binary.op == OpCode::BinLogicalAnd + && self.values_look_the_same(&yes.data, &binary.right.data) + { + return Expr::init( + E::Binary { + op: OpCode::BinLogicalAnd, + left: Expr::join_with_left_associative_op( + OpCode::BinLogicalOr, + test, + binary.left, + ), + right: binary.right, + }, + loc, + ); + } + + // "a ? b(c, d) : b(e, d)" => "b(a ? c : e, d)" + if let ExprData::ECall(mut y) = yes.data + && !y.args.is_empty() + && let ExprData::ECall(n) = no.data + && n.args.len() == y.args.len() + && y.optional_chain == n.optional_chain + && y.is_direct_eval == n.is_direct_eval + && y.can_be_unwrapped_if_unused == n.can_be_unwrapped_if_unused + && self.values_look_the_same(&y.target.data, &n.target.data) + // Only do this if the condition can be reordered past the call target + // without side effects. For example, if the test or the call target is + // an unbound identifier, reordering could potentially mean evaluating + // the code could throw a different ReferenceError. + && self.expr_can_be_removed_if_unused(&test) + && self.expr_can_be_removed_if_unused(&y.target) + { + let same_tail_args = (1..y.args.len()) + .all(|i| self.values_look_the_same(&y.args[i].data, &n.args[i].data)); + if same_tail_args { + let y0 = y.args[0]; + let n0 = n.args[0]; + match (y0.data, n0.data) { + // "a ? b(...c) : b(...e)" => "b(...a ? c : e)" + (ExprData::ESpread(yes_spread), ExprData::ESpread(no_spread)) => { + e.test = test; + e.yes = yes_spread.value; + e.no = no_spread.value; + let inner = self.mangle_if_expr(loc, e); + y.args[0] = self.new_expr(E::Spread { value: inner }, loc); + return Expr { + loc, + data: ExprData::ECall(y), + }; + } + (ExprData::ESpread(_), _) | (_, ExprData::ESpread(_)) => {} + // "a ? b(c) : b(e)" => "b(a ? c : e)" + _ => { + e.test = test; + e.yes = y0; + e.no = n0; + y.args[0] = self.mangle_if_expr(loc, e); + return Expr { + loc, + data: ExprData::ECall(y), + }; + } + } + } + } + + // Try using the "??" or "?." operators + if let ExprData::EBinary(binary) = test.data { + let (check, when_null, when_non_null) = match binary.op { + OpCode::BinLooseEq => { + if matches!(binary.right.data, ExprData::ENull(_)) { + // "a == null ? _ : _" + (Some(binary.left), yes, no) + } else if matches!(binary.left.data, ExprData::ENull(_)) { + // "null == a ? _ : _" + (Some(binary.right), yes, no) + } else { + (None, yes, no) + } + } + OpCode::BinLooseNe => { + if matches!(binary.right.data, ExprData::ENull(_)) { + // "a != null ? _ : _" + (Some(binary.left), no, yes) + } else if matches!(binary.left.data, ExprData::ENull(_)) { + // "null != a ? _ : _" + (Some(binary.right), no, yes) + } else { + (None, yes, no) + } + } + _ => (None, yes, no), + }; + + if let Some(check) = check + && self.expr_can_be_removed_if_unused(&check) + { + // "a != null ? a : b" => "a ?? b" + if self.values_look_the_same(&check.data, &when_non_null.data) { + return Expr::join_with_left_associative_op( + OpCode::BinNullishCoalescing, + check, + when_null, + ); + } + + // "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)" + if matches!(when_null.data, ExprData::EUndefined(_)) + && self.try_to_insert_optional_chain(check, when_non_null) + { + return when_non_null; + } + } + } + + e.test = test; + e.yes = yes; + e.no = no; + Expr { + loc, + data: ExprData::EIf(e), + } + } + + /// Make `expr` an optional chain rooted at `test`: "a.b.c" => "a?.b.c" + /// when `test` is "a". Port of esbuild's `TryToInsertOptionalChain`. + pub(crate) fn try_to_insert_optional_chain(&mut self, test: Expr, expr: Expr) -> bool { + if !self.stack_check.is_safe_to_recurse() { + return false; + } + match expr.data { + ExprData::EDot(mut e) => { + if self.values_look_the_same(&test.data, &e.target.data) { + e.optional_chain = Some(OptionalChain::Start); + return true; + } + if self.try_to_insert_optional_chain(test, e.target) { + if e.optional_chain.is_none() { + e.optional_chain = Some(OptionalChain::Continuation); + } + return true; + } + } + ExprData::EIndex(mut e) => { + if self.values_look_the_same(&test.data, &e.target.data) { + e.optional_chain = Some(OptionalChain::Start); + return true; + } + if self.try_to_insert_optional_chain(test, e.target) { + if e.optional_chain.is_none() { + e.optional_chain = Some(OptionalChain::Continuation); + } + return true; + } + } + ExprData::ECall(mut e) => { + if self.values_look_the_same(&test.data, &e.target.data) { + e.optional_chain = Some(OptionalChain::Start); + return true; + } + if self.try_to_insert_optional_chain(test, e.target) { + if e.optional_chain.is_none() { + e.optional_chain = Some(OptionalChain::Continuation); + } + return true; + } + } + _ => {} + } + false + } + + /// True when two expressions are written the same way and evaluating + /// either one gives the same value. Port of esbuild's `ValuesLookTheSame`. + pub(crate) fn values_look_the_same(&mut self, left: &ExprData, right: &ExprData) -> bool { + if !self.stack_check.is_safe_to_recurse() { + return false; + } + + if let ExprData::EInlinedEnum(b) = right { + return self.values_look_the_same(left, &b.value.data); + } + + match left { + ExprData::EInlinedEnum(a) => return self.values_look_the_same(&a.value.data, right), + + ExprData::EIdentifier(a) => { + if let ExprData::EIdentifier(b) = right + && a.ref_ == b.ref_ + { + return true; + } + } + + ExprData::EDot(a) => { + if let ExprData::EDot(b) = right + && a.optional_chain == b.optional_chain + && a.can_be_removed_if_unused == b.can_be_removed_if_unused + && a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused + && a.name.slice() == b.name.slice() + && self.values_look_the_same(&a.target.data, &b.target.data) + { + return true; + } + } + + ExprData::EIndex(a) => { + if let ExprData::EIndex(b) = right + && a.optional_chain == b.optional_chain + && self.values_look_the_same(&a.target.data, &b.target.data) + && self.values_look_the_same(&a.index.data, &b.index.data) + { + return true; + } + } + + ExprData::EIf(a) => { + if let ExprData::EIf(b) = right + && self.values_look_the_same(&a.test.data, &b.test.data) + && self.values_look_the_same(&a.yes.data, &b.yes.data) + && self.values_look_the_same(&a.no.data, &b.no.data) + { + return true; + } + } + + ExprData::EUnary(a) => { + if let ExprData::EUnary(b) = right + && a.op == b.op + && self.values_look_the_same(&a.value.data, &b.value.data) + { + return true; + } + } + + ExprData::EBinary(a) => { + if let ExprData::EBinary(b) = right + && a.op == b.op + && self.values_look_the_same(&a.left.data, &b.left.data) + && self.values_look_the_same(&a.right.data, &b.right.data) + { + return true; + } + } + + ExprData::ECall(a) => { + if let ExprData::ECall(b) = right + && a.optional_chain == b.optional_chain + && a.is_direct_eval == b.is_direct_eval + && a.can_be_unwrapped_if_unused == b.can_be_unwrapped_if_unused + && a.args.len() == b.args.len() + && self.values_look_the_same(&a.target.data, &b.target.data) + { + return (0..a.args.len()) + .all(|i| self.values_look_the_same(&a.args[i].data, &b.args[i].data)); + } + } + + // Special-case to distinguish between negative an non-negative zero when mangling + // "a ? -0 : 0" => "a ? -0 : 0" + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness + ExprData::ENumber(a) => { + if let ExprData::ENumber(b) = right + && a.value() == 0.0 + && b.value() == 0.0 + && a.value().is_sign_negative() != b.value().is_sign_negative() + { + return false; + } + } + + _ => {} + } + + matches!( + ExprData::eql::<_, StrictEql>(left, right, self), + Equality::Equal + ) + } + + /// True when two jump statements do the same thing. Port of esbuild's + /// `jumpStmtsLookTheSame`. + fn jump_stmts_look_the_same(&mut self, left: StmtData, right: StmtData) -> bool { + match (left, right) { + (StmtData::SBreak(a), StmtData::SBreak(b)) => match (a.label, b.label) { + (None, None) => true, + (Some(a), Some(b)) => a.ref_ == b.ref_, + _ => false, + }, + (StmtData::SContinue(a), StmtData::SContinue(b)) => match (a.label, b.label) { + (None, None) => true, + (Some(a), Some(b)) => a.ref_ == b.ref_, + _ => false, + }, + (StmtData::SReturn(a), StmtData::SReturn(b)) => match (a.value, b.value) { + (None, None) => true, + (Some(a), Some(b)) => self.values_look_the_same(&a.data, &b.data), + _ => false, + }, + (StmtData::SThrow(a), StmtData::SThrow(b)) => { + self.values_look_the_same(&a.value.data, &b.value.data) + } + _ => false, + } + } +} + +/// `Vec::extend_from_slice` needs `T: Clone`, and `G::Decl` is field-wise `Copy` +/// without the derive, so each declaration is copied bitwise. +fn append_decls(dst: &mut G::DeclList, src: &[G::Decl]) { + for d in src { + // SAFETY: Decl is field-wise Copy (Binding, Option). + dst.push(unsafe { core::ptr::read(d) }); + } +} + +pub(crate) fn is_jump_statement(data: StmtData) -> bool { + matches!( + data, + StmtData::SBreak(_) | StmtData::SContinue(_) | StmtData::SReturn(_) | StmtData::SThrow(_) + ) +} + +pub(crate) fn stmts_care_about_scope(stmts: &[Stmt]) -> bool { + stmts.iter().any(statement_cares_about_scope) +} + +/// Fold a leading `if (x) break;` of a loop body into the loop test. Port of +/// esbuild's `mangleFor`. `while` loops are converted to `for` first so that +/// both shapes get this. +pub(crate) fn mangle_for(s: &mut S::For, bump: &Bump) { + // Get the first statement in the loop + let mut first = s.body; + if let StmtData::SBlock(block) = first.data + && !block.stmts.is_empty() + { + first = block.stmts[0]; + } + + let StmtData::SIf(if_s) = first.data else { + return; + }; + + // "for (;;) if (x) break;" => "for (; !x;) ;" + // "for (; a;) if (x) break;" => "for (; a && !x;) ;" + // "for (;;) if (x) break; else y();" => "for (; !x;) y();" + // "for (; a;) if (x) break; else y();" => "for (; a && !x;) y();" + if let StmtData::SBreak(break_s) = if_s.yes.data + && break_s.label.is_none() + { + let not = match if_s.test.data { + ExprData::EUnary(unary) if unary.op == OpCode::UnNot => unary.value, + _ => if_s.test.not(bump), + }; + s.test = Some(match s.test { + Some(test) => Expr::join_with_left_associative_op(OpCode::BinLogicalAnd, test, not), + None => not, + }); + s.body = drop_first_statement(s.body, if_s.no); + return; + } + + // "for (;;) if (x) y(); else break;" => "for (; x;) y();" + // "for (; a;) if (x) y(); else break;" => "for (; a && x;) y();" + if let Some(no) = if_s.no + && let StmtData::SBreak(break_s) = no.data + && break_s.label.is_none() + { + s.test = Some(match s.test { + Some(test) => { + Expr::join_with_left_associative_op(OpCode::BinLogicalAnd, test, if_s.test) + } + None => if_s.test, + }); + s.body = drop_first_statement(s.body, Some(if_s.yes)); + } +} + +fn drop_first_statement(body: Stmt, replace: Option) -> Stmt { + if let StmtData::SBlock(mut block) = body.data + && !block.stmts.is_empty() + { + if let Some(replace) = replace { + block.stmts.slice_mut()[0] = replace; + } else if block.stmts.len() == 2 && !statement_cares_about_scope(&block.stmts[1]) { + return block.stmts[1]; + } else { + block.stmts = StoreSlice::new(&block.stmts.slice()[1..]); + } + return body; + } + if let Some(replace) = replace { + return replace; + } + body.to_empty() +} diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index f5bdf9467cc4..e3047ce39050 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -2,6 +2,7 @@ //! AST visitor pass: visits statements, expressions, bindings, function bodies, //! classes, and declarations. This is the second pass after parsing. +pub(crate) mod mangle; pub mod visit_binary; pub(crate) mod visit_expr; pub(crate) mod visit_stmt; @@ -15,8 +16,7 @@ use crate::parser::{ }; use bun_alloc::{ArenaVec as BumpVec, ArenaVecExt as _}; use bun_ast as js_ast; -use bun_ast::G::{Decl, PropertyKind}; -use bun_ast::OpCode; +use bun_ast::G::PropertyKind; use bun_ast::b::B as BData; use bun_ast::flags; use bun_ast::s::Kind as LocalKind; @@ -24,7 +24,7 @@ use bun_ast::scope::{Kind as ScopeKind, Member as ScopeMember}; use bun_ast::symbol::Kind as SymbolKind; use bun_ast::{ AssignTarget, B, Binding, BindingNodeIndex, E, Expr, ExprData, ExprNodeList, G, LocRef, S, - Stmt, StmtData, Symbol, + Stmt, StmtData, }; use bun_collections::VecExt; // `parser::SideEffects` is a stub enum without the assoc fns; the real @@ -1449,6 +1449,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } else { None }; + let old_in_react_compiler_candidate = p.in_react_compiler_candidate; + if rc_pending.is_some() { + p.in_react_compiler_candidate = true; + } #[cfg(debug_assertions)] let initial_scope: js_ast::StoreRef = p.current_scope; @@ -1742,379 +1746,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.react_compiler_result = Some(result); } } + p.in_react_compiler_candidate = old_in_react_compiler_candidate; - if !p.options.features.minify_syntax || !p.options.features.dead_code_elimination { + // A case body is one slice of the switch block scope, so a `let` or + // `const` declared here can still be used by a later case that has not + // been visited yet. `s_switch` mangles every case body once all of them + // have been visited and the use counts are final. + if kind == StmtsKind::SwitchStmt { return Ok(()); } - // SAFETY: current_scope is a valid arena ptr for the parse. - if p.current_scope().parent.is_some() && !p.current_scope().contains_direct_eval { - // Remove inlined constants now that we know whether any of these statements - // contained a direct eval() or not. This can't be done earlier when we - // encounter the constant because we haven't encountered the eval() yet. - // Inlined constants are not removed if they are in a top-level scope or - // if they are exported (which could be in a nested TypeScript namespace). - if p.const_values.count() > 0 { - let items: &mut [Stmt] = stmts.as_mut_slice(); - for stmt in items.iter_mut() { - match stmt.data { - StmtData::SEmpty(_) - | StmtData::SComment(_) - | StmtData::SDirective(_) - | StmtData::SDebugger(_) - | StmtData::STypeScript(_) => continue, - StmtData::SLocal(mut local) => { - // "using" / "await using" declarations have disposal - // side-effects on scope exit. Their refs can end up in - // `const_values` via the macro path in `visitDecl` - // (`could_be_macro`), so skip them here to avoid - // silently dropping the declaration. - if local.kind.is_using() { - continue; - } - if !local.is_export && !local.origin.is_commonjs_export() { - let mut any_decl_in_const_values = local.kind == LocalKind::KConst; - let decls: &mut [Decl] = local.decls.slice_mut(); - let mut end: usize = 0; - for idx in 0..decls.len() { - if let BData::BIdentifier(id_ptr) = decls[idx].binding.data { - let id_ref = id_ptr.r#ref; - if p.const_values.contains(&id_ref) { - any_decl_in_const_values = true; - let symbol = &p.symbols[id_ref.inner_index() as usize]; - if symbol.use_count_estimate == 0 { - // Skip declarations that are constants with zero usage - continue; - } - } - } - // `Decl` is field-wise `Copy` but lacks the - // derive; `swap` compacts in place (idx >= end always). - decls.swap(end, idx); - end += 1; - } - local.decls.truncate(end); - if any_decl_in_const_values { - if end == 0 { - *stmt = stmt.to_empty(); - } - continue; - } - } - } - _ => {} - } - - // Break after processing relevant statements - break; - } - } - } - - let mut is_control_flow_dead = false; - - let mut output: ListManaged<'a, Stmt> = ListManaged::with_capacity_in(stmts.len(), p.arena); - - let dead_code_elimination = p.options.features.dead_code_elimination; - for stmt in stmts.iter().copied() { - if is_control_flow_dead - && dead_code_elimination - && !SideEffects::should_keep_stmt_in_dead_control_flow(stmt, p.arena) - { - // Strip unnecessary statements if the control flow is dead here - continue; - } - - // Inline single-use variable declarations where possible: - // - // // Before - // let x = fn(); - // return x.y(); - // - // // After - // return fn().y(); - // - // The declaration must not be exported. We can't just check for the - // "export" keyword because something might do "export {id};" later on. - // Instead we just ignore all top-level declarations for now. That means - // this optimization currently only applies in nested scopes. - // - // Ignore declarations if the scope is shadowed by a direct "eval" call. - // The eval'd code may indirectly reference this symbol and the actual - // use count may be greater than 1. - // SAFETY: current_scope is a valid arena ptr for the parse. - if p.current_scope != p.module_scope && !p.current_scope().contains_direct_eval { - // Keep inlining variables until a failure or until there are none left. - // That handles cases like this: - // - // // Before - // let x = fn(); - // let y = x.prop; - // return y; - // - // // After - // return fn().prop; - // - 'inner: while output.len() > 0 { - // Ignore "var" declarations since those have function-level scope and - // we may not have visited all of their uses yet by this point. We - // should have visited all the uses of "let" and "const" declarations - // by now since they are scoped to this block which we just finished - // visiting. - let prev_idx = output.len() - 1; - // borrowck: read the `StoreRef` (Copy) first, then re-borrow - // `output` only when truncating. - let StmtData::SLocal(mut local) = output[prev_idx].data else { - break; - }; - // "using" / "await using" declarations have disposal - // side-effects on scope exit, so they must not be - // removed by inlining their initializer into the use. - if local.decls.len_u32() == 0 - || local.kind == LocalKind::KVar - || local.kind.is_using() - || local.is_export - { - break; - } - - // The variable must be initialized, since we will be substituting - // the value into the usage. - let last_idx = (local.decls.len_u32() - 1) as usize; - let last: &mut Decl = &mut local.decls.slice_mut()[last_idx]; - let Some(replacement) = last.value else { break }; - - // The binding must be an identifier that is only used once. - // Ignore destructuring bindings since that's not the simple case. - // Destructuring bindings could potentially execute side-effecting - // code which would invalidate reordering. - let BData::BIdentifier(ident_ptr) = last.binding.data else { - break; - }; - let id = ident_ptr.r#ref; - - let symbol: &Symbol = &p.symbols[id.inner_index() as usize]; - - // Try to substitute the identifier with the initializer. This will - // fail if something with side effects is in between the declaration - // and the usage. - if symbol.use_count_estimate == 1 - && p.substitute_single_use_symbol_in_stmt(stmt, id, replacement) - { - // `const ns = await import(x); return ns` — the single use just - // moved into `replacement`; unless it was an accounted-for read - // (`f(ns.a)`), the namespace escapes there. - if p.dynamic_import_namespace_locals.contains_key(&id) - && p.namespace_tracked_uses.get(&id).copied().unwrap_or(0) == 0 - { - // Read as "more uses than accounted for" when finalizing. - p.namespace_tracked_uses.insert(id, u32::MAX); - } - match local.decls.len_u32() { - 1 => { - local.decls.clear(); - let new_len = output.len() - 1; - output.truncate(new_len); - continue 'inner; - } - _ => { - let n = local.decls.len() - 1; - local.decls.truncate(n); - continue 'inner; - } - } - } - break; - } - } - - // don't merge super calls to ensure they are called before "this" is accessed - if stmt.is_super_call() { - output.push(stmt); - continue; - } - - // The following calls to `joinWithComma` are only enabled during bundling. We do this - // to avoid changing line numbers too much for source maps - - match stmt.data { - StmtData::SEmpty(_) => continue, - - // skip directives for now - StmtData::SDirective(_) => continue, - - StmtData::SLocal(local) => { - // Merge adjacent local statements - if output.len() > 0 { - let prev_idx = output.len() - 1; - let prev_stmt = &mut output[prev_idx]; - if let StmtData::SLocal(mut prev_local) = prev_stmt.data { - if local.can_merge_with(&prev_local) { - // `Vec::append_slice` requires `T: Clone` - // but `G::Decl` lacks the derive (its fields are all - // `Copy`). Per-element bitwise copy instead. - // - // The parse pass allocates `decls` in the bump arena - // (`from_bump_slice` → `Origin::Borrowed`); promote to a - // global-heap buffer before growing it. - for d in local.decls.slice() { - // SAFETY: Decl is field-wise Copy (Binding, Option). - prev_local.decls.push(unsafe { core::ptr::read(d) }); - } - continue; - } - } - } - } - - StmtData::SExpr(s_expr) => { - // Merge adjacent expression statements - if output.len() > 0 { - let prev_idx = output.len() - 1; - let prev_stmt = &mut output[prev_idx]; - if let StmtData::SExpr(mut prev_expr) = prev_stmt.data { - if !prev_stmt.is_super_call() - && p.options.runtime_merge_adjacent_expression_statements() - { - prev_expr.does_not_affect_tree_shaking = prev_expr - .does_not_affect_tree_shaking - && s_expr.does_not_affect_tree_shaking; - prev_expr.value = - Expr::join_with_comma(prev_expr.value, s_expr.value); - continue; - } - } else if let StmtData::SLocal(prev_local) = prev_stmt.data { - // - // Input: - // var f; - // f = 123; - // Output: - // var f = 123; - // - // This doesn't handle every case. Only the very simple one. - if let ExprData::EBinary(bin_assign) = s_expr.value.data { - if prev_local.decls.len_u32() == 1 - && bin_assign.op == OpCode::BinAssign - // we can only do this with var because var is hoisted - // the statement we are merging into may use the statement before its defined. - && prev_local.kind == LocalKind::KVar - { - if let ExprData::EIdentifier(left_id) = bin_assign.left.data { - // `prev_local` is a `StoreRef` (Copy) so - // re-slicing here writes through to the arena slot. - let mut prev_local = prev_local; - let decl = &mut prev_local.decls.slice_mut()[0]; - if let BData::BIdentifier(bid_ptr) = decl.binding.data { - let bid_ref = bid_ptr.r#ref; - if bid_ref.eql(left_id.ref_) - // If the value was assigned, we shouldn't merge it incase it was used in the current statement - // https://github.com/oven-sh/bun/issues/2948 - // We don't have a more granular way to check symbol usage so this is the best we can do - && decl.value.is_none() - { - decl.value = Some(bin_assign.right); - p.ignore_usage(left_id.ref_); - continue; - } - } - } - } - } - } - } - } - StmtData::SSwitch(mut s_switch) => { - // Absorb a previous expression statement - if output.len() > 0 && p.options.runtime_merge_adjacent_expression_statements() - { - let prev_idx = output.len() - 1; - let prev_stmt = output[prev_idx]; - if let StmtData::SExpr(prev_expr) = prev_stmt.data { - if !prev_stmt.is_super_call() { - s_switch.test = - Expr::join_with_comma(prev_expr.value, s_switch.test); - output.truncate(prev_idx); - } - } - } - } - StmtData::SIf(mut s_if) => { - // Absorb a previous expression statement - if output.len() > 0 && p.options.runtime_merge_adjacent_expression_statements() - { - let prev_idx = output.len() - 1; - let prev_stmt = output[prev_idx]; - if let StmtData::SExpr(prev_expr) = prev_stmt.data { - if !prev_stmt.is_super_call() { - s_if.test = Expr::join_with_comma(prev_expr.value, s_if.test); - output.truncate(prev_idx); - } - } - } - - // TODO: optimize jump - } - - StmtData::SReturn(mut ret) => { - // Merge return statements with the previous expression statement - if output.len() > 0 - && ret.value.is_some() - && p.options.runtime_merge_adjacent_expression_statements() - { - let prev_idx = output.len() - 1; - let prev_stmt = output[prev_idx]; - if let StmtData::SExpr(prev_expr) = prev_stmt.data { - if !prev_stmt.is_super_call() { - ret.value = Some(Expr::join_with_comma( - prev_expr.value, - ret.value.unwrap(), - )); - output[prev_idx] = stmt; - continue; - } - } - } - - is_control_flow_dead = true; - } - - StmtData::SBreak(_) | StmtData::SContinue(_) => { - is_control_flow_dead = true; - } - - StmtData::SThrow(s_throw) => { - // Merge throw statements with the previous expression statement - if output.len() > 0 && p.options.runtime_merge_adjacent_expression_statements() - { - let prev_idx = output.len() - 1; - let prev_stmt = output[prev_idx]; - if let StmtData::SExpr(prev_expr) = prev_stmt.data { - if !prev_stmt.is_super_call() { - output[prev_idx] = p.s( - S::Throw { - value: Expr::join_with_comma( - prev_expr.value, - s_throw.value, - ), - }, - stmt.loc, - ); - continue; - } - } - } - - is_control_flow_dead = true; - } - - _ => {} - } - - output.push(stmt); + if !p.options.features.minify_syntax || !p.options.features.dead_code_elimination { + return Ok(()); } - // stmts.deinit(); — Drop handles freeing the old buffer (BumpVec is arena-backed). - *stmts = output; + let input = core::mem::replace(stmts, ListManaged::new_in(p.arena)); + *stmts = p.mangle_stmts(input, kind); Ok(()) } } diff --git a/src/js_parser/visit/visit_binary.rs b/src/js_parser/visit/visit_binary.rs index 5c1ebed71e9d..2a2999fa460f 100644 --- a/src/js_parser/visit/visit_binary.rs +++ b/src/js_parser/visit/visit_binary.rs @@ -72,6 +72,50 @@ fn try_optimize_typeof_undefined<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bo )) } +/// "a === null || a === void 0" (or "a !== null && a !== void 0" with +/// `BinStrictNe`): the two operands of the `== null` that replaces it. +fn is_binary_null_and_undefined(left: Expr, right: Expr, op: Op::Code) -> Option<(Expr, Expr)> { + let ExprData::EBinary(a) = left.data else { + return None; + }; + let ExprData::EBinary(b) = right.data else { + return None; + }; + if a.op != op || b.op != op { + return None; + } + + let (mut id_a, mut eq_a) = (a.left, a.right); + let (mut id_b, mut eq_b) = (b.left, b.right); + + // Detect when the identifier comes second and flip the order of our checks + if matches!(eq_a.data, ExprData::EIdentifier(_)) { + core::mem::swap(&mut id_a, &mut eq_a); + } + if matches!(eq_b.data, ExprData::EIdentifier(_)) { + core::mem::swap(&mut id_b, &mut eq_b); + } + + let (ExprData::EIdentifier(id_a), ExprData::EIdentifier(id_b)) = (id_a.data, id_b.data) else { + return None; + }; + if id_a.ref_ != id_b.ref_ { + return None; + } + + // "a === null || a === void 0" + if matches!(eq_a.data, ExprData::ENull(_)) && matches!(eq_b.data, ExprData::EUndefined(_)) { + return Some((a.left, a.right)); + } + + // "a === void 0 || a === null" + if matches!(eq_a.data, ExprData::EUndefined(_)) && matches!(eq_b.data, ExprData::ENull(_)) { + return Some((b.left, b.right)); + } + + None +} + // `Expr.Data.eql(left, right, p, .{loose,strict})` — thin adapter // from the `const STRICT: bool` shape used at the four call sites below to the // canonical `ExprData::eql` (Expr.rs). Kept as a free fn so @@ -298,6 +342,13 @@ impl BinaryExpressionVisitor { if let Some(optimized) = try_optimize_typeof_undefined(e_, p, Op::Code::BinGt) { return optimized; } + + // "typeof x === 'string'" => "typeof x == 'string'" + if p.full_minify_syntax() + && SideEffects::can_change_strict_to_loose(&e_.left.data, &e_.right.data) + { + e_.op = Op::Code::BinLooseEq; + } } // const after_op_loc = locAfterOp(e_.); @@ -348,6 +399,13 @@ impl BinaryExpressionVisitor { if let Some(optimized) = try_optimize_typeof_undefined(e_, p, Op::Code::BinLt) { return optimized; } + + // "typeof x !== 'string'" => "typeof x != 'string'" + if p.full_minify_syntax() + && SideEffects::can_change_strict_to_loose(&e_.left.data, &e_.right.data) + { + e_.op = Op::Code::BinLooseNe; + } } } Op::Code::BinNullishCoalescing => { @@ -372,6 +430,20 @@ impl BinaryExpressionVisitor { return e_.right; } } + + if p.full_minify_syntax() { + // "a ?? (b ?? c)" => "a ?? b ?? c" + if let ExprData::EBinary(right) = e_.right.data + && right.op == Op::Code::BinNullishCoalescing + { + e_.left = Expr::join_with_left_associative_op( + Op::Code::BinNullishCoalescing, + e_.left, + right.left, + ); + e_.right = right.right; + } + } } Op::Code::BinLogicalOr => { if let Some(side_effects) = SideEffects::to_boolean(p, &e_.left.data) { @@ -394,6 +466,29 @@ impl BinaryExpressionVisitor { return e_.right; } } + + if p.full_minify_syntax() { + // "a || (b || c)" => "a || b || c" + if let ExprData::EBinary(right) = e_.right.data + && right.op == Op::Code::BinLogicalOr + { + e_.left = Expr::join_with_left_associative_op( + Op::Code::BinLogicalOr, + e_.left, + right.left, + ); + e_.right = right.right; + } + + // "a === null || a === undefined" => "a == null" + if let Some((left, right)) = + is_binary_null_and_undefined(e_.left, e_.right, Op::Code::BinStrictEq) + { + e_.op = Op::Code::BinLooseEq; + e_.left = left; + e_.right = right; + } + } } Op::Code::BinLogicalAnd => { if let Some(side_effects) = SideEffects::to_boolean(p, &e_.left.data) { @@ -416,6 +511,29 @@ impl BinaryExpressionVisitor { return e_.right; } } + + if p.full_minify_syntax() { + // "a && (b && c)" => "a && b && c" + if let ExprData::EBinary(right) = e_.right.data + && right.op == Op::Code::BinLogicalAnd + { + e_.left = Expr::join_with_left_associative_op( + Op::Code::BinLogicalAnd, + e_.left, + right.left, + ); + e_.right = right.right; + } + + // "a !== null && a !== undefined" => "a != null" + if let Some((left, right)) = + is_binary_null_and_undefined(e_.left, e_.right, Op::Code::BinStrictNe) + { + e_.op = Op::Code::BinLooseNe; + e_.left = left; + e_.right = right; + } + } } Op::Code::BinAdd => { if p.should_fold_typescript_constant_expressions { diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index d096d19750ac..67f48169d00d 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -686,8 +686,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let expr = *e; let _ = in_; let mut e_ = expr.data.e_template().expect("infallible: variant checked"); - if e_.tag.is_some() { - p.visit_expr(e_.tag.as_mut().unwrap()); + if let Some(tag) = e_.tag.as_mut() { + p.template_tag = tag.data; + p.visit_expr(tag); } // Visit the interpolation values before the macro dispatch below: its @@ -1495,6 +1496,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut e_ = e.data.e_if().expect("infallible: variant checked"); let is_call_target = matches!(p.call_target, Data::EIf(ct) if core::ptr::eq(&raw const *e_, &raw const *ct)); + let is_delete_target = matches!(p.delete_target, Data::EIf(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); + let is_template_tag = matches!(p.template_tag, Data::EIf(tt) if core::ptr::eq(&raw const *e_, &raw const *tt)); let prev_in_branch = p.in_branch_condition; p.in_branch_condition = true; @@ -1507,6 +1510,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let Some(side_effects) = SideEffects::to_boolean(p, &e_.test.data) else { p.visit_expr(&mut e_.yes); p.visit_expr(&mut e_.no); + // "delete (a ? b.c : b.c)" deletes nothing, "delete b.c" would. + if p.full_minify_syntax() && !is_delete_target { + let result = p.mangle_if_expr(e.loc, e_); + // "(a ? b.c : b.c)()" => "(0, b.c)()", not "b.c()". A tagged + // template binds `this` the same way. + *e = if (is_call_target || is_template_tag) && result.has_value_for_this_in_call() { + p.new_expr(E::Number::new(0.0), result.loc) + .join_with_comma(result) + } else { + result + }; + } return; }; @@ -1519,24 +1534,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.visit_expr(&mut e_.no); p.is_control_flow_dead = old; - if side_effects.side_effects == SideEffects::CouldHaveSideEffects { - *e = SideEffects::simplify_unused_expr(p, e_.test) - .unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc)) - .join_with_comma(e_.yes); - return; - } - - // "(1 ? fn : 2)()" => "fn()" - // "(1 ? this.fn : 2)" => "this.fn" - // "(1 ? this.fn : 2)()" => "(0, this.fn)()" - if is_call_target && e_.yes.has_value_for_this_in_call() { - *e = p - .new_expr(E::Number::new(0.0), e_.test.loc) - .join_with_comma(e_.yes); - return; - } - - *e = e_.yes; + // "(a, true) ? b : c" => "a, b" + *e = Self::fold_if_branch( + p, + side_effects.side_effects, + e_.test, + e_.yes, + is_call_target || is_template_tag, + ); } else { // "false ? dead : live" let old = p.is_control_flow_dead; @@ -1546,24 +1551,45 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.visit_expr(&mut e_.no); // "(a, false) ? b : c" => "a, c" - if side_effects.side_effects == SideEffects::CouldHaveSideEffects { - *e = SideEffects::simplify_unused_expr(p, e_.test) - .unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc)) - .join_with_comma(e_.no); - return; - } + *e = Self::fold_if_branch( + p, + side_effects.side_effects, + e_.test, + e_.no, + is_call_target || is_template_tag, + ); + } + } - // "(1 ? fn : 2)()" => "fn()" - // "(1 ? this.fn : 2)" => "this.fn" - // "(1 ? this.fn : 2)()" => "(0, this.fn)()" - if is_call_target && e_.no.has_value_for_this_in_call() { - *e = p - .new_expr(E::Number::new(0.0), e_.test.loc) - .join_with_comma(e_.no); - return; - } - *e = e_.no; + /// The branch a constant-test conditional folds to. The test is kept in + /// front of it when it may have side effects. A call or template tag + /// target that turns into a property access gets `(0, …)` in front so it + /// still binds `this` to `undefined`: + /// + /// "(a, true) ? b : c" => "a, b" + /// "(1 ? fn : 2)()" => "fn()" + /// "(1 ? this.fn : 2)" => "this.fn" + /// "(1 ? this.fn : 2)()" => "(0, this.fn)()" + /// "(typeof x ? this.fn : 2)()" => "(0, this.fn)()" + fn fold_if_branch( + p: &mut Self, + test_side_effects: SideEffects, + test: Expr, + branch: Expr, + binds_this: bool, + ) -> Expr { + if test_side_effects == SideEffects::CouldHaveSideEffects + && let Some(test) = SideEffects::simplify_unused_expr(p, test) + && !test.is_missing() + { + return test.join_with_comma(branch); + } + if binds_this && branch.has_value_for_this_in_call() { + return p + .new_expr(E::Number::new(0.0), test.loc) + .join_with_comma(branch); } + branch } #[inline(never)] // PERF(port:frame): see e_jsx_element. @@ -2675,19 +2701,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - // Collapse a single-`return` body to a shorthand expression body when - // minifying syntax while bundling: `(a) => { return a; }` becomes - // `(a) => a`. The printer (see js_printer EArrow) emits the shorthand - // when `prefer_expr` is set and the lone statement is a `return` with a - // value. A bare `return;` (no value) keeps the block body. - // - // Gated on `bundle` like the `e_function` name-drop below: the runtime - // transpiler forces `minify_syntax` on for `target.is_bun()` - // (see bundler/options.rs), so without this guard the collapse would - // also run for `bun run`/`bun test` and change an arrow's - // `Function.prototype.toString()` output. - if p.options.features.minify_syntax - && p.options.bundle + // Collapse a single-`return` body to a shorthand expression body: + // `(a) => { return a; }` becomes `(a) => a`. The printer (see + // js_printer EArrow) emits the shorthand when `prefer_expr` is set and + // the lone statement is a `return` with a value. A bare `return;` (no + // value) keeps the block body. + if p.full_minify_syntax() && stmts_list.len() == 1 && matches!( stmts_list[0].data, @@ -2720,10 +2739,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // Restore now so the stack-local pointer never escapes this frame. p.react_refresh.hook_ctx_storage = prev_hook_ctx; - // Remove unused function names when minifying (only when bundling is enabled) - // unless --keep-names is specified - if p.options.features.minify_syntax - && p.options.bundle + // Remove unused function names when minifying unless --keep-names is specified + if p.full_minify_syntax() && !p.options.features.minify_keep_names // SAFETY: current_scope is a live arena ptr while the parser exists. && !p.current_scope().contains_direct_eval @@ -2789,10 +2806,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O return; } - // Remove unused class names when minifying (only when bundling is enabled) - // unless --keep-names is specified - if p.options.features.minify_syntax - && p.options.bundle + // Remove unused class names when minifying unless --keep-names is specified + if p.full_minify_syntax() && !p.options.features.minify_keep_names // SAFETY: current_scope is a live arena ptr while the parser exists. && !p.current_scope().contains_direct_eval diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs index 5c90a2df04ab..7dfb808299b2 100644 --- a/src/js_parser/visit/visit_stmt.rs +++ b/src/js_parser/visit/visit_stmt.rs @@ -1,4 +1,5 @@ #![warn(unused_must_use)] +use super::mangle::{mangle_for, stmts_care_about_scope}; use crate::Error; use crate::lexer as js_lexer; use crate::p::{P, ReactRefreshExportKind}; @@ -1346,6 +1347,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data.stmt = p.visit_single_stmt(data.stmt, StmtsKind::None); p.pop_scope(); + if p.full_minify_syntax() { + // Optimize "x: break x" which some people apparently write by hand + if let StmtData::SBreak(child) = data.stmt.data + && let Some(label) = child.label + && label.ref_ == ref_ + { + return Ok(()); + } + + // Remove the label if it's not necessary + if p.symbols[ref_.inner_index() as usize].use_count_estimate == 0 { + p.append_if_body_preserving_scope(stmts, data.stmt); + return Ok(()); + } + } + stmts.push(*stmt); Ok(()) } @@ -1678,7 +1695,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data.body = p.visit_loop_body(data.body); data.test = SideEffects::simplify_boolean(p, data.test); - if let Some(result) = SideEffects::to_boolean(p, &data.test.data) { + let known = SideEffects::to_boolean(p, &data.test.data); + if let Some(result) = known { if result.side_effects == SideEffects::NoSideEffects { data.test = p.new_expr( E::Boolean { @@ -1689,6 +1707,34 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } + if p.full_minify_syntax() { + // "while (a) {}" => "for (;a;) {}" + // A true value is implied + let test = match known { + Some(result) + if result.value && result.side_effects == SideEffects::NoSideEffects => + { + None + } + _ => Some(data.test), + }; + let for_stmt = p.s( + S::For { + init: None, + test, + update: None, + body: data.body, + }, + stmt.loc, + ); + let StmtData::SFor(mut for_ref) = for_stmt.data else { + unreachable!() + }; + mangle_for(&mut for_ref, p.arena); + stmts.push(for_stmt); + return Ok(()); + } + stmts.push(*stmt); Ok(()) } @@ -1766,81 +1812,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if p.options.features.minify_syntax { - if let Some(effects) = effects { - if effects.value { - if data.no.is_none() - || !SideEffects::should_keep_stmt_in_dead_control_flow( - data.no.unwrap(), - p.arena, - ) - { - if effects.side_effects == SideEffects::CouldHaveSideEffects { - // Keep the condition if it could have side effects (but is still known to be truthy) - if let Some(test) = SideEffects::simplify_unused_expr(p, data.test) { - stmts.push(p.s( - S::SExpr { - value: test, - ..Default::default() - }, - test.loc, - )); - } - } - - return p.append_if_body_preserving_scope(stmts, data.yes); - } else { - // We have to keep the "no" branch - } - } else { - // The test is falsy - if !SideEffects::should_keep_stmt_in_dead_control_flow(data.yes, p.arena) { - if effects.side_effects == SideEffects::CouldHaveSideEffects { - // Keep the condition if it could have side effects (but is still known to be truthy) - if let Some(test) = SideEffects::simplify_unused_expr(p, data.test) { - stmts.push(p.s( - S::SExpr { - value: test, - ..Default::default() - }, - test.loc, - )); - } - } - - if data.no.is_none() { - return Ok(()); - } - - return p.append_if_body_preserving_scope(stmts, data.no.unwrap()); - } - } - } - - // TODO: more if statement syntax minification - let can_remove_test = p.expr_can_be_removed_if_unused(&data.test); - match data.yes.data { - StmtData::SExpr(yes_expr) => { - if yes_expr.value.is_missing() { - if let Some(no) = data.no { - if no.is_missing_expr() && can_remove_test { - return Ok(()); - } - } else if can_remove_test { - return Ok(()); - } - } - } - StmtData::SEmpty(_) => { - if let Some(no) = data.no { - if no.is_missing_expr() && can_remove_test { - return Ok(()); - } - } else if can_remove_test { - return Ok(()); - } - } - _ => {} - } + let StmtData::SIf(s_ref) = stmt.data else { + unreachable!() + }; + p.mangle_if(stmts, stmt.loc, s_ref); + return Ok(()); } stmts.push(*stmt); @@ -1886,8 +1862,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O local.decls.slice(), RelocateVarsMode::Normal, ); - if let Some(relocated) = relocate.stmt { - data.init = Some(relocated); + if relocate.ok { + // `stmt` is the assignments that replace the declaration. + // Without initializers there is nothing left to run here. + data.init = relocate.stmt; } } } @@ -1895,6 +1873,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.pop_scope(); + if p.full_minify_syntax() { + mangle_for(data, p.arena); + } + stmts.push(*stmt); Ok(()) } @@ -2072,6 +2054,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.pop_scope(); if let Some(catch) = &mut data.catch { + let old_is_control_flow_dead = p.is_control_flow_dead; + + // If the try body is empty, then the catch body is dead. Only the + // build paths strip it, so plain transpiler output stays as written. + if data.body.is_empty() + && p.full_minify_syntax() + && p.options.features.dead_code_elimination + { + p.is_control_flow_dead = true; + } + p.push_scope_for_visit_pass(js_ast::scope::Kind::CatchBinding, catch.loc) .expect("unreachable"); { @@ -2089,6 +2082,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O catch.body = list_to_stmts(_stmts); } p.pop_scope(); + + p.is_control_flow_dead = old_is_control_flow_dead; } if let Some(finally) = &mut data.finally { @@ -2103,6 +2098,67 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.pop_scope(); } + if p.full_minify_syntax() { + if data.body.is_empty() { + // Try to drop the whole thing if the try body is empty + let mut keep_catch = false; + + // Certain "catch" blocks need to be preserved: + // + // try {} catch { let foo } // Can be removed + // try {} catch { var foo } // Must be kept + // + if let Some(catch) = &data.catch { + for stmt2 in catch.body.slice() { + if SideEffects::should_keep_stmt_in_dead_control_flow(*stmt2, p.arena) { + keep_catch = true; + break; + } + } + } + + // Make sure to preserve the "finally" block if present + if !keep_catch { + let Some(finally) = &data.finally else { + return Ok(()); + }; + if !stmts_care_about_scope(finally.stmts.slice()) { + stmts.extend_from_slice(finally.stmts.slice()); + return Ok(()); + } + stmts.push(p.s( + S::Block { + stmts: finally.stmts, + close_brace_loc: js_ast::Loc::EMPTY, + }, + finally.loc, + )); + return Ok(()); + } + } else if let Some(finally) = &data.finally + && finally.stmts.is_empty() + { + if data.catch.is_some() { + // Just remove the "finally" block if there's a "catch" + data.finally = None; + } else { + // Otherwise, try to unwrap the whole "try" statement + if !stmts_care_about_scope(data.body.slice()) { + stmts.extend_from_slice(data.body.slice()); + return Ok(()); + } + stmts.push(p.s( + S::Block { + stmts: data.body, + close_brace_loc: js_ast::Loc::EMPTY, + }, + stmt.loc, + )); + return Ok(()); + } + } + } + stmts.push(*stmt); Ok(()) } @@ -2135,6 +2191,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } p.fn_or_arrow_data_visit.is_inside_switch = old_is_inside_switch; + // All case bodies share this block scope, so a `let`/`const` + // declared in one case can be used by a later case. Only now, with + // every case visited, are the use counts final. + if p.options.features.minify_syntax && p.options.features.dead_code_elimination { + for i in 0..cases.len() { + let body = stmts_to_list(p.arena, cases[i].body); + cases[i].body = list_to_stmts(p.mangle_stmts(body, StmtsKind::SwitchStmt)); + } + } + for i in 0..cases.len() { if p.should_lower_using_declarations(cases[i].body.slice()) { lowered_using = true; diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 7eeda62a814a..e7ab20d1fcc5 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -4207,7 +4207,7 @@ pub(crate) mod __gated_printer { self.add_source_mapping(expr.loc); // Optional chains are forbidden in template tags // `Expr::is_optional_chain` is gated upstream; inline its body. - let is_optional_chain = match &expr.data { + let is_optional_chain = match &tag.data { ExprData::EDot(d) => d.optional_chain.is_some(), ExprData::EIndex(i) => i.optional_chain.is_some(), ExprData::ECall(c) => c.optional_chain.is_some(), diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 6ad49e143f1a..5bbf9827aca5 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -56,7 +56,9 @@ bun_core::declare_scope!(cache, visible); /// u8/u16/u32 ids and implied slots dropped, instead of fixed u32 arrays. /// Version 27: ModuleInfo string table holds Latin-1 / UTF-16 bodies, not WTF-8. /// Version 28: the define table and `--drop` entries participate in the features hash. -const EXPECTED_VERSION: u32 = 28; +/// Version 29: the single-use `let`/`const` inliner no longer deletes a declaration +/// in one switch case clause that a later clause still uses. +const EXPECTED_VERSION: u32 = 29; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index f7ae8991b9ea..a8acc85a3f75 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -210,6 +210,9 @@ impl BuildCommand { this_transpiler.options.code_splitting = ctx.bundler_options.code_splitting; this_transpiler.options.split_require = ctx.bundler_options.split_require; this_transpiler.options.minify_syntax = ctx.bundler_options.minify_syntax; + // `bun build --no-bundle` parses through the transpiler, not the + // bundler's parse task, so the statement passes are turned on here. + this_transpiler.options.minify_syntax_statements = ctx.bundler_options.minify_syntax; this_transpiler.options.minify_whitespace = ctx.bundler_options.minify_whitespace; this_transpiler.options.minify_identifiers = ctx.bundler_options.minify_identifiers; this_transpiler.options.keep_names = ctx.bundler_options.keep_names; diff --git a/test/bundler/bundler_bytecode_portable.test.ts b/test/bundler/bundler_bytecode_portable.test.ts index a3debaf55203..ea2fcc3b0b73 100644 --- a/test/bundler/bundler_bytecode_portable.test.ts +++ b/test/bundler/bundler_bytecode_portable.test.ts @@ -474,24 +474,24 @@ describe("bytecode cache portability", () => { "sha256": "2a5d62fb4ca9d107e3a5bb2abe6a73f3c859a6f1a91c6c3d77653cb8c2053361", }, "bun build --bytecode --minify all.js": { - "js": "721d67b0c2135aad554c9962b4e4505a4d6856b50da537fee7566d43f6cfdbda", + "js": "84c5bbb4d0c2ab2c2663dbf749488d140e5087d29d1fd0bd08a0d4711e0cbb5d", "jsc": { - "bytes": 1998992, - "sha256": "f2631b3ba2b532e52183d5e6a44a7dbf5c0515b368d95fc692bf0e5ad15eb8f6", + "bytes": 2001536, + "sha256": "70815d92e6fb90269a845cdde7f9d98a388d4b3bc15aeb821c6f3c64ea2e1d29", }, }, "bun build --bytecode --minify features.js": { - "js": "d49da0aa39824bf9eba2af5d3a010525ad14ca5c1cedc0d8adcfdd5f4984a0d0", + "js": "c68a6bb1464d0cd4d51865b2fb477f5c6ed454d58461f5feb0c2273799cc27b6", "jsc": { - "bytes": 46152, - "sha256": "db36543ec2430a8cbdce13a6a980148f502e2c6b135cf95f62b6743c3fa30968", + "bytes": 46128, + "sha256": "82d1bd2621d2aaa23c0d80d8755608a3f4a44ed02c4ca7678f4d9d3031acc3a8", }, }, "bun build --bytecode --minify records.js": { - "js": "889cbb2c9525ff69a2676a6e81d97bb87760bdee65178b836c9c1d6808ac7c6e", + "js": "7dee3ebff15236bf392c940b63932d088b469049ac86aa91bb6833e44e9480d6", "jsc": { - "bytes": 89144, - "sha256": "3c3e86151fe78e6d56d7e7033dfa0e97b062af3b86d7ad75b723d91c77b11cba", + "bytes": 89128, + "sha256": "41851070b6000fb8cc321ebd5f0d2d96e9661c17e017553e73a23e565cb26fb2", }, }, "bun build --bytecode acorn/dist/acorn.mjs": { diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 762453475b4d..a8e4ca6230c9 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1302,7 +1302,7 @@ describe("bundler", () => { snapshotSourceMap: { "entry.js.map": { files: ["../node_modules/react/index.js", "../entry.js"], - mappingsExactMatch: "2lBACA,WAAW,IAAQ,EAAE,ICDrB,aACA,QAAQ,IAAI,CAAK", + mappingsExactMatch: "klBACA,WAAW,IAAQ,EAAE,ICDrB,aACA,QAAQ,IAAI,CAAK", }, }, }); diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts index 4c8ea49b3bc0..c71a8081d741 100644 --- a/test/bundler/bundler_minify.test.ts +++ b/test/bundler/bundler_minify.test.ts @@ -1179,8 +1179,8 @@ describe("bundler", () => { "e2 instanceof Error", 'e1.message === "with new"', 'e2.message === "without new"', - 'typeof e1.stack === "string"', - 'typeof e2.stack === "string"', + 'typeof e1.stack == "string"', + 'typeof e2.stack == "string"', "withNew.constructor === withoutNew.constructor", ], minifySyntax: true, @@ -1354,12 +1354,12 @@ describe("bundler", () => { "JSON.stringify(a1) === JSON.stringify(a2)", "a1.constructor === a2.constructor", "sparse.length === 5", - "0 in sparse === !1", + "0 in sparse == !1", 'JSON.stringify(sparse) === "[null,null,null,null,null]"', "a3.length === a4.length && a3.length === 3 && a3[0] === void 0", - "typeof o1 === typeof o2", + "typeof o1 == typeof o2", "o1.constructor === o2.constructor", - "typeof f1 === typeof f2", + "typeof f1 == typeof f2", "f1() === f2()", "r1.source === r2.source", "r1.flags === r2.flags", @@ -1398,7 +1398,7 @@ describe("bundler", () => { onAfterBundle(api) { const file = api.readFile("out.js"); expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot( - `"console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x==="string");console.log(x==="undefined");console.log(y==="undefined");console.log(typeof x==="undefinedx");"`, + `"console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x<"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x>"u");console.log(typeof x=="string");console.log(x==="undefined");console.log(y==="undefined");console.log(typeof x=="undefinedx");"`, ); }, }); @@ -1485,13 +1485,14 @@ describe("bundler", () => { }, }); - // A bare `return;` (no value) cannot be expressed as a shorthand arrow body, - // so the block body must be preserved even when minifying. + // A bare `return;` (no value) cannot be expressed as a shorthand arrow body. + // A trailing `return;` is dropped, and the empty block body stays. itBundled("minify/ArrowBareReturnKeepsBlock", { files: { "/entry.js": /* js */ ` export const bare = (x) => { return; }; export const undef = (x) => { return undefined; }; + export const tail = (x) => { x(); return; }; export function fn(a) { return a + 1; } `, }, @@ -1499,9 +1500,10 @@ describe("bundler", () => { minifyIdentifiers: false, onAfterBundle(api) { const code = api.readFile("/out.js"); - // Bare `return;` and `return undefined;` (normalized to `return;`) stay as blocks. - expect(code).toMatch(/bare = \(x\) => \{\s*return;?\s*\}/); - expect(code).toMatch(/undef = \(x\) => \{\s*return;?\s*\}/); + // Bare `return;` and `return undefined;` (normalized to `return;`) are dropped. + expect(code).toMatch(/bare = \(x\) => \{\s*\}/); + expect(code).toMatch(/undef = \(x\) => \{\s*\}/); + expect(code).toMatch(/tail = \(x\) => \{\s*x\(\);?\s*\}/); // Function declarations are never rewritten into arrows. expect(code).toContain("function fn(a)"); }, @@ -1589,14 +1591,13 @@ describe("bundler", () => { }, }); - // The collapse is gated on bundling, so minify-syntax alone (no bundle) must - // keep the block body. The runtime transpiler (`bun run`/`bun test`) forces - // minify-syntax on for bun targets but never bundles, so collapsing here - // would change `Function.prototype.toString()` output at runtime. - itBundled("minify/ArrowReturnNotCollapsedWhenNotBundling", { + // `bun build --no-bundle --minify-syntax` runs the same statement-level + // passes as the bundler, including the arrow body collapse. + itBundled("minify/ArrowReturnCollapsedWhenNotBundling", { files: { "/entry.js": /* js */ ` export const foo = (a) => { return a + 1; }; + export function bar(a, b) { if (a) return b(); return 0; } `, }, bundling: false, @@ -1604,7 +1605,237 @@ describe("bundler", () => { minifyIdentifiers: false, onAfterBundle(api) { const code = api.readFile("/out.js"); - expect(code).toMatch(/=>\s*\{\s*return a \+ 1;?\s*\}/); + expect(code).toMatch(/foo = \(a\) => a \+ 1/); + expect(code).toMatch(/return a \? b\(\) : 0/); + }, + }); + + // The runtime transpiler (`bun run`/`bun test`) forces minify-syntax on for + // bun targets but never bundles. It must not restructure statements or + // rewrite comparisons, so that `Function.prototype.toString()` and line + // numbers stay close to the source. + test("runtime transpiler keeps statement structure", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + /* js */ ` + const foo = (a) => { return a + 1; }; + function bar(a, b) { + if (a) return b(); + if (!a) b(); else b(); + while (a) if (b()) break; + try { b() } catch (e) { b() } + if (typeof a === "string" || a === null || a === void 0) b(); + if ((a >>> 0) !== 0) b(); + if (!!a && true) b(); + if ((a || false) || false) b(); + a != null && a.b(); + return 0; + } + console.log(foo.toString()); + console.log(bar.toString()); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "(a) => { + return a + 1; + } + function bar(a, b) { + if (a) + return b(); + if (!a) + b(); + else + b(); + while (a) + if (b()) + break; + try { + b(); + } catch (e) { + b(); + } + if (typeof a === "string" || a === null || a === void 0) + b(); + if (a >>> 0 !== 0) + b(); + if (a) + b(); + if (a) + b(); + a != null && a.b(); + return 0; + }" + `); + expect(exitCode).toBe(0); + }); + + // Statement-level syntax minification (esbuild's mangleIf, mangleStmts, + // mangleFor, MangleIfExpr). Each function is checked for shape, then the + // bundle runs to show the rewrites keep the original behavior. + itBundled("minify/StatementLevelMangling", { + files: { + "/entry.js": /* js */ ` + export function ifElseToTernary(a) { if (a) return 1; else return 2 } + export function ifChain(r) { if (!r) return 1; if (r.x) return 2; return 3 } + export function ifAnd(f, i, g) { if (f) { if (i) g() } } + export function ifOr(e, g) { if (e); else g() } + export function throwChain(a, b) { if (a) throw b; throw 1 } + export function whileBreak(g) { let n = 0; while (true) { if (g(n)) break; n++ } return n } + export function catchUnused(g, h) { try { g() } catch (t) { h() } } + export function catchVarQuirk() { try { throw 1 } catch (x) { var x = 2 } return x } + export function usedLabel(b) { x: { if (b) break x; return 1 } return 2 } + export function unusedLabel(b) { x: { return b } } + export function negZero(b) { return b ? -0 : 0 } + export function optChain(a) { return a != null ? a.b : undefined } + export function nullish(a, b) { return a != null ? a : b } + export function callTarget(o, a) { return (a ? o.m : o.m)() } + export function tagTarget(o, a) { return (a ? o.m : o.m)\`x\` } + export function tagTargetFolded(o) { return (1 ? o.m : 2)\`x\` } + export function tagTargetTypeof(o) { return (typeof o ? o.m : 2)\`x\` } + export function callTargetTypeof(o) { return (typeof o ? o.m : 2)() } + export function tagOptionalChain(a) { return (a != null ? a.b() : void 0)\`x\` } + export function implicitReturn(y, z) { if (y) return; z() } + export function loopContinue(xs, f) { for (const x of xs) { if (x > 2) continue; f(x) } } + export function typeofEq(t) { return typeof t === "string" } + export function nullUndef(n) { return n === void 0 || n === null } + export function ternaryBool(a) { return a ? true : false } + export function sameArgs(a, b, c, d) { return a ? b(c) : b(d) } + export function exprStmts(a, b) { a(); b(); return 1 } + export function deleteCond(o, t) { delete (t ? o.b : o.b); return "b" in o } + + const out = []; + const log = (...v) => out.push(...v); + log(ifElseToTernary(1), ifElseToTernary(0)); + log(ifChain(null), ifChain({ x: 1 }), ifChain({})); + { let n = 0; ifAnd(1, 1, () => n++); ifAnd(1, 0, () => n++); ifAnd(0, 1, () => n++); log(n); } + { let n = 0; ifOr(0, () => n++); ifOr(1, () => n++); log(n); } + try { throwChain(1, "b") } catch (e) { log(e) } + try { throwChain(0, "b") } catch (e) { log(e) } + log(whileBreak(n => n >= 3)); + { let n = 0; catchUnused(() => { throw 0 }, () => n++); log(n); } + log(catchVarQuirk()); + log(usedLabel(true), usedLabel(false), unusedLabel(7)); + log(Object.is(negZero(true), -0), Object.is(negZero(false), 0)); + log(optChain(null), optChain({ b: 5 })); + log(nullish(null, 2), nullish(0, 2)); + log(callTarget({ m() { return this === undefined } }, true)); + log(callTargetTypeof({ m() { return this === undefined } })); + log(tagOptionalChain({ b() { return s => s[0] } })); + { let n = 0; implicitReturn(true, () => n++); implicitReturn(false, () => n++); log(n); } + { const seen = []; loopContinue([1, 2, 3, 4], x => seen.push(x)); log(seen.join()); } + log(typeofEq("s"), typeofEq(1)); + log(nullUndef(undefined), nullUndef(null), nullUndef(0)); + log(ternaryBool(1), ternaryBool("")); + log(sameArgs(true, x => x * 2, 1, 2), sameArgs(false, x => x * 2, 1, 2)); + { const seen = []; log(exprStmts(() => seen.push("a"), () => seen.push("b")), seen.join()); } + log(deleteCond({ b: 1 }, true), deleteCond({ b: 1 }, false)); + console.log(JSON.stringify(out)); + `, + }, + minifySyntax: true, + minifyWhitespace: true, + minifyIdentifiers: false, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + const fns = [...code.matchAll(/function (\w+)\([^)]*\)\{(.*?)\}(?=function |var out)/g)].map(m => [m[1], m[2]]); + expect(Object.fromEntries(fns)).toEqual({ + ifElseToTernary: "return a?1:2", + ifChain: "return r?r.x?2:3:1", + ifAnd: "f&&i&&g()", + ifOr: "e||g()", + throwChain: "throw a?b:1", + whileBreak: "let n=0;for(;!g(n);)n++;return n", + catchUnused: "try{g()}catch{h()}", + // The hoisted `var x` shares its name with the catch binding, so the binding stays. + catchVarQuirk: "try{throw 1}catch(x){var x=2}return x", + usedLabel: "x:{if(b)break x;return 1}return 2", + unusedLabel: "return b", + // `-0` and `0` are not the same value, so the branches are not merged. + negZero: "return b?-0:0", + optChain: "return a?.b", + nullish: "return a??b", + // The comma keeps `this` unbound, as it was for the conditional. The + // bundle is right for the tag too, but the runtime transpiler drops + // that comma today (#40829), so only the shape is checked for it. + callTarget: "return(0,o.m)()", + tagTarget: "return(0,o.m)`x`", + tagTargetFolded: "return(0,o.m)`x`", + tagTargetTypeof: "return(0,o.m)`x`", + callTargetTypeof: "return(0,o.m)()", + // An optional chain cannot be a template tag, so the tag is parenthesized. + tagOptionalChain: "return(a?.b())`x`", + implicitReturn: "y||z()", + loopContinue: "for(let x of xs)x>2||f(x)", + typeofEq: 'return typeof t=="string"', + nullUndef: "return n==null", + ternaryBool: "return!!a", + sameArgs: "return b(a?c:d)", + exprStmts: "return a(),b(),1", + // `delete` of a conditional deletes nothing, so the conditional stays. + deleteCond: 'return delete(t?o.b:o.b),"b"in o', + }); + }, + run: { + stdout: + '[1,2,1,2,3,1,1,"b",1,3,1,null,2,1,7,true,true,null,5,2,0,true,true,"x",1,"1,2",true,false,true,true,false,true,false,2,4,1,"a,b",true,true]', + }, + }); + + // Every `if` rewrite runs with a truthy and a falsy test. The side effects + // of the test and of each branch must match the original `if`, and + // comma-joined branches stay grouped inside the conditional. + itBundled("minify/IfStatementMangling", { + files: { + "/entry.js": /* js */ ` + const out = []; + function both(a){if(a)out.push("y"+a);else out.push("n"+a);} + function yes(a){if(a)out.push("Y"+a);} + function notYes(a){if(!a)out.push("N"+a);} + function notBoth(a){if(!a)out.push("b"+a);else out.push("c"+a);} + function blocks(a){if(a){out.push("x"+a);out.push("y"+a);}else{out.push("z"+a);out.push("w"+a);}} + function retBoth(a){if(a)return "r1";else return "r2";} + function retMixed(a){if(a)return;else return "m";} + function throwBoth(a){try{if(a)throw "t1";else throw "t2";}catch(e){return e;}} + function emptyYes(a){if(a){}else out.push("E"+a);} + function emptyBoth(a){let n=0;if((n++,a)){}else{};out.push("eb"+n);} + + for (const v of [0, 1]) { + both(v); yes(v); notYes(v); notBoth(v); blocks(v); + out.push(retBoth(v), String(retMixed(v)), throwBoth(v)); + emptyYes(v); emptyBoth(v); + } + console.log(out.join(",")); + `, + }, + minifySyntax: true, + minifyWhitespace: true, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + const fns = [...code.matchAll(/function (\w+)\([^)]*\)\{(.*?)\}(?=function |for\()/g)].map(m => [m[1], m[2]]); + expect(Object.fromEntries(fns)).toEqual({ + both: 'a?out.push("y"+a):out.push("n"+a)', + yes: 'a&&out.push("Y"+a)', + notYes: 'a||out.push("N"+a)', + notBoth: 'a?out.push("c"+a):out.push("b"+a)', + blocks: 'a?(out.push("x"+a),out.push("y"+a)):(out.push("z"+a),out.push("w"+a))', + retBoth: 'return a?"r1":"r2"', + // `return a?void 0:"m"` at the end of a function body becomes an `if`. + retMixed: 'if(!a)return"m"', + throwBoth: 'try{throw a?"t1":"t2"}catch(e){return e}', + emptyYes: 'a||out.push("E"+a)', + emptyBoth: 'let n=0;n++,out.push("eb"+n)', + }); + }, + run: { + stdout: "n0,N0,b0,z0,w0,r2,m,t2,E0,eb1,y1,Y1,c1,x1,y1,r1,undefined,t1,eb1", }, }); @@ -1681,6 +1912,137 @@ describe("bundler", () => { expect(code.match(/let keep = /g)).toHaveLength(10); }, }); + + // A `let`/`const` declared in one case clause is scoped to the whole switch + // block, so a later clause can still read or assign it. The single-use + // inliner used to run per clause and deleted `tag`, `s` and `n` after it + // had only seen the uses in the declaring clause. `only` has its single use + // in the same clause and must still be inlined. + itBundled("minify/SwitchCaseDeclUsedInLaterCase", { + files: { + "/entry.js": /* js */ ` + function capture(v) { return v; } + function fallthrough(k) { + switch (k) { + case 1: const tag = { id: 1 }; capture(tag); + case 2: return typeof tag; + } + } + function defaultClause() { + switch (1) { + case 1: let s = "s1"; capture(s.length); + default: { return s + "!"; } + } + } + function reassigned(k) { + switch (k) { + case 1: let n = k; capture(n); + case 2: n = 5; return n; + } + } + function sameClause(v) { + switch (v) { + case 1: const only = v; return capture(only + 1); + } + } + console.log(JSON.stringify([fallthrough(1), defaultClause(), reassigned(1), sameClause(1)])); + `, + }, + capture: ["v", "tag", "s.length", "n", "v + 1"], + minifySyntax: true, + minifyIdentifiers: false, + target: "bun", + run: { + stdout: '["object","s1!",5,2]', + }, + }); + + // A long run of `if (x) return;` nests into one `&&` chain and a long run + // of `if (x) return y;` into one `?:` chain. Both stop at 128 levels so the + // recursive walkers and the printer stay within the stack. The rest of the + // list stays as written (in `early` it joins the flat `||` chain that the + // same-jump merge builds), the build succeeds, and the output runs. + const longIfs = (n: number, body: (i: number) => string) => + Array.from({ length: n }, (_, i) => `if (a[${i}]) ${body(i)};`).join("\n"); + itBundled("minify/LongJumpChainsStayBounded", { + files: { + "/entry.js": /* js */ ` + function early(a) { ${longIfs(600, () => "return")} return "end"; } + function lookup(a) { ${longIfs(600, i => `return ${i}`)} return -1; } + function raise(a) { ${longIfs(600, i => `throw ${i}`)} throw -1; } + const hit = i => { const a = []; a[i] = 1; return a; }; + let thrown; + try { raise(hit(599)); } catch (e) { thrown = e; } + console.log(JSON.stringify([early(hit(599)), early([]), lookup(hit(0)), lookup(hit(599)), lookup([]), thrown])); + `, + }, + minifySyntax: true, + minifyWhitespace: true, + minifyIdentifiers: false, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + const fn = (name: string) => code.match(new RegExp(`function ${name}\\(a\\)\\{(.*?)\\}(?:function |var )`))![1]; + const count = (s: string, re: RegExp) => (s.match(re) ?? []).length; + expect({ + earlyAnd: count(fn("early"), /&&/g), + earlyOr: count(fn("early"), /\|\|/g), + earlyTernary: count(fn("early"), /\?/g), + lookupIfs: count(fn("lookup"), /if\(a\[\d+\]\)/g), + lookupTernary: count(fn("lookup"), /\?/g), + raiseIfs: count(fn("raise"), /if\(a\[\d+\]\)/g), + raiseTernary: count(fn("raise"), /\?/g), + }).toEqual({ + earlyAnd: 127, + earlyOr: 471, + earlyTernary: 1, + lookupIfs: 472, + lookupTernary: 128, + raiseIfs: 472, + raiseTernary: 128, + }); + }, + run: { + stdout: '[null,"end",0,599,-1,599]', + }, + }); +}); + +// The runtime transpiler runs the same single-use inliner. A `const` declared +// in one case clause and read after a fall-through into the next clause must +// keep its declaration. +test("runtime transpiler keeps a switch case declaration that a later case reads", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + /* js */ ` + function f(k) { + switch (k) { + case 1: const tag = { id: 1 }; use(tag); + case 2: return typeof tag; + } + } + function g() { + switch (1) { + case 1: let s = "s1"; use(s.length); + default: { return s + "!"; } + } + } + function use(v) { return v; } + let r; + try { r = g(); } catch (e) { r = e.constructor.name; } + console.log(JSON.stringify([f(1), r])); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe('["object","s1!"]\n'); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); }); // The runtime transpiler (`bun run`/`bun test`) implicitly enables diff --git a/test/bundler/bundler_npm.test.ts b/test/bundler/bundler_npm.test.ts index 2c5c29a375b8..577558f1ce44 100644 --- a/test/bundler/bundler_npm.test.ts +++ b/test/bundler/bundler_npm.test.ts @@ -55,17 +55,17 @@ describe("bundler", () => { "../entry.tsx", ], mappings: [ - ["react.development.js:524:'getContextName'", "1:5623:at"], - ["react.development.js:2495:'actScopeDepth'", "23:4082:or++"], - ["react.development.js:696:''Component'", '1:7685:\'Component "%s"'], - ["entry.tsx:6:'\"Content-Type\"'", '100:18811:"Content-Type"'], - ["entry.tsx:11:''", "100:19065:void"], - ["entry.tsx:23:'await'", "100:19164:await"], + ["react.development.js:524:'getContextName'", "1:5578:at"], + ["react.development.js:2495:'actScopeDepth'", "23:3981:or++"], + ["react.development.js:696:''Component'", '1:7616:\'Component "%s"'], + ["entry.tsx:6:'\"Content-Type\"'", '100:18564:"Content-Type"'], + ["entry.tsx:11:''", "100:18818:void"], + ["entry.tsx:23:'await'", "100:18917:await"], ], }, }, expectExactFilesize: { - "out/entry.js": 221989, + "out/entry.js": 218949, }, run: { stdout: "

Hello World

This is an example.

", diff --git a/test/bundler/esbuild/js_parser_mangle.test.ts b/test/bundler/esbuild/js_parser_mangle.test.ts new file mode 100644 index 000000000000..a4d9b34a8696 --- /dev/null +++ b/test/bundler/esbuild/js_parser_mangle.test.ts @@ -0,0 +1,641 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Ported from esbuild's internal/js_parser/js_parser_test.go. Each case is a +// program and the output esbuild prints for it with `--minify-syntax`. Bun +// prints the same tokens with a different layout (indentation, `!0` for +// `true`), so both sides are compared with the layout normalized away. +// +// Two kinds of cases keep Bun's own output instead of esbuild's: +// - `// bun: relocates var`: Bun hoists a nested `var` to the top level of a +// transform-only build (esbuild only does that when bundling), so the `var` +// statement ends up after the loop or try instead of inside it. +// - `// bun: strict mode`: Bun parses the file as a module, so a function +// declaration in a block does not get the sloppy-mode `var` alias. +// +// Cases that depend on esbuild's `MaybeSimplifyEqualityComparison` +// (`!a === false` => `!!a`, `(a, b) === c` => `a, b === c`) are not ported. + +type Case = [input: string, expected: string]; + +function normalize(code: string): string { + return code + .replaceAll("!0", "true") + .replaceAll("!1", "false") + .replace(/\s+/g, " ") + .trim() + .replace(/ ?([^\w$ ]) ?/g, "$1"); +} + +// One `bun build --no-bundle --minify-syntax` run per group, every case as its own entry point. +async function mangle(cases: Case[]): Promise { + const files: Record = {}; + cases.forEach(([input], i) => { + files[`case${i}.js`] = input; + }); + using dir = tempDir("esbuild-mangle", files); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "--no-bundle", "--minify-syntax", "--outdir=out", ...Object.keys(files)], + cwd: String(dir), + env: bunEnv, + stdout: "ignore", + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + return cases.map((_, i) => readFileSync(join(String(dir), "out", `case${i}.js`), "utf8")); +} + +function check(name: string, cases: Case[]) { + test.concurrent(name, async () => { + const outputs = await mangle(cases); + const got: Record = {}; + const want: Record = {}; + cases.forEach(([input, expected], i) => { + got[`${i}: ${input}`] = normalize(outputs[i]); + want[`${i}: ${input}`] = normalize(expected); + }); + expect(got).toEqual(want); + }); +} + +check("TestMangleFor", [ + ["var a; while (1) ;", "for (var a; ; ) ;\n"], + ["let a; while (1) ;", "let a;\nfor (; ; ) ;\n"], + ["const a=0; while (1) ;", "const a = 0;\nfor (; ; ) ;\n"], + ["var a; for (var b;;) ;", "for (var a;; )\n ;\nvar b;\n"], // bun: relocates var + ["let a; for (let b;;) ;", "let a;\nfor (let b; ; ) ;\n"], + ["const a=0; for (const b = 1;;) ;", "const a = 0;\nfor (const b = 1; ; ) ;\n"], + ["export var a; while (1) ;", "export var a;\nfor (; ; ) ;\n"], + ["export let a; while (1) ;", "export let a;\nfor (; ; ) ;\n"], + ["export const a=0; while (1) ;", "export const a = 0;\nfor (; ; ) ;\n"], + ["export var a; for (var b;;) ;", "export var a;\nfor (;; )\n ;\nvar b;\n"], // bun: relocates var + ["export let a; for (let b;;) ;", "export let a;\nfor (let b; ; ) ;\n"], + ["export const a=0; for (const b = 1;;) ;", "export const a = 0;\nfor (const b = 1; ; ) ;\n"], + ["var a; for (let b;;) ;", "var a;\nfor (let b; ; ) ;\n"], + ["let a; for (const b=0;;) ;", "let a;\nfor (const b = 0; ; ) ;\n"], + ["const a=0; for (var b;;) ;", "const a = 0;\nfor (;; )\n ;\nvar b;\n"], // bun: relocates var + ["a(); while (1) ;", "for (a(); ; ) ;\n"], + ["a(); for (b();;) ;", "for (a(), b(); ; ) ;\n"], + ["for (; ;) if (x) break;", "for (; !x; ) ;\n"], + ["for (; ;) if (!x) break;", "for (; x; ) ;\n"], + ["for (; a;) if (x) break;", "for (; a && !x; ) ;\n"], + ["for (; a;) if (!x) break;", "for (; a && x; ) ;\n"], + ["for (; ;) { if (x) break; y(); }", "for (; !x; )\n y();\n"], + ["for (; a;) { if (x) break; y(); }", "for (; a && !x; )\n y();\n"], + ["for (; ;) if (x) break; else y();", "for (; !x; ) y();\n"], + ["for (; a;) if (x) break; else y();", "for (; a && !x; ) y();\n"], + ["for (; ;) { if (x) break; else y(); z(); }", "for (; !x; )\n y(), z();\n"], + ["for (; a;) { if (x) break; else y(); z(); }", "for (; a && !x; )\n y(), z();\n"], + ["for (; ;) if (x) y(); else break;", "for (; x; ) y();\n"], + ["for (; ;) if (!x) y(); else break;", "for (; !x; ) y();\n"], + ["for (; a;) if (x) y(); else break;", "for (; a && x; ) y();\n"], + ["for (; a;) if (!x) y(); else break;", "for (; a && !x; ) y();\n"], + ["for (; ;) { if (x) y(); else break; z(); }", "for (; x; ) {\n y();\n z();\n}\n"], + ["for (; a;) { if (x) y(); else break; z(); }", "for (; a && x; ) {\n y();\n z();\n}\n"], +]); + +check("TestMangleLoopJump", [ + ["while (x) { if (1) break; z(); }", "for (; x; )\n break;\n"], + ["while (x) { if (1) continue; z(); }", "for (; x; )\n ;\n"], + ["foo: while (a) while (x) { if (1) continue foo; z(); }", "foo: for (; a; ) for (; x; )\n continue foo;\n"], + ["while (x) { y(); if (1) break; z(); }", "for (; x; ) {\n y();\n break;\n}\n"], + ["while (x) { y(); if (1) continue; z(); }", "for (; x; )\n y();\n"], + ["while (x) { y(); debugger; if (1) continue; z(); }", "for (; x; ) {\n y();\n debugger;\n}\n"], + ["while (x) { let y = z(); if (1) continue; z(); }", "for (; x; ) {\n let y = z();\n}\n"], + ["while (x) { debugger; if (y) { if (1) break; z() } }", "for (; x; ) {\n debugger;\n if (y)\n break;\n}\n"], + ["while (x) { debugger; if (y) { if (1) continue; z() } }", "for (; x; ) {\n debugger;\n y;\n}\n"], + ["while (x) { debugger; if (1) { if (1) break; z() } }", "for (; x; ) {\n debugger;\n break;\n}\n"], + ["while (x) { debugger; if (1) { if (1) continue; z() } }", "for (; x; )\n debugger;\n"], + ["while (x()) continue", "for (; x(); ) ;\n"], + ["while (x) { y(); continue }", "for (; x; )\n y();\n"], + ["while (x) { if (y) { z(); continue } }", "for (; x; )\n if (y) {\n z();\n continue;\n }\n"], + [ + "label: while (x) while (y) { z(); continue label }", + "label: for (; x; ) for (; y; ) {\n z();\n continue label;\n}\n", + ], + ["while (x) { if (y) continue; z(); }", "for (; x; )\n y || z();\n"], + ["while (x) { if (y) continue; else z(); w(); }", "for (; x; )\n y || (z(), w());\n"], + ["while (x) { t(); if (y) continue; z(); }", "for (; x; )\n t(), !y && z();\n"], + ["while (x) { t(); if (y) continue; else z(); w(); }", "for (; x; )\n t(), !y && (z(), w());\n"], + ["while (x) { debugger; if (y) continue; z(); }", "for (; x; ) {\n debugger;\n y || z();\n}\n"], + ["while (x) { debugger; if (y) continue; else z(); w(); }", "for (; x; ) {\n debugger;\n y || (z(), w());\n}\n"], + ["while (x) { if (y) continue; function y() {} }", "for (;x; )\n ;\n"], // bun: strict mode + ["while (x) { if (y) continue; let y }", "for (; x; ) {\n if (y) continue;\n let y;\n}\n"], + ["while (x) { if (y) continue; var y }", "for (;x; )\n ;\nvar y;\n"], // bun: relocates var +]); + +check("TestMangleBlock", [ + ["while(1) { while (1) {} }", "for (; ; )\n for (; ; )\n ;\n"], + ["while(1) { const x = y; }", "for (; ; ) {\n const x = y;\n}\n"], + ["while(1) { let x; }", "for (; ; ) {\n let x;\n}\n"], + ["while(1) { var x; }", "for (;; )\n ;\nvar x;\n"], // bun: relocates var + ["while(1) { class X {} }", "for (; ; ) {\n class X {\n }\n}\n"], + ["while(1) { function x() {} }", "for (;; ) {\n let x = function() {};\n}\n"], // bun: strict mode + ["while(1) { function* x() {} }", "for (; ; ) {\n function* x() {\n }\n}\n"], + ["while(1) { async function x() {} }", "for (; ; ) {\n async function x() {\n }\n}\n"], + ["while(1) { async function* x() {} }", "for (; ; ) {\n async function* x() {\n }\n}\n"], +]); + +check("TestMangleIf", [ + ["1 ? a() : b()", "a();\n"], + ["0 ? a() : b()", "b();\n"], + ["a ? a : b", "a || b;\n"], + ["a ? b : a", "a && b;\n"], + ["a.x ? a.x : b", "a.x ? a.x : b;\n"], + ["a.x ? b : a.x", "a.x ? b : a.x;\n"], + ["a ? b() : c()", "a ? b() : c();\n"], + ["!a ? b() : c()", "a ? c() : b();\n"], + ["!!a ? b() : c()", "a ? b() : c();\n"], + ["!!!a ? b() : c()", "a ? c() : b();\n"], + ["if (1) a(); else b()", "a();\n"], + ["if (0) a(); else b()", "b();\n"], + ["if (a) b(); else c()", "a ? b() : c();\n"], + ["if (!a) b(); else c()", "a ? c() : b();\n"], + ["if (!!a) b(); else c()", "a ? b() : c();\n"], + ["if (!!!a) b(); else c()", "a ? c() : b();\n"], + ["if (1) a()", "a();\n"], + ["if (0) a()", ""], + ["if (a) b()", "a && b();\n"], + ["if (!a) b()", "a || b();\n"], + ["if (!!a) b()", "a && b();\n"], + ["if (!!!a) b()", "a || b();\n"], + ["if (1) {} else a()", ""], + ["if (0) {} else a()", "a();\n"], + ["if (a) {} else b()", "a || b();\n"], + ["if (!a) {} else b()", "a && b();\n"], + ["if (!!a) {} else b()", "a || b();\n"], + ["if (!!!a) {} else b()", "a && b();\n"], + ["if (a) {} else throw b", "if (!a)\n throw b;\n"], + ["if (!a) {} else throw b", "if (a)\n throw b;\n"], + ["a(); if (b) throw c", "if (a(), b) throw c;\n"], + ["if (a) if (b) throw c", "if (a && b) throw c;\n"], + ["if (true) { let a = b; if (c) throw d }", "{\n let a = b;\n if (c) throw d;\n}\n"], + ["if (true) { if (a) throw b; if (c) throw d }", "if (a) throw b;\nif (c) throw d;\n"], + ["if (false) throw a; else { let b = c; if (d) throw e }", "{\n let b = c;\n if (d) throw e;\n}\n"], + ["if (false) throw a; else { if (b) throw c; if (d) throw e }", "if (b) throw c;\nif (d) throw e;\n"], + [ + "if (a) { if (b) throw c; else { let d = e; if (f) throw g } }", + "if (a) {\n if (b) throw c;\n {\n let d = e;\n if (f) throw g;\n }\n}\n", + ], + [ + "if (a) { if (b) throw c; else if (d) throw e; else if (f) throw g }", + "if (a) {\n if (b) throw c;\n if (d) throw e;\n if (f) throw g;\n}\n", + ], + ["a = b ? true : false", "a = !!b;\n"], + ["a = b ? false : true", "a = !b;\n"], + ["a = !b ? true : false", "a = !b;\n"], + ["a = !b ? false : true", "a = !!b;\n"], + ["a = b == c ? true : false", "a = b == c;\n"], + ["a = b != c ? true : false", "a = b != c;\n"], + ["a = b === c ? true : false", "a = b === c;\n"], + ["a = b !== c ? true : false", "a = b !== c;\n"], + ["a ? b(c) : b(d)", "a ? b(c) : b(d);\n"], + ["let a; a ? b(c) : b(d)", "let a;\na ? b(c) : b(d);\n"], + ["let a, b; a ? b(c) : b(d)", "let a, b;\nb(a ? c : d);\n"], + ["let a, b; a ? b(c, 0) : b(d)", "let a, b;\na ? b(c, 0) : b(d);\n"], + ["let a, b; a ? b(c) : b(d, 0)", "let a, b;\na ? b(c) : b(d, 0);\n"], + ["let a, b; a ? b(c, 0) : b(d, 1)", "let a, b;\na ? b(c, 0) : b(d, 1);\n"], + ["let a, b; a ? b(c, 0) : b(d, 0)", "let a, b;\nb(a ? c : d, 0);\n"], + ["let a, b; a ? b(...c) : b(d)", "let a, b;\na ? b(...c) : b(d);\n"], + ["let a, b; a ? b(c) : b(...d)", "let a, b;\na ? b(c) : b(...d);\n"], + ["let a, b; a ? b(...c) : b(...d)", "let a, b;\nb(...a ? c : d);\n"], + ["let a, b; a ? b(a) : b(c)", "let a, b;\nb(a || c);\n"], + ["let a, b; a ? b(c) : b(a)", "let a, b;\nb(a && c);\n"], + ["let a, b; a ? b(...a) : b(...c)", "let a, b;\nb(...a || c);\n"], + ["let a, b; a ? b(...c) : b(...a)", "let a, b;\nb(...a && c);\n"], + ["let a; a.x ? b(c) : b(d)", "let a;\na.x ? b(c) : b(d);\n"], + ["let a, b; a.x ? b(c) : b(d)", "let a, b;\na.x ? b(c) : b(d);\n"], + ["let a, b; a ? b.y(c) : b.y(d)", "let a, b;\na ? b.y(c) : b.y(d);\n"], + ["let a, b; a.x ? b.y(c) : b.y(d)", "let a, b;\na.x ? b.y(c) : b.y(d);\n"], + ["a ? b : c ? b : d", "a || c ? b : d;\n"], + ["a ? b ? c : d : d", "a && b ? c : d;\n"], + ["a ? c : (b, c)", "a || b, c;\n"], + ["a ? (b, c) : c", "a && b, c;\n"], + ["a ? c : (b, d)", "a ? c : (b, d);\n"], + ["a ? (b, c) : d", "a ? (b, c) : d;\n"], + ["a ? b || c : c", "a && b || c;\n"], + ["a ? b || c : d", "a ? b || c : d;\n"], + ["a ? b && c : c", "a ? b && c : c;\n"], + ["a ? c : b && c", "(a || b) && c;\n"], + ["a ? c : b && d", "a ? c : b && d;\n"], + ["a ? c : b || c", "a ? c : b || c;\n"], + ["a = b == null ? c : b", "a = b == null ? c : b;\n"], + ["a = b != null ? b : c", "a = b != null ? b : c;\n"], + ["let b; a = b == null ? c : b", "let b;\na = b ?? c;\n"], + ["let b; a = b != null ? b : c", "let b;\na = b ?? c;\n"], + ["let b; a = b == null ? b : c", "let b;\na = b == null ? b : c;\n"], + ["let b; a = b != null ? c : b", "let b;\na = b != null ? c : b;\n"], + ["let b; a = null == b ? c : b", "let b;\na = b ?? c;\n"], + ["let b; a = null != b ? b : c", "let b;\na = b ?? c;\n"], + ["let b; a = null == b ? b : c", "let b;\na = b == null ? b : c;\n"], + ["let b; a = null != b ? c : b", "let b;\na = b != null ? c : b;\n"], + ["let b; a = b.x == null ? c : b.x", "let b;\na = b.x == null ? c : b.x;\n"], + ["let b; a = b.x != null ? b.x : c", "let b;\na = b.x != null ? b.x : c;\n"], + ["let b; a = null == b.x ? c : b.x", "let b;\na = b.x == null ? c : b.x;\n"], + ["let b; a = null != b.x ? b.x : c", "let b;\na = b.x != null ? b.x : c;\n"], + ["let b; a = b === null ? c : b", "let b;\na = b === null ? c : b;\n"], + ["let b; a = b !== null ? b : c", "let b;\na = b !== null ? b : c;\n"], + ["let b; a = null === b ? c : b", "let b;\na = b === null ? c : b;\n"], + ["let b; a = null !== b ? b : c", "let b;\na = b !== null ? b : c;\n"], + ["let b; a = null === b || b === undefined ? c : b", "let b;\na = b ?? c;\n"], + ["let b; a = b !== undefined && b !== null ? b : c", "let b;\na = b ?? c;\n"], + ["a(b ? 0 : 0)", "a((b, 0));\n"], + ["a(b ? +0 : -0)", "a(b ? 0 : -0);\n"], + ["a(b ? +0 : 0)", "a((b, 0));\n"], + ["a(b ? -0 : 0)", "a(b ? -0 : 0);\n"], + ["a ? b : b", "a, b;\n"], + ["let a; a ? b : b", "let a;\nb;\n"], + ["a ? -b : -b", "a, -b;\n"], + ["a ? b.c : b.c", "a, b.c;\n"], + ["a ? b?.c : b?.c", "a, b?.c;\n"], + ["a ? b[c] : b[c]", "a, b[c];\n"], + ["a ? b() : b()", "a, b();\n"], + ["a ? b?.() : b?.()", "a, b?.();\n"], + ["a ? b?.[c] : b?.[c]", "a, b?.[c];\n"], + ["a ? b == c : b == c", "a, b == c;\n"], + ["a ? b.c(d + e[f]) : b.c(d + e[f])", "a, b.c(d + e[f]);\n"], + ["a ? -b : !b", "a ? -b : b;\n"], + ["a ? b() : b(c)", "a ? b() : b(c);\n"], + ["a ? b(c) : b(d)", "a ? b(c) : b(d);\n"], + ["a ? b?.c : b.c", "a ? b?.c : b.c;\n"], + ["a ? b?.() : b()", "a ? b?.() : b();\n"], + ["a ? b?.[c] : b[c]", "a ? b?.[c] : b[c];\n"], + ["a ? b == c : b != c", "a ? b == c : b != c;\n"], + ["a ? b.c(d + e[f]) : b.c(d + e[g])", "a ? b.c(d + e[f]) : b.c(d + e[g]);\n"], + ["(a, b) ? c : d", "a, b ? c : d;\n"], + ["return a && ((b && c) && (d && e))", "return a && b && c && d && e;\n"], + ["return a || ((b || c) || (d || e))", "return a || b || c || d || e;\n"], + ["return a ?? ((b ?? c) ?? (d ?? e))", "return a ?? b ?? c ?? d ?? e;\n"], + ["if (a) if (b) if (c) d", "a && b && c && d;\n"], + ["if (!a) if (!b) if (!c) d", "a || b || c || d;\n"], + ["let a, b, c; return a != null ? a : b != null ? b : c", "let a, b, c;\nreturn a ?? b ?? c;\n"], + ["if (a) return c; if (b) return d;", "if (a) return c;\nif (b) return d;\n"], + ["if (a) return c; if (b) return c;", "if (a || b) return c;\n"], + ["if (a) return c; if (b) return;", "if (a) return c;\nif (b) return;\n"], + ["if (a) return; if (b) return c;", "if (a) return;\nif (b) return c;\n"], + ["if (a) return; if (b) return;", "if (a || b) return;\n"], + ["if (a) throw c; if (b) throw d;", "if (a) throw c;\nif (b) throw d;\n"], + ["if (a) throw c; if (b) throw c;", "if (a || b) throw c;\n"], + ["while (x) { if (a) break; if (b) break; }", "for (; x && !(a || b); )\n ;\n"], + ["while (x) { if (a) continue; if (b) continue; }", "for (; x; )\n a || b;\n"], + ["while (x) { debugger; if (a) break; if (b) break; }", "for (; x; ) {\n debugger;\n if (a || b) break;\n}\n"], + ["while (x) { debugger; if (a) continue; if (b) continue; }", "for (; x; ) {\n debugger;\n a || b;\n}\n"], + [ + "x: while (x) y: while (y) { if (a) break x; if (b) break y; }", + "x: for (; x; ) y: for (; y; ) {\n if (a) break x;\n if (b) break y;\n}\n", + ], + [ + "x: while (x) y: while (y) { if (a) continue x; if (b) continue y; }", + "x: for (; x; ) y: for (; y; ) {\n if (a) continue x;\n if (b) continue y;\n}\n", + ], + [ + "x: while (x) y: while (y) { if (a) break x; if (b) break x; }", + "x: for (; x; ) for (; y; )\n if (a || b) break x;\n", + ], + [ + "x: while (x) y: while (y) { if (a) continue x; if (b) continue x; }", + "x: for (; x; ) for (; y; )\n if (a || b) continue x;\n", + ], + [ + "x: while (x) y: while (y) { if (a) break y; if (b) break y; }", + "for (; x; ) y: for (; y; )\n if (a || b) break y;\n", + ], + [ + "x: while (x) y: while (y) { if (a) continue y; if (b) continue y; }", + "for (; x; ) y: for (; y; )\n if (a || b) continue y;\n", + ], + ["if (x ? y : 0) foo()", "x && y && foo();\n"], + ["if (x ? y : 1) foo()", "(!x || y) && foo();\n"], + ["if (x ? 0 : y) foo()", "!x && y && foo();\n"], + ["if (x ? 1 : y) foo()", "(x || y) && foo();\n"], + ["if (x ? y : 0) ; else foo()", "x && y || foo();\n"], + ["if (x ? y : 1) ; else foo()", "!x || y || foo();\n"], + ["if (x ? 0 : y) ; else foo()", "!x && y || foo();\n"], + ["if (x ? 1 : y) ; else foo()", "x || y || foo();\n"], + ["(x ? y : 0) && foo();", "x && y && foo();\n"], + ["(x ? y : 1) && foo();", "(!x || y) && foo();\n"], + ["(x ? 0 : y) && foo();", "!x && y && foo();\n"], + ["(x ? 1 : y) && foo();", "(x || y) && foo();\n"], + ["(x ? y : 0) || foo();", "x && y || foo();\n"], + ["(x ? y : 1) || foo();", "!x || y || foo();\n"], + ["(x ? 0 : y) || foo();", "!x && y || foo();\n"], + ["(x ? 1 : y) || foo();", "x || y || foo();\n"], + ["if (!!a || !!b) throw 0", "if (a || b) throw 0;\n"], + ["if (!!a && !!b) throw 0", "if (a && b) throw 0;\n"], + ["if (!!a ? !!b : !!c) throw 0", "if (a ? b : c) throw 0;\n"], + ["if ((a + b) !== 0) throw 0", "if (a + b !== 0) throw 0;\n"], + ["if ((a | b) !== 0) throw 0", "if ((a | b) !== 0) throw 0;\n"], + ["if ((a & b) !== 0) throw 0", "if ((a & b) !== 0) throw 0;\n"], + ["if ((a ^ b) !== 0) throw 0", "if ((a ^ b) !== 0) throw 0;\n"], + ["if ((a << b) !== 0) throw 0", "if (a << b !== 0) throw 0;\n"], + ["if ((a >> b) !== 0) throw 0", "if (a >> b !== 0) throw 0;\n"], + ["if ((a >>> b) !== 0) throw 0", "if (a >>> b) throw 0;\n"], + ["if (+a !== 0) throw 0", "if (+a != 0) throw 0;\n"], + ["if (~a !== 0) throw 0", "if (~a !== 0) throw 0;\n"], + ["if (0 != (a + b)) throw 0", "if (a + b != 0) throw 0;\n"], + ["if (0 != (a | b)) throw 0", "if ((a | b) != 0) throw 0;\n"], + ["if (0 != (a & b)) throw 0", "if ((a & b) != 0) throw 0;\n"], + ["if (0 != (a ^ b)) throw 0", "if ((a ^ b) != 0) throw 0;\n"], + ["if (0 != (a << b)) throw 0", "if (a << b != 0) throw 0;\n"], + ["if (0 != (a >> b)) throw 0", "if (a >> b != 0) throw 0;\n"], + ["if (0 != (a >>> b)) throw 0", "if (a >>> b) throw 0;\n"], + ["if (0 != +a) throw 0", "if (+a != 0) throw 0;\n"], + ["if (0 != ~a) throw 0", "if (~a != 0) throw 0;\n"], + ["if ((a + b) === 0) throw 0", "if (a + b === 0) throw 0;\n"], + ["if ((a | b) === 0) throw 0", "if ((a | b) === 0) throw 0;\n"], + ["if ((a & b) === 0) throw 0", "if ((a & b) === 0) throw 0;\n"], + ["if ((a ^ b) === 0) throw 0", "if ((a ^ b) === 0) throw 0;\n"], + ["if ((a << b) === 0) throw 0", "if (a << b === 0) throw 0;\n"], + ["if ((a >> b) === 0) throw 0", "if (a >> b === 0) throw 0;\n"], + ["if ((a >>> b) === 0) throw 0", "if (!(a >>> b)) throw 0;\n"], + ["if (+a === 0) throw 0", "if (+a == 0) throw 0;\n"], + ["if (~a === 0) throw 0", "if (~a === 0) throw 0;\n"], + ["if (0 == (a + b)) throw 0", "if (a + b == 0) throw 0;\n"], + ["if (0 == (a | b)) throw 0", "if ((a | b) == 0) throw 0;\n"], + ["if (0 == (a & b)) throw 0", "if ((a & b) == 0) throw 0;\n"], + ["if (0 == (a ^ b)) throw 0", "if ((a ^ b) == 0) throw 0;\n"], + ["if (0 == (a << b)) throw 0", "if (a << b == 0) throw 0;\n"], + ["if (0 == (a >> b)) throw 0", "if (a >> b == 0) throw 0;\n"], + ["if (0 == (a >>> b)) throw 0", "if (!(a >>> b)) throw 0;\n"], + ["if (0 == +a) throw 0", "if (+a == 0) throw 0;\n"], + ["if (0 == ~a) throw 0", "if (~a == 0) throw 0;\n"], +]); + +check("TestMangleWrapToAvoidAmbiguousElse", [ + ["if (a) { if (b) return c } else return d", "if (a) {\n if (b) return c;\n} else return d;\n"], + [ + "if (a) while (1) { if (b) return c } else return d", + "if (a) {\n for (; ; )\n if (b) return c;\n} else return d;\n", + ], + [ + "if (a) for (;;) { if (b) return c } else return d", + "if (a) {\n for (; ; )\n if (b) return c;\n} else return d;\n", + ], + [ + "if (a) for (x in y) { if (b) return c } else return d", + "if (a) {\n for (x in y)\n if (b) return c;\n} else return d;\n", + ], + [ + "if (a) for (x of y) { if (b) return c } else return d", + "if (a) {\n for (x of y)\n if (b) return c;\n} else return d;\n", + ], + [ + "if (a) with (x) { if (b) return c } else return d", + "if (a) {\n with (x)\n if (b) return c;\n} else return d;\n", + ], + ["if (a) x: { if (b) break x } else return c", "if (a) {\n x:\n if (b) break x;\n} else return c;\n"], +]); + +check("TestMangleOptionalChain", [ + ["let a; return a != null ? a.b : undefined", "let a;\nreturn a?.b;\n"], + ["let a; return a != null ? a[b] : undefined", "let a;\nreturn a?.[b];\n"], + ["let a; return a != null ? a(b) : undefined", "let a;\nreturn a?.(b);\n"], + ["let a; return a == null ? undefined : a.b", "let a;\nreturn a?.b;\n"], + ["let a; return a == null ? undefined : a[b]", "let a;\nreturn a?.[b];\n"], + ["let a; return a == null ? undefined : a(b)", "let a;\nreturn a?.(b);\n"], + ["let a; return null != a ? a.b : undefined", "let a;\nreturn a?.b;\n"], + ["let a; return null != a ? a[b] : undefined", "let a;\nreturn a?.[b];\n"], + ["let a; return null != a ? a(b) : undefined", "let a;\nreturn a?.(b);\n"], + ["let a; return null == a ? undefined : a.b", "let a;\nreturn a?.b;\n"], + ["let a; return null == a ? undefined : a[b]", "let a;\nreturn a?.[b];\n"], + ["let a; return null == a ? undefined : a(b)", "let a;\nreturn a?.(b);\n"], + ["return a != null ? a.b : undefined", "return a != null ? a.b : void 0;\n"], + ["let a; return a != null ? a.b : null", "let a;\nreturn a != null ? a.b : null;\n"], + ["let a; return a != null ? b.a : undefined", "let a;\nreturn a != null ? b.a : void 0;\n"], + ["let a; return a != 0 ? a.b : undefined", "let a;\nreturn a != 0 ? a.b : void 0;\n"], + ["let a; return a !== null ? a.b : undefined", "let a;\nreturn a !== null ? a.b : void 0;\n"], + ["let a; return a != undefined ? a.b : undefined", "let a;\nreturn a?.b;\n"], + ["let a; return a != null ? a?.b : undefined", "let a;\nreturn a?.b;\n"], + ["let a; return a != null ? a.b.c[d](e) : undefined", "let a;\nreturn a?.b.c[d](e);\n"], + ["let a; return a != null ? a?.b.c[d](e) : undefined", "let a;\nreturn a?.b.c[d](e);\n"], + ["let a; return a != null ? a.b.c?.[d](e) : undefined", "let a;\nreturn a?.b.c?.[d](e);\n"], + ["let a; return a != null ? a?.b.c?.[d](e) : undefined", "let a;\nreturn a?.b.c?.[d](e);\n"], + ["a != null && a.b()", "a?.b();\n"], + ["a == null || a.b()", "a?.b();\n"], + ["null != a && a.b()", "a?.b();\n"], + ["null == a || a.b()", "a?.b();\n"], + ["a == null && a.b()", "a == null && a.b();\n"], + ["a != null || a.b()", "a != null || a.b();\n"], + ["null == a && a.b()", "a == null && a.b();\n"], + ["null != a || a.b()", "a != null || a.b();\n"], + ["x = a != null && a.b()", "x = a != null && a.b();\n"], + ["x = a == null || a.b()", "x = a == null || a.b();\n"], + ["if (a != null) a.b()", "a?.b();\n"], + ["if (a == null) ; else a.b()", "a?.b();\n"], + ["if (a == null) a.b()", "a == null && a.b();\n"], + ["if (a != null) ; else a.b()", "a != null || a.b();\n"], +]); + +check("TestMangleReturn", [ + ["function foo() { x(); return; }", "function foo() {\n x();\n}\n"], + ["let foo = function() { x(); return; }", "let foo = function() {\n x();\n};\n"], + ["let foo = () => { x(); return; }", "let foo = () => {\n x();\n};\n"], + ["function foo() { x(); return y; }", "function foo() {\n return x(), y;\n}\n"], + ["let foo = function() { x(); return y; }", "let foo = function() {\n return x(), y;\n};\n"], + ["let foo = () => { x(); return y; }", "let foo = () => (x(), y);\n"], + ["x(); return;", "x();\nreturn;\n"], + [ + "function foo() { a = b; if (a) return a; if (b) c = b; return c; }", + "function foo() {\n return a = b, a || (b && (c = b), c);\n}\n", + ], + [ + "function foo() { a = b; if (a) return; if (b) c = b; return c; }", + "function foo() {\n if (a = b, !a)\n return b && (c = b), c;\n}\n", + ], + ["function foo() { if (!a) return b; return c; }", "function foo() {\n return a ? c : b;\n}\n"], + ["if (1) return a(); else return b()", "return a();\n"], + ["if (0) return a(); else return b()", "return b();\n"], + ["if (a) return b(); else return c()", "return a ? b() : c();\n"], + ["if (!a) return b(); else return c()", "return a ? c() : b();\n"], + ["if (!!a) return b(); else return c()", "return a ? b() : c();\n"], + ["if (!!!a) return b(); else return c()", "return a ? c() : b();\n"], + ["if (1) return a(); return b()", "return a();\n"], + ["if (0) return a(); return b()", "return b();\n"], + ["if (a) return b(); return c()", "return a ? b() : c();\n"], + ["if (!a) return b(); return c()", "return a ? c() : b();\n"], + ["if (!!a) return b(); return c()", "return a ? b() : c();\n"], + ["if (!!!a) return b(); return c()", "return a ? c() : b();\n"], + ["if (a) return b; else return c; return d;\n", "return a ? b : c;\n"], + ["function x() { if (y) return; z(); }", "function x() {\n y || z();\n}\n"], + ["function x() { if (y) return; else z(); w(); }", "function x() {\n y || (z(), w());\n}\n"], + ["function x() { t(); if (y) return; z(); }", "function x() {\n t(), !y && z();\n}\n"], + ["function x() { t(); if (y) return; else z(); w(); }", "function x() {\n t(), !y && (z(), w());\n}\n"], + ["function x() { debugger; if (y) return; z(); }", "function x() {\n debugger;\n y || z();\n}\n"], + ["function x() { debugger; if (y) return; else z(); w(); }", "function x() {\n debugger;\n y || (z(), w());\n}\n"], + ["function x() { if (y) { if (z) return; } }", "function x() {\n y && z;\n}\n"], + [ + "function x() { if (y) { if (z) return; w(); } }", + "function x() {\n if (y) {\n if (z) return;\n w();\n }\n}\n", + ], + ["function foo(x) { if (!x.y) {} else return x }", "function foo(x) {\n if (x.y)\n return x;\n}\n"], + ["function foo(x) { if (!x.y) return undefined; return x }", "function foo(x) {\n if (x.y)\n return x;\n}\n"], + ["function x() { if (y) return; function y() {} }", "function x() {\n if (y) return;\n function y() {\n }\n}\n"], + ["function x() { if (y) return; let y }", "function x() {\n if (y) return;\n let y;\n}\n"], + ["function x() { if (y) return; var y }", "function x() {\n if (!y)\n var y;\n}\n"], +]); + +check("TestMangleThrow", [ + [ + "function foo() { a = b; if (a) throw a; if (b) c = b; throw c; }", + "function foo() {\n throw a = b, a || (b && (c = b), c);\n}\n", + ], + ["function foo() { if (!a) throw b; throw c; }", "function foo() {\n throw a ? c : b;\n}\n"], + ["if (1) throw a(); else throw b()", "throw a();\n"], + ["if (0) throw a(); else throw b()", "throw b();\n"], + ["if (a) throw b(); else throw c()", "throw a ? b() : c();\n"], + ["if (!a) throw b(); else throw c()", "throw a ? c() : b();\n"], + ["if (!!a) throw b(); else throw c()", "throw a ? b() : c();\n"], + ["if (!!!a) throw b(); else throw c()", "throw a ? c() : b();\n"], + ["if (1) throw a(); throw b()", "throw a();\n"], + ["if (0) throw a(); throw b()", "throw b();\n"], + ["if (a) throw b(); throw c()", "throw a ? b() : c();\n"], + ["if (!a) throw b(); throw c()", "throw a ? c() : b();\n"], + ["if (!!a) throw b(); throw c()", "throw a ? b() : c();\n"], + ["if (!!!a) throw b(); throw c()", "throw a ? c() : b();\n"], +]); + +check("TestMangleNestedLogical", [ + ["(a && b) && c", "a && b && c;\n"], + ["a && (b && c)", "a && b && c;\n"], + ["(a || b) && c", "(a || b) && c;\n"], + ["a && (b || c)", "a && (b || c);\n"], + ["(a || b) || c", "a || b || c;\n"], + ["a || (b || c)", "a || b || c;\n"], + ["(a && b) || c", "a && b || c;\n"], + ["a || (b && c)", "a || b && c;\n"], +]); + +check("TestMangleEquals", [ + ["return typeof x === y", "return typeof x === y;\n"], + ["return typeof x !== y", "return typeof x !== y;\n"], + ["return y === typeof x", "return y === typeof x;\n"], + ["return y !== typeof x", "return y !== typeof x;\n"], + ["return typeof x === 'string'", 'return typeof x == "string";\n'], + ["return typeof x !== 'string'", 'return typeof x != "string";\n'], + ["return 'string' === typeof x", 'return typeof x == "string";\n'], + ["return 'string' !== typeof x", 'return typeof x != "string";\n'], + ["return a === 0", "return a === 0;\n"], + ["return a !== 0", "return a !== 0;\n"], + ["return +a === 0", "return +a == 0;\n"], + ["return +a !== 0", "return +a != 0;\n"], + ["return -a === 0", "return -a === 0;\n"], + ["return -a !== 0", "return -a !== 0;\n"], + ["return a === ''", 'return a === "";\n'], + ["return a !== ''", 'return a !== "";\n'], + ["return (a + '!') === 'a!'", 'return a + "!" == "a!";\n'], + ["return (a + '!') !== 'a!'", 'return a + "!" != "a!";\n'], + ["return (a += '!') === 'a!'", 'return (a += "!") == "a!";\n'], + ["return (a += '!') !== 'a!'", 'return (a += "!") != "a!";\n'], + ["return a === false", "return a === false;\n"], + ["return a === true", "return a === true;\n"], + ["return a !== false", "return a !== false;\n"], + ["return a !== true", "return a !== true;\n"], + ["return a === !b", "return a === !b;\n"], + ["return a === !b", "return a === !b;\n"], + ["return a !== !b", "return a !== !b;\n"], + ["return a !== !b", "return a !== !b;\n"], + ["return !a === !b", "return !a == !b;\n"], + ["return !a === !b", "return !a == !b;\n"], + ["return !a !== !b", "return !a != !b;\n"], + ["return !a !== !b", "return !a != !b;\n"], + ["return (a -= 1n) !== -1", "return (a -= 1n) !== -1;\n"], + ["return (a *= 1n) !== -1", "return (a *= 1n) !== -1;\n"], + ["return (a **= 1n) !== -1", "return (a **= 1n) !== -1;\n"], + ["return (a /= 1n) !== -1", "return (a /= 1n) !== -1;\n"], + ["return (a %= 1n) !== -1", "return (a %= 1n) !== -1;\n"], + ["return (a &= 1n) !== -1", "return (a &= 1n) !== -1;\n"], + ["return (a |= 1n) !== -1", "return (a |= 1n) !== -1;\n"], + ["return (a ^= 1n) !== -1", "return (a ^= 1n) !== -1;\n"], +]); + +check("TestMangleEqualsUndefined", [ + ["return a === void 0", "return a === void 0;\n"], + ["return a !== void 0", "return a !== void 0;\n"], + ["return void 0 === a", "return a === void 0;\n"], + ["return void 0 !== a", "return a !== void 0;\n"], + ["return a == void 0", "return a == null;\n"], + ["return a != void 0", "return a != null;\n"], + ["return void 0 == a", "return a == null;\n"], + ["return void 0 != a", "return a != null;\n"], + ["return a === null || a === undefined", "return a == null;\n"], + ["return a === null || a !== undefined", "return a === null || a !== void 0;\n"], + ["return a !== null || a === undefined", "return a !== null || a === void 0;\n"], + ["return a === null && a === undefined", "return a === null && a === void 0;\n"], + ["return a.x === null || a.x === undefined", "return a.x === null || a.x === void 0;\n"], + ["return a === undefined || a === null", "return a == null;\n"], + ["return a === undefined || a !== null", "return a === void 0 || a !== null;\n"], + ["return a !== undefined || a === null", "return a !== void 0 || a === null;\n"], + ["return a === undefined && a === null", "return a === void 0 && a === null;\n"], + ["return a.x === undefined || a.x === null", "return a.x === void 0 || a.x === null;\n"], + ["return a !== null && a !== undefined", "return a != null;\n"], + ["return a !== null && a === undefined", "return a !== null && a === void 0;\n"], + ["return a === null && a !== undefined", "return a === null && a !== void 0;\n"], + ["return a !== null || a !== undefined", "return a !== null || a !== void 0;\n"], + ["return a.x !== null && a.x !== undefined", "return a.x !== null && a.x !== void 0;\n"], + ["return a !== undefined && a !== null", "return a != null;\n"], + ["return a !== undefined && a === null", "return a !== void 0 && a === null;\n"], + ["return a === undefined && a !== null", "return a === void 0 && a !== null;\n"], + ["return a !== undefined || a !== null", "return a !== void 0 || a !== null;\n"], + ["return a.x !== undefined && a.x !== null", "return a.x !== void 0 && a.x !== null;\n"], +]); + +check("TestMangleTypeofEqualsUndefined", [ + ["return typeof x !== 'undefined'", 'return typeof x < "u";\n'], + ["return typeof x != 'undefined'", 'return typeof x < "u";\n'], + ["return 'undefined' !== typeof x", 'return typeof x < "u";\n'], + ["return 'undefined' != typeof x", 'return typeof x < "u";\n'], + ["return typeof x === 'undefined'", 'return typeof x > "u";\n'], + ["return typeof x == 'undefined'", 'return typeof x > "u";\n'], + ["return 'undefined' === typeof x", 'return typeof x > "u";\n'], + ["return 'undefined' == typeof x", 'return typeof x > "u";\n'], +]); + +check("TestMangleCatch", [ + ["try { throw 0 } catch (e) { console.log(0) }", "try {\n throw 0;\n} catch {\n console.log(0);\n}\n"], + ["try { throw 0 } catch (e) { console.log(0, e) }", "try {\n throw 0;\n} catch (e) {\n console.log(0, e);\n}\n"], + ["try { throw 0 } catch (e) { 0 && console.log(0, e) }", "try {\n throw 0;\n} catch {\n}\n"], + ["try { thrower() } catch ([a]) { console.log(0) }", "try {\n thrower();\n} catch ([a]) {\n console.log(0);\n}\n"], + [ + "try { thrower() } catch ({ a }) { console.log(0) }", + "try {\n thrower();\n} catch ({ a }) {\n console.log(0);\n}\n", + ], + [ + "try { throw 1 } catch (x) { y(x); var x = 2; y(x) }", + "try {\n throw 1;\n} catch (x) {\n y(x), x = 2, y(x);\n}\nvar x;\n", + ], // bun: relocates var + ["try { throw 1 } catch (x) { var x = 2; y(x) }", "try {\n throw 1;\n} catch (x) {\n x = 2, y(x);\n}\nvar x;\n"], // bun: relocates var + ["try { throw 1 } catch (x) { var x = 2 }", "try {\n throw 1;\n} catch (x) {\n x = 2;\n}\nvar x;\n"], // bun: relocates var + ["try { throw 1 } catch (x) { eval('x') }", 'try {\n throw 1;\n} catch (x) {\n eval("x");\n}\n'], + ["if (y) try { throw 1 } catch (x) {} else eval('x')", 'if (y) try {\n throw 1;\n} catch {\n}\nelse eval("x");\n'], +]); + +check("TestMangleTry", [ + ["try { throw 0 } catch (e) { foo() }", "try {\n throw 0;\n} catch {\n foo();\n}\n"], + ["try {} catch (e) { var foo }", "var foo;\n"], // bun: relocates var + ["try {} catch (e) { foo() }", ""], + ["try {} catch (e) { foo() } finally {}", ""], + ["try {} finally { foo() }", "foo();\n"], + ["try {} catch (e) { foo() } finally { bar() }", "bar();\n"], + ["try {} finally { var x = foo() }", "x = foo();\nvar x;\n"], // bun: relocates var + ["try {} catch (e) { foo() } finally { var x = bar() }", "x = bar();\nvar x;\n"], // bun: relocates var + ["try {} finally { let x = foo() }", "{\n let x = foo();\n}\n"], + ["try {} catch (e) { foo() } finally { let x = bar() }", "{\n let x = bar();\n}\n"], + ["try { foo() } catch {}", "try {\n foo();\n} catch {\n}\n"], + ["try { foo() } catch {} finally {}", "try {\n foo();\n} catch {\n}\n"], + ["try { foo() } finally {}", "foo();\n"], + ["try { var x = foo() } catch {}", "try {\n x = foo();\n} catch {}\nvar x;\n"], // bun: relocates var + ["try { var x = foo() } catch {} finally {}", "try {\n x = foo();\n} catch {}\nvar x;\n"], // bun: relocates var + ["try { var x = foo() } finally {}", "x = foo();\nvar x;\n"], // bun: relocates var + ["try { let x = foo() } catch {}", "try {\n let x = foo();\n} catch {\n}\n"], + ["try { let x = foo() } catch {} finally {}", "try {\n let x = foo();\n} catch {\n}\n"], + ["try { let x = foo() } finally {}", "{\n let x = foo();\n}\n"], + ["x: try { while (true) ; break x } catch {}", "x: try {\n for (; ; ) ;\n break x;\n} catch {\n}\n"], + [ + "d: { e: { try { while (1) { break d } } catch { break e } } }", + "d:\n e:\n try {\n for (; ; )\n break d;\n } catch {\n break e;\n }\n", + ], +]); diff --git a/test/bundler/transpiler/react-compiler-fixtures.test.ts b/test/bundler/transpiler/react-compiler-fixtures.test.ts index a90f5d3ea775..38f9623460cf 100644 --- a/test/bundler/transpiler/react-compiler-fixtures.test.ts +++ b/test/bundler/transpiler/react-compiler-fixtures.test.ts @@ -222,15 +222,6 @@ const TODO: Record = Object.assign(Object.create(null), {}); // `__proto__: null`: a fixture named "constructor" exists. const MINIFY_SYNTAX_DIVERGENCE: Record = Object.assign(Object.create(null), { - // minify.syntax drops the unreferenced name from `function formatWithUnit() {}`, - // making it eligible for the compiler's function-outlining pass (upstream only - // outlines anonymous function expressions). The outlined module-level function - // is referentially stable with no memo slot, so `_c(1)` is legitimately gone. - // This is better output, not a bailout. - "new-mutability/repro-destructure-from-prop-with-default-value": { - compiledFunctions: 0, - reason: "anonymized function expression becomes outlineable; 0 slots needed", - }, // minify.syntax constant-folds `const x = 0` into the `useMemo` dependency // array, turning `[x]` into `[0]`; the manual-memoization parser only accepts // identifier / member expressions as dependencies, so it errors. This is a