From 026c21ec68b8c32a2768231c4e89e7f618a77dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Sat, 7 Aug 2021 17:27:52 +0900 Subject: [PATCH] feat(es/minifier): Make minifier parallel (#2009) swc_ecma_minifier: - Introduce `bundle` mode, which can be used to parallelize processing of bundled files. - Add a function analyzer that checks if the function references something from the outer scope. - Split out parellsizable passes. - Split `optimzer` into pure / non-pure. - Run pure optimizations in parallel. --- Cargo.lock | 5 +- Cargo.toml | 8 + ecmascript/minifier/Cargo.toml | 3 +- ecmascript/minifier/src/analyzer/ctx.rs | 3 + ecmascript/minifier/src/analyzer/mod.rs | 57 +- ecmascript/minifier/src/compress/mod.rs | 243 +++++-- .../minifier/src/compress/optimize/bools.rs | 623 +--------------- .../src/compress/optimize/collapse_vars.rs | 324 --------- .../src/compress/optimize/computed_props.rs | 41 -- .../src/compress/optimize/conditionals.rs | 189 +---- .../src/compress/optimize/evaluate.rs | 602 +--------------- .../minifier/src/compress/optimize/fns.rs | 2 +- .../src/compress/optimize/if_return.rs | 15 +- .../src/compress/optimize/join_vars.rs | 2 +- .../minifier/src/compress/optimize/loops.rs | 104 --- .../minifier/src/compress/optimize/misc.rs | 34 - .../minifier/src/compress/optimize/mod.rs | 180 +---- .../minifier/src/compress/optimize/ops.rs | 405 +---------- .../src/compress/optimize/sequences.rs | 239 +----- .../minifier/src/compress/optimize/strings.rs | 285 -------- .../minifier/src/compress/optimize/unused.rs | 78 -- .../minifier/src/compress/optimize/util.rs | 64 -- .../src/compress/{optimize => pure}/arrows.rs | 30 +- .../minifier/src/compress/pure/bools.rs | 681 ++++++++++++++++++ .../minifier/src/compress/pure/conds.rs | 184 +++++ ecmascript/minifier/src/compress/pure/ctx.rs | 56 ++ .../minifier/src/compress/pure/dead_code.rs | 85 +++ .../minifier/src/compress/pure/evaluate.rs | 465 ++++++++++++ .../minifier/src/compress/pure/loops.rs | 110 +++ ecmascript/minifier/src/compress/pure/misc.rs | 26 + ecmascript/minifier/src/compress/pure/mod.rs | 420 +++++++++++ .../compress/{optimize => pure}/numbers.rs | 4 +- .../compress/{optimize => pure}/properties.rs | 71 +- .../minifier/src/compress/pure/sequences.rs | 218 ++++++ .../minifier/src/compress/pure/strings.rs | 291 ++++++++ .../compress/{optimize => pure}/unsafes.rs | 6 +- ecmascript/minifier/src/compress/pure/vars.rs | 291 ++++++++ ecmascript/minifier/src/compress/util.rs | 588 +++++++++++++++ ecmascript/minifier/src/hygiene.rs | 135 ---- ecmascript/minifier/src/lib.rs | 18 +- ecmascript/minifier/src/marks.rs | 34 +- ecmascript/minifier/src/metadata/mod.rs | 452 ++++++++++++ ecmascript/minifier/src/metadata/tests.rs | 160 ++++ ecmascript/minifier/src/pass/hygiene/mod.rs | 3 +- .../minifier/src/pass/mangle_names/mod.rs | 4 +- ecmascript/minifier/src/pass/mangle_props.rs | 9 +- ecmascript/minifier/src/pass/precompress.rs | 2 +- ecmascript/minifier/src/util/mod.rs | 1 + ecmascript/minifier/src/util/unit.rs | 63 ++ ecmascript/minifier/tests/compress.rs | 4 + .../fixture/projects/backbone/20/output.js | 4 +- .../fixture/projects/react/8/output.js | 6 +- .../fixture/terser/issue-2435-1/4/output.js | 2 +- .../tests/projects/output/angular-1.2.5.js | 16 +- .../tests/projects/output/backbone-1.1.0.js | 4 +- .../tests/projects/output/jquery-1.9.1.js | 8 +- .../projects/output/jquery.mobile-1.4.2.js | 10 +- .../tests/projects/output/mootools-1.4.5.js | 4 +- .../tests/projects/output/react-17.0.2.js | 26 +- .../tests/projects/output/react-dom-17.0.2.js | 107 ++- .../tests/projects/output/yui-3.12.0.js | 3 +- .../block-scope/do_not_hoist_let/output.js | 4 +- .../compress/issue-281/issue_1595_3/output.js | 1 - 63 files changed, 4595 insertions(+), 3517 deletions(-) delete mode 100644 ecmascript/minifier/src/compress/optimize/computed_props.rs delete mode 100644 ecmascript/minifier/src/compress/optimize/misc.rs rename ecmascript/minifier/src/compress/{optimize => pure}/arrows.rs (81%) create mode 100644 ecmascript/minifier/src/compress/pure/bools.rs create mode 100644 ecmascript/minifier/src/compress/pure/conds.rs create mode 100644 ecmascript/minifier/src/compress/pure/ctx.rs create mode 100644 ecmascript/minifier/src/compress/pure/dead_code.rs create mode 100644 ecmascript/minifier/src/compress/pure/evaluate.rs create mode 100644 ecmascript/minifier/src/compress/pure/loops.rs create mode 100644 ecmascript/minifier/src/compress/pure/misc.rs create mode 100644 ecmascript/minifier/src/compress/pure/mod.rs rename ecmascript/minifier/src/compress/{optimize => pure}/numbers.rs (97%) rename ecmascript/minifier/src/compress/{optimize => pure}/properties.rs (73%) create mode 100644 ecmascript/minifier/src/compress/pure/sequences.rs create mode 100644 ecmascript/minifier/src/compress/pure/strings.rs rename ecmascript/minifier/src/compress/{optimize => pure}/unsafes.rs (84%) create mode 100644 ecmascript/minifier/src/compress/pure/vars.rs create mode 100644 ecmascript/minifier/src/compress/util.rs delete mode 100644 ecmascript/minifier/src/hygiene.rs create mode 100644 ecmascript/minifier/src/metadata/mod.rs create mode 100644 ecmascript/minifier/src/metadata/tests.rs create mode 100644 ecmascript/minifier/src/util/unit.rs diff --git a/Cargo.lock b/Cargo.lock index eeaadd64fbf..8cddfdd693f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2498,7 +2498,7 @@ dependencies = [ [[package]] name = "swc_ecma_minifier" -version = "0.18.1" +version = "0.18.2" dependencies = [ "ansi_term 0.12.1", "anyhow", @@ -2507,6 +2507,7 @@ dependencies = [ "log", "once_cell", "pretty_assertions 0.6.1", + "rayon", "regex", "retain_mut", "serde", @@ -2529,7 +2530,7 @@ dependencies = [ [[package]] name = "swc_ecma_parser" -version = "0.66.0" +version = "0.66.1" dependencies = [ "either", "enum_kind", diff --git a/Cargo.toml b/Cargo.toml index 325532484f4..a6234481c7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,11 @@ lto = true [profile.bench] codegen-units = 1 debug = true + +# Without this, printing diff consumes more than a minute. + +[profile.dev.package.pretty_assertions] +opt-level = 3 + +[profile.test.package.pretty_assertions] +opt-level = 3 diff --git a/ecmascript/minifier/Cargo.toml b/ecmascript/minifier/Cargo.toml index c1390a0843f..e3073de46ca 100644 --- a/ecmascript/minifier/Cargo.toml +++ b/ecmascript/minifier/Cargo.toml @@ -7,7 +7,7 @@ include = ["Cargo.toml", "src/**/*.rs", "src/lists/*.json"] license = "Apache-2.0/MIT" name = "swc_ecma_minifier" repository = "https://github.com/swc-project/swc.git" -version = "0.18.1" +version = "0.18.2" [features] debug = [] @@ -18,6 +18,7 @@ indexmap = "1.7.0" log = "0.4" once_cell = "1.5.2" pretty_assertions = {version = "0.6.1", optional = true} +rayon = "1.5.1" regex = "1.5.3" retain_mut = "0.1.2" serde = {version = "1.0.118", features = ["derive"]} diff --git a/ecmascript/minifier/src/analyzer/ctx.rs b/ecmascript/minifier/src/analyzer/ctx.rs index 2ca49b46f90..233bc1059aa 100644 --- a/ecmascript/minifier/src/analyzer/ctx.rs +++ b/ecmascript/minifier/src/analyzer/ctx.rs @@ -16,6 +16,9 @@ impl UsageAnalyzer { #[derive(Debug, Default, Clone, Copy)] pub(super) struct Ctx { + /// See [crate::marks::Marks] + pub skip_standalone: bool, + pub var_decl_kind_of_pat: Option, pub in_var_decl_with_no_side_effect_for_member_access: bool, diff --git a/ecmascript/minifier/src/analyzer/mod.rs b/ecmascript/minifier/src/analyzer/mod.rs index 212498f2e68..5323f84f0a1 100644 --- a/ecmascript/minifier/src/analyzer/mod.rs +++ b/ecmascript/minifier/src/analyzer/mod.rs @@ -23,7 +23,9 @@ mod ctx; /// TODO: Track assignments to variables via `arguments`. /// TODO: Scope-local. (Including block) -pub(crate) fn analyze(n: &N, marks: Marks) -> ProgramData +/// +/// If `marks` is [None], markers are ignored. +pub(crate) fn analyze(n: &N, marks: Option) -> ProgramData where N: VisitWith, { @@ -237,7 +239,7 @@ impl ProgramData { #[derive(Debug)] pub(crate) struct UsageAnalyzer { data: ProgramData, - marks: Marks, + marks: Option, scope: ScopeData, ctx: Ctx, } @@ -415,7 +417,11 @@ impl Visit for UsageAnalyzer { } fn visit_call_expr(&mut self, n: &CallExpr, _: &dyn Node) { - let inline_prevented = self.ctx.inline_prevented || n.span.has_mark(self.marks.noinline); + let inline_prevented = self.ctx.inline_prevented + || self + .marks + .map(|marks| n.span.has_mark(marks.noinline)) + .unwrap_or_default(); { let ctx = Ctx { @@ -588,18 +594,35 @@ impl Visit for UsageAnalyzer { fn visit_function(&mut self, n: &Function, _: &dyn Node) { n.decorators.visit_with(n, self); - self.with_child(n.span.ctxt, ScopeKind::Fn, |child| { - n.params.visit_with(n, child); + let is_standalone = self + .marks + .map(|marks| n.span.has_mark(marks.standalone)) + .unwrap_or_default(); - match &n.body { - Some(body) => { - // We use visit_children_with instead of visit_with to bypass block scope - // handler. - body.visit_children_with(child); + // We don't dig into standalone function, as it does not share any variable with + // outer scope. + if self.ctx.skip_standalone && is_standalone { + return; + } + + let ctx = Ctx { + skip_standalone: self.ctx.skip_standalone || is_standalone, + ..self.ctx + }; + + self.with_ctx(ctx) + .with_child(n.span.ctxt, ScopeKind::Fn, |child| { + n.params.visit_with(n, child); + + match &n.body { + Some(body) => { + // We use visit_children_with instead of visit_with to bypass block scope + // handler. + body.visit_children_with(child); + } + None => {} } - None => {} - } - }) + }) } fn visit_if_stmt(&mut self, n: &IfStmt, _: &dyn Node) { @@ -665,6 +688,14 @@ impl Visit for UsageAnalyzer { } } + fn visit_module(&mut self, n: &Module, _: &dyn Node) { + let ctx = Ctx { + skip_standalone: true, + ..self.ctx + }; + n.visit_children_with(&mut *self.with_ctx(ctx)) + } + fn visit_named_export(&mut self, n: &NamedExport, _: &dyn Node) { if n.src.is_some() { return; diff --git a/ecmascript/minifier/src/compress/mod.rs b/ecmascript/minifier/src/compress/mod.rs index e1b4d2cfa86..3d64a532766 100644 --- a/ecmascript/minifier/src/compress/mod.rs +++ b/ecmascript/minifier/src/compress/mod.rs @@ -4,45 +4,50 @@ use self::optimize::optimizer; use self::optimize::OptimizerState; use crate::analyzer::analyze; use crate::analyzer::ProgramData; +use crate::analyzer::UsageAnalyzer; use crate::compress::hoist_decls::decl_hoister; +use crate::compress::pure::pure_optimizer; use crate::debug::dump; use crate::debug::invoke; use crate::marks::Marks; use crate::option::CompressOptions; use crate::util::now; +use crate::util::unit::CompileUnit; use crate::util::Optional; +use crate::MAX_PAR_DEPTH; #[cfg(feature = "pretty_assertions")] use pretty_assertions::assert_eq; +use rayon::prelude::*; use std::borrow::Cow; use std::fmt; use std::fmt::Debug; use std::fmt::Display; use std::fmt::Formatter; +use std::thread; use std::time::Instant; +use swc_common::chain; use swc_common::pass::CompilerPass; -use swc_common::pass::Repeat; use swc_common::pass::Repeated; -use swc_common::sync::Lrc; -use swc_common::{chain, SourceMap}; +use swc_common::Globals; use swc_ecma_ast::*; -use swc_ecma_transforms::fixer; use swc_ecma_transforms::optimization::simplify::dead_branch_remover; use swc_ecma_transforms::optimization::simplify::expr_simplifier; use swc_ecma_transforms::pass::JsPass; -use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::StmtLike; use swc_ecma_visit::as_folder; use swc_ecma_visit::noop_visit_mut_type; -use swc_ecma_visit::FoldWith; use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMutWith; +use swc_ecma_visit::VisitWith; mod drop_console; mod hoist_decls; mod optimize; +mod pure; +mod util; pub(crate) fn compressor<'a>( - cm: Lrc, + globals: &'a Globals, marks: Marks, options: &'a CompressOptions, ) -> impl 'a + JsPass { @@ -51,30 +56,29 @@ pub(crate) fn compressor<'a>( visitor: drop_console(), }; let compressor = Compressor { - cm, + globals, marks, options, changed: false, pass: 0, data: None, optimizer_state: Default::default(), + left_parallel_depth: 0, }; - chain!( - console_remover, - Repeat::new(as_folder(compressor)), - expr_simplifier() - ) + chain!(console_remover, as_folder(compressor), expr_simplifier()) } struct Compressor<'a> { - cm: Lrc, + globals: &'a Globals, marks: Marks, options: &'a CompressOptions, changed: bool, pass: usize, data: Option, optimizer_state: OptimizerState, + /// `0` means we should not create more threads. + left_parallel_depth: u8, } impl CompilerPass for Compressor<'_> { @@ -83,18 +87,6 @@ impl CompilerPass for Compressor<'_> { } } -impl Repeated for Compressor<'_> { - fn changed(&self) -> bool { - self.changed - } - - fn reset(&mut self) { - self.changed = false; - self.pass += 1; - self.data = None; - } -} - impl Compressor<'_> { fn handle_stmt_likes(&mut self, stmts: &mut Vec) where @@ -123,14 +115,108 @@ impl Compressor<'_> { // TODO: drop unused } -} -impl VisitMut for Compressor<'_> { - noop_visit_mut_type!(); + /// Optimize a bundle in a parallel. + fn visit_par(&mut self, nodes: &mut Vec) + where + N: Send + Sync + for<'aa> VisitMutWith>, + { + log::debug!("visit_par(left_depth = {})", self.left_parallel_depth); - fn visit_mut_module(&mut self, n: &mut Module) { - debug_assert!(self.data.is_none()); - self.data = Some(analyze(&*n, self.marks)); + if self.left_parallel_depth == 0 || cfg!(target_arch = "wasm32") { + for node in nodes { + let mut v = Compressor { + globals: self.globals, + marks: self.marks, + options: self.options, + changed: false, + pass: self.pass, + data: None, + optimizer_state: Default::default(), + left_parallel_depth: 0, + }; + node.visit_mut_with(&mut v); + + self.changed |= v.changed; + } + } else { + let results = nodes + .par_iter_mut() + .map(|node| { + swc_common::GLOBALS.set(&self.globals, || { + let mut v = Compressor { + globals: self.globals, + marks: self.marks, + options: self.options, + changed: false, + pass: self.pass, + data: None, + optimizer_state: Default::default(), + left_parallel_depth: self.left_parallel_depth - 1, + }; + node.visit_mut_with(&mut v); + + v.changed + }) + }) + .collect::>(); + + for changed in results { + self.changed |= changed; + } + } + } + + fn optimize_unit_repeatedly(&mut self, n: &mut N) + where + N: CompileUnit + VisitWith + for<'aa> VisitMutWith>, + { + if cfg!(feature = "debug") { + log::debug!( + "Optimizing a compile unit within `{:?}`", + thread::current().name() + ); + } + + { + let data = analyze(&*n, Some(self.marks)); + + let mut v = decl_hoister( + DeclHoisterConfig { + hoist_fns: self.options.hoist_fns, + hoist_vars: self.options.hoist_vars, + top_level: self.options.top_level(), + }, + &data, + ); + n.apply(&mut v); + self.changed |= v.changed(); + } + + loop { + self.changed = false; + self.optimize_unit(n); + self.pass += 1; + if !self.changed { + break; + } + } + + // let last_mark = n.remove_mark(); + // assert!( + // N::is_module() || last_mark == self.marks.standalone, + // "{:?}; last={:?}", + // self.marks, + // last_mark + // ); + } + + /// Optimize a module. `N` can be [Module] or [FnExpr]. + fn optimize_unit(&mut self, n: &mut N) + where + N: CompileUnit + VisitWith + for<'aa> VisitMutWith>, + { + self.data = Some(analyze(&*n, Some(self.marks))); if self.options.passes != 0 && self.options.passes + 1 <= self.pass { let done = dump(&*n); @@ -144,7 +230,7 @@ impl VisitMut for Compressor<'_> { } let start = if cfg!(feature = "debug") { - let start = dump(&n.clone().fold_with(&mut fixer(None))); + let start = n.dump(); log::debug!("===== Start =====\n{}", start); start } else { @@ -160,7 +246,7 @@ impl VisitMut for Compressor<'_> { let start_time = now(); let mut visitor = expr_simplifier(); - n.visit_mut_with(&mut visitor); + n.apply(&mut visitor); self.changed |= visitor.changed(); if visitor.changed() { log::debug!("compressor: Simplified expressions"); @@ -180,7 +266,7 @@ impl VisitMut for Compressor<'_> { } if cfg!(feature = "debug") && !visitor.changed() { - let simplified = dump(&n.clone().fold_with(&mut fixer(None))); + let simplified = n.dump(); if start != simplified { assert_eq!( DebugUsingDisplay(&start), @@ -192,6 +278,29 @@ impl VisitMut for Compressor<'_> { } } + { + log::info!( + "compress/pure: Running pure optimizer (pass = {})", + self.pass + ); + + let start_time = now(); + + let mut visitor = pure_optimizer(&self.options, self.marks); + n.apply(&mut visitor); + self.changed |= visitor.changed(); + + if let Some(start_time) = start_time { + let end_time = Instant::now(); + + log::info!( + "compress/pure: Pure optimizer took {:?} (pass = {})", + end_time - start_time, + self.pass + ); + } + } + { log::info!("compress: Running optimizer (pass = {})", self.pass); @@ -203,13 +312,12 @@ impl VisitMut for Compressor<'_> { self.optimizer_state = Default::default(); let mut visitor = optimizer( - self.cm.clone(), self.marks, self.options, self.data.as_ref().unwrap(), &mut self.optimizer_state, ); - n.visit_mut_with(&mut visitor); + n.apply(&mut visitor); self.changed |= visitor.changed(); if let Some(start_time) = start_time { @@ -235,7 +343,7 @@ impl VisitMut for Compressor<'_> { let start_time = now(); let mut v = dead_branch_remover(); - n.map_with_mut(|n| n.fold_with(&mut v)); + n.apply(&mut v); if let Some(start_time) = start_time { let end_time = Instant::now(); @@ -261,26 +369,39 @@ impl VisitMut for Compressor<'_> { self.changed |= v.changed(); } + } +} + +impl VisitMut for Compressor<'_> { + noop_visit_mut_type!(); + + fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) { + if n.function.span.has_mark(self.marks.standalone) { + self.optimize_unit_repeatedly(n); + return; + } n.visit_mut_children_with(self); + } + + fn visit_mut_module(&mut self, n: &mut Module) { + let is_bundle_mode = n.span.has_mark(self.marks.bundle_of_standalones); + + if is_bundle_mode { + self.left_parallel_depth = MAX_PAR_DEPTH - 1; + } else { + self.optimize_unit_repeatedly(n); + return; + } + + n.visit_mut_children_with(self); + + self.optimize_unit_repeatedly(n); invoke(&*n); } fn visit_mut_module_items(&mut self, stmts: &mut Vec) { - { - let mut v = decl_hoister( - DeclHoisterConfig { - hoist_fns: self.options.hoist_fns, - hoist_vars: self.options.hoist_vars, - top_level: self.options.top_level(), - }, - self.data.as_ref().unwrap(), - ); - stmts.visit_mut_with(&mut v); - self.changed |= v.changed(); - } - self.handle_stmt_likes(stmts); stmts.retain(|stmt| match stmt { @@ -298,24 +419,8 @@ impl VisitMut for Compressor<'_> { }); } - fn visit_mut_script(&mut self, n: &mut Script) { - debug_assert!(self.data.is_none()); - self.data = Some(analyze(&*n, self.marks)); - - { - let mut v = decl_hoister( - DeclHoisterConfig { - hoist_fns: self.options.hoist_fns, - hoist_vars: self.options.hoist_vars, - top_level: self.options.top_level(), - }, - self.data.as_ref().unwrap(), - ); - n.body.visit_mut_with(&mut v); - self.changed |= v.changed(); - } - - n.visit_mut_children_with(self); + fn visit_mut_prop_or_spreads(&mut self, nodes: &mut Vec) { + self.visit_par(nodes); } fn visit_mut_stmts(&mut self, stmts: &mut Vec) { diff --git a/ecmascript/minifier/src/compress/optimize/bools.rs b/ecmascript/minifier/src/compress/optimize/bools.rs index e942a8b9ec4..05a9cce0c56 100644 --- a/ecmascript/minifier/src/compress/optimize/bools.rs +++ b/ecmascript/minifier/src/compress/optimize/bools.rs @@ -1,137 +1,18 @@ use super::Optimizer; -use crate::compress::optimize::{is_pure_undefined, Ctx}; +use crate::compress::optimize::Ctx; +use crate::compress::util::negate_cost; use crate::debug::dump; -use crate::util::make_bool; use swc_atoms::js_word; use swc_common::Spanned; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::Type; -use swc_ecma_utils::Value; use swc_ecma_utils::Value::Known; -use swc_ecma_utils::Value::Unknown; use swc_ecma_utils::{undefined, ExprExt}; /// Methods related to the options `bools` and `bool_as_ints`. impl Optimizer<'_> { - /// - /// - `!condition() || !-3.5` => `!condition()` - /// - /// In this case, if lhs is false, rhs is also false so it's removable. - pub(super) fn remove_useless_pipes(&mut self, e: &mut Expr) { - if !self.options.bools { - return; - } - - match e { - Expr::Bin(BinExpr { - left, - op: op @ op!("&&"), - right, - .. - }) - | Expr::Bin(BinExpr { - left, - op: op @ op!("||"), - right, - .. - }) => { - let lt = left.get_type(); - let rt = right.get_type(); - - match (lt, rt) { - (Known(Type::Bool), Known(Type::Bool)) => { - let rb = right.as_pure_bool(); - let rb = match rb { - Known(v) => v, - Unknown => return, - }; - - // - let can_remove = if *op == op!("&&") { rb } else { !rb }; - - if can_remove { - if *op == op!("&&") { - log::debug!("booleans: Compressing `!foo && true` as `!foo`"); - } else { - log::debug!("booleans: Compressing `!foo || false` as `!foo`"); - } - self.changed = true; - *e = *left.take(); - return; - } - } - _ => {} - } - } - _ => {} - } - } - - /// `!(a && b)` => `!a || !b` - pub(super) fn optimize_bools(&mut self, e: &mut Expr) { - if !self.options.bools { - return; - } - - match e { - Expr::Unary(UnaryExpr { - op: op!("!"), arg, .. - }) => match &mut **arg { - Expr::Bin(BinExpr { - op: op!("&&"), - left, - right, - .. - }) => { - if negate_cost(&left, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX) >= 0 - || negate_cost(&right, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX) - >= 0 - { - return; - } - log::debug!("Optimizing `!(a && b)` as `!a || !b`"); - self.changed = true; - self.negate(arg); - *e = *arg.take(); - return; - } - - Expr::Unary(UnaryExpr { - op: op!("!"), - arg: arg_of_arg, - .. - }) => match &mut **arg_of_arg { - Expr::Bin(BinExpr { - op: op!("||"), - left, - right, - .. - }) => { - if negate_cost(&left, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX) > 0 - && negate_cost(&right, self.ctx.in_bool_ctx, false) - .unwrap_or(isize::MAX) - > 0 - { - return; - } - log::debug!("Optimizing `!!(a || b)` as `!a && !b`"); - self.changed = true; - self.negate(arg_of_arg); - *e = *arg.take(); - return; - } - - _ => {} - }, - - _ => {} - }, - _ => {} - } - } - /// **This negates bool**. /// /// Returns true if it's negated. @@ -174,9 +55,8 @@ impl Optimizer<'_> { // // `_ || 'undefined' == typeof require` log::debug!( - "bools({}): Negating: (!a && !b) => !(a || b) (because both expression are good for \ + "bools: Negating: (!a && !b) => !(a || b) (because both expression are good for \ negation)", - self.line_col(e.span) ); let start = dump(&*e); @@ -202,277 +82,6 @@ impl Optimizer<'_> { true } - pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) { - if !self.options.bools { - return; - } - - let delete = match e { - Expr::Unary( - u @ UnaryExpr { - op: op!("delete"), .. - }, - ) => u, - _ => return, - }; - - if delete.arg.may_have_side_effects() { - return; - } - - let convert_to_true = match &*delete.arg { - Expr::Seq(..) - | Expr::Cond(..) - | Expr::Bin(BinExpr { op: op!("&&"), .. }) - | Expr::Bin(BinExpr { op: op!("||"), .. }) => true, - // V8 and terser test ref have different opinion. - Expr::Ident(Ident { - sym: js_word!("Infinity"), - .. - }) => false, - Expr::Ident(Ident { - sym: js_word!("undefined"), - .. - }) => false, - Expr::Ident(Ident { - sym: js_word!("NaN"), - .. - }) => false, - - e if is_pure_undefined(&e) => true, - - Expr::Ident(..) => true, - - // NaN - Expr::Bin(BinExpr { - op: op!("/"), - right, - .. - }) => { - let rn = right.as_number(); - let v = if let Known(rn) = rn { - if rn != 0.0 { - true - } else { - false - } - } else { - false - }; - - if v { - true - } else { - self.changed = true; - let span = delete.arg.span(); - log::debug!("booleans: Compressing `delete` as sequence expression"); - *e = Expr::Seq(SeqExpr { - span, - exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))], - }); - return; - } - } - - _ => false, - }; - - if convert_to_true { - self.changed = true; - let span = delete.arg.span(); - log::debug!("booleans: Compressing `delete` => true"); - *e = make_bool(span, true); - return; - } - } - - pub(super) fn compress_comparsion_of_typeof(&mut self, e: &mut BinExpr) { - fn should_optimize(l: &Expr, r: &Expr) -> bool { - match (l, r) { - ( - Expr::Unary(UnaryExpr { - op: op!("typeof"), .. - }), - Expr::Lit(..), - ) => true, - _ => false, - } - } - - match e.op { - op!("===") | op!("!==") => {} - _ => return, - } - - if should_optimize(&e.left, &e.right) || should_optimize(&e.right, &e.left) { - log::debug!("bools: Compressing comparison of `typeof` with literal"); - self.changed = true; - e.op = match e.op { - op!("===") => { - op!("==") - } - op!("!==") => { - op!("!=") - } - _ => { - unreachable!() - } - } - } - } - - /// This method converts `!1` to `0`. - pub(super) fn optimize_expr_in_bool_ctx(&mut self, n: &mut Expr) { - if !self.options.bools { - return; - } - - match n { - Expr::Bin(BinExpr { - op: op!("&&") | op!("||"), - left, - right, - .. - }) => { - // Regardless if it's truthy or falsy, we can optimize it because it will be - // casted as bool anyway. - self.optimize_expr_in_bool_ctx(&mut **left); - self.optimize_expr_in_bool_ctx(&mut **right); - return; - } - - Expr::Seq(e) => { - if let Some(last) = e.exprs.last_mut() { - self.optimize_expr_in_bool_ctx(&mut **last); - } - } - - _ => {} - } - - match n { - Expr::Unary(UnaryExpr { - span, - op: op!("!"), - arg, - }) => match &mut **arg { - Expr::Lit(Lit::Num(Number { value, .. })) => { - log::debug!("Optimizing: number => number (in bool context)"); - - self.changed = true; - *n = Expr::Lit(Lit::Num(Number { - span: *span, - value: if *value == 0.0 { 1.0 } else { 0.0 }, - })) - } - - Expr::Unary(UnaryExpr { - op: op!("!"), arg, .. - }) => { - log::debug!("bools: !!expr => expr (in bool ctx)"); - self.changed = true; - *n = *arg.take(); - return; - } - _ => {} - }, - - Expr::Unary(UnaryExpr { - span, - op: op!("typeof"), - arg, - }) => { - log::debug!("Optimizing: typeof => true (in bool context)"); - self.changed = true; - - match &**arg { - Expr::Ident(..) => { - *n = Expr::Lit(Lit::Num(Number { - span: *span, - value: 1.0, - })) - } - _ => { - // Return value of typeof is always truthy - let true_expr = Box::new(Expr::Lit(Lit::Num(Number { - span: *span, - value: 1.0, - }))); - *n = Expr::Seq(SeqExpr { - span: *span, - exprs: vec![arg.take(), true_expr], - }) - } - } - } - - Expr::Lit(Lit::Str(s)) => { - log::debug!("Converting string as boolean expressions"); - self.changed = true; - *n = Expr::Lit(Lit::Num(Number { - span: s.span, - value: if s.value.is_empty() { 0.0 } else { 1.0 }, - })); - } - - Expr::Lit(Lit::Num(num)) => { - if num.value == 1.0 || num.value == 0.0 { - return; - } - if self.options.bools { - log::debug!("booleans: Converting number as boolean expressions"); - self.changed = true; - *n = Expr::Lit(Lit::Num(Number { - span: num.span, - value: if num.value == 0.0 { 0.0 } else { 1.0 }, - })); - } - } - - Expr::Bin(BinExpr { - op: op!("??"), - left, - right, - .. - }) => { - // Optimize if (a ?? false); as if (a); - if let Value::Known(false) = right.as_pure_bool() { - log::debug!( - "Dropping right operand of `??` as it's always false (in bool context)" - ); - self.changed = true; - *n = *left.take(); - } - } - - Expr::Bin(BinExpr { - op: op!("||"), - left, - right, - .. - }) => { - // `a || false` => `a` (as it will be casted to boolean anyway) - - if let Known(false) = right.as_pure_bool() { - log::debug!("bools: `expr || false` => `expr` (in bool context)"); - self.changed = true; - *n = *left.take(); - return; - } - } - - _ => { - let span = n.span(); - let v = n.as_pure_bool(); - if let Known(v) = v { - log::debug!("Optimizing expr as {} (in bool context)", v); - *n = make_bool(span, v); - return; - } - } - } - } - pub(super) fn compress_if_stmt_as_expr(&mut self, s: &mut Stmt) { if !self.options.bools { return; @@ -561,229 +170,3 @@ impl Optimizer<'_> { } } } - -pub(crate) fn is_ok_to_negate_for_cond(e: &Expr) -> bool { - match e { - Expr::Update(..) => false, - _ => true, - } -} - -pub(crate) fn is_ok_to_negate_rhs(rhs: &Expr) -> bool { - match rhs { - Expr::Member(..) => true, - Expr::Bin(BinExpr { - op: op!("===") | op!("!==") | op!("==") | op!("!="), - .. - }) => true, - - Expr::Call(..) | Expr::New(..) => false, - - Expr::Update(..) => false, - - Expr::Bin(BinExpr { - op: op!("&&") | op!("||"), - left, - right, - .. - }) => is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right), - - Expr::Bin(BinExpr { left, right, .. }) => { - is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right) - } - - Expr::Assign(e) => is_ok_to_negate_rhs(&e.right), - - Expr::Unary(UnaryExpr { - op: op!("!") | op!("delete"), - .. - }) => true, - - Expr::Seq(e) => { - if let Some(last) = e.exprs.last() { - is_ok_to_negate_rhs(&last) - } else { - true - } - } - - Expr::Cond(e) => is_ok_to_negate_rhs(&e.cons) && is_ok_to_negate_rhs(&e.alt), - - _ => { - if !rhs.may_have_side_effects() { - return true; - } - - if cfg!(feature = "debug") { - log::warn!("unimplemented: is_ok_to_negate_rhs: `{}`", dump(&*rhs)); - } - - false - } - } -} - -/// A negative value means that it's efficient to negate the expression. -pub(crate) fn negate_cost(e: &Expr, in_bool_ctx: bool, is_ret_val_ignored: bool) -> Option { - fn cost( - e: &Expr, - in_bool_ctx: bool, - bin_op: Option, - is_ret_val_ignored: bool, - ) -> isize { - match e { - Expr::Unary(UnaryExpr { - op: op!("!"), arg, .. - }) => { - // TODO: Check if this argument is actually start of line. - match &**arg { - Expr::Call(CallExpr { - callee: ExprOrSuper::Expr(callee), - .. - }) => match &**callee { - Expr::Fn(..) => return 0, - _ => {} - }, - _ => {} - } - - if in_bool_ctx { - let c = -cost(arg, true, None, is_ret_val_ignored); - return c.min(-1); - } - - match &**arg { - Expr::Unary(UnaryExpr { op: op!("!"), .. }) => -1, - - _ => 1, - } - } - Expr::Bin(BinExpr { - op: op!("===") | op!("!==") | op!("==") | op!("!="), - .. - }) => 0, - - Expr::Bin(BinExpr { - op: op @ op!("||") | op @ op!("&&"), - left, - right, - .. - }) => { - let l_cost = cost(&left, in_bool_ctx, Some(*op), false); - - if !is_ret_val_ignored && !is_ok_to_negate_rhs(&right) { - return l_cost + 3; - } - l_cost + cost(&right, in_bool_ctx, Some(*op), is_ret_val_ignored) - } - - Expr::Cond(CondExpr { cons, alt, .. }) - if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) => - { - cost(&cons, in_bool_ctx, bin_op, is_ret_val_ignored) - + cost(&alt, in_bool_ctx, bin_op, is_ret_val_ignored) - } - - Expr::Cond(..) - | Expr::Update(..) - | Expr::Bin(BinExpr { - op: op!("in") | op!("instanceof"), - .. - }) => 3, - - Expr::Assign(..) => { - if is_ret_val_ignored { - 0 - } else { - 3 - } - } - - Expr::Seq(e) => { - if let Some(last) = e.exprs.last() { - return cost(&last, in_bool_ctx, bin_op, is_ret_val_ignored); - } - - if is_ret_val_ignored { - 0 - } else { - 1 - } - } - - _ => { - if is_ret_val_ignored { - 0 - } else { - 1 - } - } - } - } - - let cost = cost(e, in_bool_ctx, None, is_ret_val_ignored); - - if cfg!(feature = "debug") { - log::trace!("negation cost of `{}` = {}", dump(&*e), cost); - } - - Some(cost) -} - -#[cfg(test)] -mod tests { - use super::negate_cost; - use swc_common::{input::SourceFileInput, FileName}; - use swc_ecma_parser::{lexer::Lexer, Parser}; - - fn assert_negate_cost(s: &str, in_bool_ctx: bool, is_ret_val_ignored: bool, expected: isize) { - testing::run_test2(false, |cm, _| { - let fm = cm.new_source_file(FileName::Anon, s.to_string()); - - let lexer = Lexer::new( - Default::default(), - swc_ecma_ast::EsVersion::latest(), - SourceFileInput::from(&*fm), - None, - ); - - let mut parser = Parser::new_from(lexer); - - let e = parser - .parse_expr() - .expect("failed to parse input as an expression"); - - let actual = negate_cost(&e, in_bool_ctx, is_ret_val_ignored).unwrap(); - - assert_eq!( - actual, expected, - "Expected negation cost of {} to be {}, but got {}", - s, expected, actual, - ); - - Ok(()) - }) - .unwrap(); - } - - #[test] - fn logical_1() { - assert_negate_cost( - "this[key] && !this.hasOwnProperty(key) || (this[key] = value)", - false, - true, - 2, - ); - } - - #[test] - #[ignore] - fn logical_2() { - assert_negate_cost( - "(!this[key] || this.hasOwnProperty(key)) && (this[key] = value)", - false, - true, - -2, - ); - } -} diff --git a/ecmascript/minifier/src/compress/optimize/collapse_vars.rs b/ecmascript/minifier/src/compress/optimize/collapse_vars.rs index 705526eb44d..9cbc163f640 100644 --- a/ecmascript/minifier/src/compress/optimize/collapse_vars.rs +++ b/ecmascript/minifier/src/compress/optimize/collapse_vars.rs @@ -1,58 +1,14 @@ use super::Optimizer; use fxhash::FxHashMap; -use swc_common::DUMMY_SP; use swc_ecma_ast::*; -use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::ident::IdentLike; -use swc_ecma_utils::prepend; use swc_ecma_utils::Id; -use swc_ecma_utils::StmtLike; use swc_ecma_visit::noop_visit_mut_type; -use swc_ecma_visit::noop_visit_type; -use swc_ecma_visit::Node; -use swc_ecma_visit::Visit; use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMutWith; -use swc_ecma_visit::VisitWith; /// Methods related to the option `collapse_vars`. impl Optimizer<'_> { - pub(super) fn collapse_seq_exprs(&mut self, e: &mut Expr) { - if !self.options.collapse_vars { - return; - } - - let seq = match e { - Expr::Seq(seq) => seq, - _ => return, - }; - - if seq.exprs.len() < 2 { - return; - } - - match ( - &*seq.exprs[seq.exprs.len() - 2], - &*seq.exprs[seq.exprs.len() - 1], - ) { - (Expr::Assign(assign), Expr::Ident(ident)) => { - // Check if lhs is same as `ident`. - match &assign.left { - PatOrExpr::Expr(_) => {} - PatOrExpr::Pat(left) => match &**left { - Pat::Ident(left) => { - if left.id.sym == ident.sym && left.id.span.ctxt == ident.span.ctxt { - seq.exprs.pop(); - } - } - _ => {} - }, - } - } - _ => {} - } - } - pub(super) fn collapse_assignment_to_vars(&mut self, e: &mut Expr) { if !self.options.collapse_vars { return; @@ -120,286 +76,6 @@ impl Optimizer<'_> { _ => {} } } - - /// Collapse single-use non-constant variables, side effects permitting. - /// - /// This merges all variables to first variable declartion with an - /// initializer. If such variable declaration is not found, variables are - /// prepended to `stmts`. - pub(super) fn collapse_vars_without_init(&mut self, stmts: &mut Vec) - where - T: StmtLike, - Vec: - VisitWith + VisitMutWith + VisitMutWith, - { - if !self.options.collapse_vars { - return; - } - - { - // let mut found_other = false; - // let mut need_work = false; - - // for stmt in &*stmts { - // match stmt.as_stmt() { - // Some(Stmt::Decl(Decl::Var( - // v - // @ - // VarDecl { - // kind: VarDeclKind::Var, - // .. - // }, - // ))) => { - // if v.decls.iter().any(|v| v.init.is_none()) { - // if found_other { - // need_work = true; - // } - // } else { - // found_other = true; - // } - // } - - // _ => { - // found_other = true; - // } - // } - // } - - // Check for nested variable declartions. - let mut v = VarWithOutInitCounter::default(); - stmts.visit_with(&Invalid { span: DUMMY_SP }, &mut v); - if !v.need_work { - return; - } - } - - self.changed = true; - log::debug!("collapse_vars: Collapsing variables without an initializer"); - - let vars = { - let mut v = VarMover { - vars: Default::default(), - var_decl_kind: Default::default(), - }; - stmts.visit_mut_with(&mut v); - - v.vars - }; - - // Prepend vars - - let mut prepender = VarPrepender { vars }; - stmts.visit_mut_with(&mut prepender); - - if !prepender.vars.is_empty() { - prepend( - stmts, - T::from_stmt(Stmt::Decl(Decl::Var(VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - declare: Default::default(), - decls: prepender.vars, - }))), - ); - } - } -} - -/// See if there's two [VarDecl] which has [VarDeclarator] without the -/// initializer. -#[derive(Default)] -pub(super) struct VarWithOutInitCounter { - need_work: bool, - found_var_without_init: bool, - found_var_with_init: bool, -} - -impl Visit for VarWithOutInitCounter { - noop_visit_type!(); - - fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {} - - fn visit_constructor(&mut self, _: &Constructor, _: &dyn Node) {} - - fn visit_function(&mut self, _: &Function, _: &dyn Node) {} - - fn visit_var_decl(&mut self, v: &VarDecl, _: &dyn Node) { - v.visit_children_with(self); - - if v.kind != VarDeclKind::Var { - return; - } - - let mut found_init = false; - for d in &v.decls { - if d.init.is_some() { - found_init = true; - } else { - if found_init { - self.need_work = true; - return; - } - } - } - - if v.decls.iter().all(|v| v.init.is_none()) { - if self.found_var_without_init || self.found_var_with_init { - self.need_work = true; - } - self.found_var_without_init = true; - } else { - self.found_var_with_init = true; - } - } - - fn visit_var_decl_or_pat(&mut self, _: &VarDeclOrPat, _: &dyn Node) {} -} - -/// Moves all varaible without initializer. -pub(super) struct VarMover { - vars: Vec, - var_decl_kind: Option, -} - -impl VisitMut for VarMover { - noop_visit_mut_type!(); - - /// Noop - fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {} - - /// Noop - fn visit_mut_constructor(&mut self, _: &mut Constructor) {} - - /// Noop - fn visit_mut_function(&mut self, _: &mut Function) {} - - fn visit_mut_module_item(&mut self, s: &mut ModuleItem) { - s.visit_mut_children_with(self); - - match s { - ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { - decl: Decl::Var(d), - .. - })) if d.decls.is_empty() => { - s.take(); - return; - } - - _ => {} - } - } - - fn visit_mut_opt_var_decl_or_expr(&mut self, n: &mut Option) { - n.visit_mut_children_with(self); - - match n { - Some(VarDeclOrExpr::VarDecl(var)) => { - if var.decls.is_empty() { - *n = None; - } - } - _ => {} - } - } - - fn visit_mut_stmt(&mut self, s: &mut Stmt) { - s.visit_mut_children_with(self); - - match s { - Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => { - s.take(); - return; - } - _ => {} - } - } - - fn visit_mut_var_decl(&mut self, v: &mut VarDecl) { - let old = self.var_decl_kind.take(); - self.var_decl_kind = Some(v.kind); - v.visit_mut_children_with(self); - self.var_decl_kind = old; - } - - fn visit_mut_var_decl_or_pat(&mut self, _: &mut VarDeclOrPat) {} - - fn visit_mut_var_declarators(&mut self, d: &mut Vec) { - d.visit_mut_children_with(self); - - if self.var_decl_kind.unwrap() != VarDeclKind::Var { - return; - } - - if d.iter().all(|v| v.init.is_some()) { - return; - } - - let has_init = d.iter().any(|v| v.init.is_some()); - - if has_init { - let mut new = vec![]; - - for v in d.take() { - if v.init.is_some() { - new.push(v) - } else { - self.vars.push(v) - } - } - - *d = new; - } - - let mut new = vec![]; - - if has_init { - new.extend(self.vars.drain(..)); - } - - for v in d.take() { - if v.init.is_some() { - new.push(v) - } else { - self.vars.push(v) - } - } - - *d = new; - } -} - -pub(super) struct VarPrepender { - vars: Vec, -} - -impl VisitMut for VarPrepender { - noop_visit_mut_type!(); - - fn visit_mut_var_decl(&mut self, v: &mut VarDecl) { - if self.vars.is_empty() { - return; - } - - if v.kind != VarDeclKind::Var { - return; - } - - if v.decls.iter().any(|d| d.init.is_some()) { - let mut decls = self.vars.take(); - decls.extend(v.decls.take()); - - v.decls = decls; - } - } - - /// Noop - fn visit_mut_function(&mut self, _: &mut Function) {} - - /// Noop - fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {} - - /// Noop - fn visit_mut_constructor(&mut self, _: &mut Constructor) {} } struct Inliner<'a> { diff --git a/ecmascript/minifier/src/compress/optimize/computed_props.rs b/ecmascript/minifier/src/compress/optimize/computed_props.rs deleted file mode 100644 index 5cb0ea445b1..00000000000 --- a/ecmascript/minifier/src/compress/optimize/computed_props.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::Optimizer; -use swc_common::SyntaxContext; -use swc_ecma_ast::*; - -/// Methods related to the option `computed_props`. -impl Optimizer<'_> { - /// If a key of is `'str'` (like `{ 'str': 1 }`) change it to [Ident] like - /// (`{ str: 1, }`) - pub(super) fn optimize_computed_prop_name_as_normal(&mut self, p: &mut PropName) { - if !self.options.computed_props { - return; - } - - match p { - PropName::Computed(c) => match &mut *c.expr { - Expr::Lit(Lit::Str(s)) => { - if s.value == *"constructor" || s.value == *"__proto__" { - return; - } - - if s.value.is_empty() || s.value.starts_with(|c: char| c.is_numeric()) { - *p = PropName::Str(s.clone()); - } else { - *p = PropName::Ident(Ident::new( - s.value.clone(), - s.span.with_ctxt(SyntaxContext::empty()), - )); - } - - return; - } - Expr::Lit(Lit::Num(n)) => { - *p = PropName::Num(n.clone()); - return; - } - _ => {} - }, - _ => {} - } - } -} diff --git a/ecmascript/minifier/src/compress/optimize/conditionals.rs b/ecmascript/minifier/src/compress/optimize/conditionals.rs index 8ff591fce8f..5f71dc088ea 100644 --- a/ecmascript/minifier/src/compress/optimize/conditionals.rs +++ b/ecmascript/minifier/src/compress/optimize/conditionals.rs @@ -1,8 +1,6 @@ use super::Optimizer; -use crate::compress::optimize::bools::negate_cost; use crate::compress::optimize::Ctx; -use crate::debug::dump; -use crate::util::make_bool; +use crate::compress::util::negate_cost; use crate::util::SpanExt; use crate::DISABLE_BUGGY_PASSES; use std::mem::swap; @@ -17,39 +15,10 @@ use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ExprExt; use swc_ecma_utils::ExprFactory; use swc_ecma_utils::StmtLike; -use swc_ecma_utils::Type; -use swc_ecma_utils::Value::Known; /// Methods related to the option `conditionals`. All methods are noop if /// `conditionals` is false. impl Optimizer<'_> { - pub(super) fn negate_cond_expr(&mut self, cond: &mut CondExpr) { - if negate_cost(&cond.test, true, false).unwrap_or(isize::MAX) >= 0 { - return; - } - - self.changed = true; - log::debug!("conditionals: `a ? foo : bar` => `!a ? bar : foo` (considered cost)"); - let start_str = dump(&*cond); - - { - let ctx = Ctx { - in_bool_ctx: true, - ..self.ctx - }; - self.with_ctx(ctx).negate(&mut cond.test); - } - swap(&mut cond.cons, &mut cond.alt); - - if cfg!(feature = "debug") { - log::trace!( - "[Change] Negated cond: `{}` => `{}`", - start_str, - dump(&*cond) - ) - } - } - /// Negates the condition of a `if` statement to reduce body size. pub(super) fn negate_if_stmt(&mut self, stmt: &mut IfStmt) { let alt = match stmt.alt.as_deref_mut() { @@ -86,7 +55,6 @@ impl Optimizer<'_> { _ => return, } } - /// This method may change return value. /// /// - `a ? b : false` => `a && b` @@ -121,161 +89,6 @@ impl Optimizer<'_> { } } - /// Removes useless operands of an logical expressions. - pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) { - if !self.options.conditionals { - return; - } - - let bin = match e { - Expr::Bin(b) => b, - _ => return, - }; - - if bin.op != op!("||") && bin.op != op!("&&") { - return; - } - - if bin.left.may_have_side_effects() { - return; - } - - let lt = bin.left.get_type(); - let rt = bin.right.get_type(); - - let _lb = bin.left.as_pure_bool(); - let rb = bin.right.as_pure_bool(); - - if let (Known(Type::Bool), Known(Type::Bool)) = (lt, rt) { - // `!!b || true` => true - if let Known(true) = rb { - self.changed = true; - log::debug!("conditionals: `!!foo || true` => `true`"); - *e = make_bool(bin.span, true); - return; - } - } - } - - pub(super) fn compress_cond_with_logical_as_logical(&mut self, e: &mut Expr) { - if !self.options.conditionals { - return; - } - - let cond = match e { - Expr::Cond(v) => v, - _ => return, - }; - - let cons_span = cond.cons.span(); - - match (&mut *cond.cons, &mut *cond.alt) { - (Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt) - if (*cons.right).eq_ignore_span(&*alt) => - { - log::debug!("conditionals: `x ? y || z : z` => `x || y && z`"); - self.changed = true; - - *e = Expr::Bin(BinExpr { - span: cond.span, - op: op!("||"), - left: Box::new(Expr::Bin(BinExpr { - span: cons_span, - op: op!("&&"), - left: cond.test.take(), - right: cons.left.take(), - })), - right: cons.right.take(), - }); - return; - } - _ => {} - } - } - - /// - /// - `foo ? bar : false` => `!!foo && bar` - /// - `!foo ? true : bar` => `!foo || bar` - /// - `foo ? false : bar` => `!foo && bar` - pub(super) fn compress_conds_as_logical(&mut self, e: &mut Expr) { - let cond = match e { - Expr::Cond(cond) => cond, - _ => return, - }; - - let lt = cond.cons.get_type(); - if let Known(Type::Bool) = lt { - let lb = cond.cons.as_pure_bool(); - if let Known(true) = lb { - log::debug!("conditionals: `foo ? true : bar` => `!!foo || bar`"); - - // Negate twice to convert `test` to boolean. - self.negate_twice(&mut cond.test); - - self.changed = true; - *e = Expr::Bin(BinExpr { - span: cond.span, - op: op!("||"), - left: cond.test.take(), - right: cond.alt.take(), - }); - return; - } - - // TODO: Verify this rule. - if let Known(false) = lb { - log::debug!("conditionals: `foo ? false : bar` => `!foo && bar`"); - - self.changed = true; - self.negate(&mut cond.test); - - *e = Expr::Bin(BinExpr { - span: cond.span, - op: op!("&&"), - left: cond.test.take(), - right: cond.alt.take(), - }); - return; - } - } - - let rt = cond.alt.get_type(); - if let Known(Type::Bool) = rt { - let rb = cond.alt.as_pure_bool(); - if let Known(false) = rb { - log::debug!("conditionals: `foo ? bar : false` => `!!foo && bar`"); - self.changed = true; - - // Negate twice to convert `test` to boolean. - self.negate_twice(&mut cond.test); - - *e = Expr::Bin(BinExpr { - span: cond.span, - op: op!("&&"), - left: cond.test.take(), - right: cond.cons.take(), - }); - return; - } - - if let Known(true) = rb { - log::debug!("conditionals: `foo ? bar : true` => `!foo || bar"); - self.changed = true; - - // Negate twice to convert `test` to boolean. - self.negate(&mut cond.test); - - *e = Expr::Bin(BinExpr { - span: cond.span, - op: op!("||"), - left: cond.test.take(), - right: cond.cons.take(), - }); - return; - } - } - } - /// /// # Example /// diff --git a/ecmascript/minifier/src/compress/optimize/evaluate.rs b/ecmascript/minifier/src/compress/optimize/evaluate.rs index 2f0cad96b25..a4f815106a7 100644 --- a/ecmascript/minifier/src/compress/optimize/evaluate.rs +++ b/ecmascript/minifier/src/compress/optimize/evaluate.rs @@ -1,7 +1,6 @@ use super::Optimizer; -use crate::compress::optimize::is_pure_undefined_or_null; +use crate::compress::util::eval_as_number; use crate::DISABLE_BUGGY_PASSES; -use std::f64; use std::num::FpCategory; use swc_atoms::js_word; use swc_common::Spanned; @@ -23,15 +22,8 @@ impl Optimizer<'_> { self.eval_global_vars(e); self.eval_numbers(e); - self.eval_number_method_call(e); self.eval_known_static_method_call(e); - self.eval_str_method_call(e); - - self.eval_array_method_call(e); - self.eval_fn_method_call(e); - - self.eval_opt_chain(e); } fn eval_global_vars(&mut self, e: &mut Expr) { @@ -314,122 +306,7 @@ impl Optimizer<'_> { } } - /// Handle calls on string literals, like `'foo'.toUpperCase()`. - fn eval_str_method_call(&mut self, e: &mut Expr) { - if !self.options.evaluate { - return; - } - - if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign { - return; - } - - let call = match e { - Expr::Call(v) => v, - _ => return, - }; - - let (s, method) = match &call.callee { - ExprOrSuper::Super(_) => return, - ExprOrSuper::Expr(callee) => match &**callee { - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - .. - }) => match (&**obj, &**prop) { - (Expr::Lit(Lit::Str(s)), Expr::Ident(prop)) => (s.clone(), prop.sym.clone()), - _ => return, - }, - _ => return, - }, - }; - - let new_val = match &*method { - "toLowerCase" => s.value.to_lowercase(), - "toUpperCase" => s.value.to_uppercase(), - "charCodeAt" => { - if call.args.len() != 1 { - return; - } - if let Expr::Lit(Lit::Num(Number { value, .. })) = &*call.args[0].expr { - if value.fract() != 0.0 { - return; - } - - let idx = value.round() as i64 as usize; - let c = s.value.chars().nth(idx); - match c { - Some(v) => { - self.changed = true; - log::debug!( - "evaluate: Evaluated `charCodeAt` of a string literal as `{}`", - v - ); - *e = Expr::Lit(Lit::Num(Number { - span: call.span, - value: v as usize as f64, - })) - } - None => { - self.changed = true; - log::debug!( - "evaluate: Evaluated `charCodeAt` of a string literal as `NaN`", - ); - *e = Expr::Ident(Ident::new( - js_word!("NaN"), - e.span().with_ctxt(SyntaxContext::empty()), - )) - } - } - } - return; - } - _ => return, - }; - - self.changed = true; - log::debug!("evaluate: Evaluated `{}` of a string literal", method); - *e = Expr::Lit(Lit::Str(Str { - value: new_val.into(), - ..s - })); - } - fn eval_numbers(&mut self, e: &mut Expr) { - if self.options.unsafe_passes && self.options.unsafe_math { - match e { - Expr::Call(CallExpr { - span, - callee: ExprOrSuper::Expr(callee), - args, - .. - }) => { - if args.len() == 1 && args[0].spread.is_none() { - match &**callee { - Expr::Ident(Ident { - sym: js_word!("Number"), - .. - }) => { - self.changed = true; - log::debug!( - "evaluate: Reducing a call to `Number` into an unary operation" - ); - - *e = Expr::Unary(UnaryExpr { - span: *span, - op: op!(unary, "+"), - arg: args.take().into_iter().next().unwrap().expr, - }); - } - _ => {} - } - } - } - _ => {} - } - } - if !self.options.evaluate { return; } @@ -440,7 +317,7 @@ impl Optimizer<'_> { match e { Expr::Call(..) => { - if let Some(value) = self.eval_as_number(&e) { + if let Some(value) = eval_as_number(&e) { self.changed = true; log::debug!("evaluate: Evaluated an expression as `{}`", value); @@ -550,406 +427,6 @@ impl Optimizer<'_> { } } - /// Evaluates method calls of a numeric constant. - fn eval_number_method_call(&mut self, e: &mut Expr) { - if !self.options.evaluate { - return; - } - - let (num, method, args) = match e { - Expr::Call(CallExpr { - callee: ExprOrSuper::Expr(callee), - args, - .. - }) => match &mut **callee { - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - .. - }) => match &mut **obj { - Expr::Lit(Lit::Num(obj)) => match &mut **prop { - Expr::Ident(prop) => (obj, prop, args), - _ => return, - }, - _ => return, - }, - _ => return, - }, - _ => return, - }; - - if args.iter().any(|arg| arg.expr.may_have_side_effects()) { - return; - } - - match &*method.sym { - "toFixed" => { - if let Some(precision) = self.eval_as_number(&args[0].expr) { - let precision = precision.floor() as usize; - let value = num_to_fixed(num.value, precision + 1); - - self.changed = true; - log::debug!( - "evaluate: Evaluating `{}.toFixed({})` as `{}`", - num, - precision, - value - ); - - *e = Expr::Lit(Lit::Str(Str { - span: e.span(), - value: value.into(), - has_escape: false, - kind: Default::default(), - })) - } - } - _ => {} - } - } - - /// This method does **not** modifies `e`. - /// - /// This method is used to test if a whole call can be replaced, while - /// preserving standalone constants. - fn eval_as_number(&mut self, e: &Expr) -> Option { - match e { - Expr::Bin(BinExpr { - op: op!(bin, "-"), - left, - right, - .. - }) => { - let l = self.eval_as_number(&left)?; - let r = self.eval_as_number(&right)?; - - return Some(l - r); - } - - Expr::Call(CallExpr { - callee: ExprOrSuper::Expr(callee), - args, - .. - }) => { - for arg in &*args { - if arg.spread.is_some() || arg.expr.may_have_side_effects() { - return None; - } - } - - match &**callee { - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - .. - }) => { - let prop = match &**prop { - Expr::Ident(i) => i, - _ => return None, - }; - - match &**obj { - Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym { - "cos" => { - let v = self.eval_as_number(&args.first()?.expr)?; - - return Some(v.cos()); - } - "sin" => { - let v = self.eval_as_number(&args.first()?.expr)?; - - return Some(v.sin()); - } - - "max" => { - let mut numbers = vec![]; - for arg in args { - let v = self.eval_as_number(&arg.expr)?; - if v.is_infinite() || v.is_nan() { - return None; - } - numbers.push(v); - } - - return Some( - numbers - .into_iter() - .max_by(|a, b| a.partial_cmp(b).unwrap()) - .unwrap_or(f64::NEG_INFINITY), - ); - } - - "min" => { - let mut numbers = vec![]; - for arg in args { - let v = self.eval_as_number(&arg.expr)?; - if v.is_infinite() || v.is_nan() { - return None; - } - numbers.push(v); - } - - return Some( - numbers - .into_iter() - .min_by(|a, b| a.partial_cmp(b).unwrap()) - .unwrap_or(f64::INFINITY), - ); - } - - "pow" => { - if args.len() != 2 { - return None; - } - let first = self.eval_as_number(&args[0].expr)?; - let second = self.eval_as_number(&args[1].expr)?; - - return Some(first.powf(second)); - } - - _ => {} - }, - _ => {} - } - } - _ => {} - } - } - - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - .. - }) => { - let prop = match &**prop { - Expr::Ident(i) => i, - _ => return None, - }; - - match &**obj { - Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym { - "PI" => return Some(f64::consts::PI), - "E" => return Some(f64::consts::E), - "LN10" => return Some(f64::consts::LN_10), - _ => {} - }, - _ => {} - } - } - - _ => { - if let Known(v) = e.as_number() { - return Some(v); - } - } - } - - None - } - - fn eval_array_method_call(&mut self, e: &mut Expr) { - if !self.options.evaluate { - return; - } - - if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign { - return; - } - - let call = match e { - Expr::Call(e) => e, - _ => return, - }; - - let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); - - for arg in &call.args { - if arg.expr.may_have_side_effects() { - return; - } - } - - let callee = match &mut call.callee { - ExprOrSuper::Super(_) => return, - ExprOrSuper::Expr(e) => &mut **e, - }; - - match callee { - Expr::Member(MemberExpr { - span, - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - }) => { - if obj.may_have_side_effects() { - return; - } - - let arr = match &mut **obj { - Expr::Array(arr) => arr, - _ => return, - }; - - let has_spread_elem = arr.elems.iter().any(|s| match s { - Some(ExprOrSpread { - spread: Some(..), .. - }) => true, - _ => false, - }); - - // Ignore array - - let method_name = match &**prop { - Expr::Ident(i) => i, - _ => return, - }; - - match &*method_name.sym { - "slice" => { - if has_spread || has_spread_elem { - return; - } - - match call.args.len() { - 0 => { - self.changed = true; - log::debug!("evaluate: Dropping array.slice call"); - *e = *obj.take(); - return; - } - 1 => { - if let Known(start) = call.args[0].expr.as_number() { - let start = start.floor() as usize; - - self.changed = true; - log::debug!("evaluate: Reducing array.slice({}) call", start); - - if start >= arr.elems.len() { - *e = Expr::Array(ArrayLit { - span: *span, - elems: Default::default(), - }); - return; - } - - let elems = arr.elems.drain(start..).collect(); - - *e = Expr::Array(ArrayLit { span: *span, elems }); - return; - } - } - _ => { - let start = call.args[0].expr.as_number(); - let end = call.args[1].expr.as_number(); - if let Known(start) = start { - let start = start.floor() as usize; - - if let Known(end) = end { - let end = end.floor() as usize; - self.changed = true; - log::debug!( - "evaluate: Reducing array.slice({}, {}) call", - start, - end - ); - let end = end.min(arr.elems.len()); - - if start >= arr.elems.len() { - *e = Expr::Array(ArrayLit { - span: *span, - elems: Default::default(), - }); - return; - } - - let elems = arr.elems.drain(start..end).collect(); - - *e = Expr::Array(ArrayLit { span: *span, elems }); - return; - } - } - } - } - } - _ => {} - } - } - _ => {} - } - } - - fn eval_fn_method_call(&mut self, e: &mut Expr) { - if !self.options.evaluate { - return; - } - - if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign { - return; - } - - let call = match e { - Expr::Call(e) => e, - _ => return, - }; - - let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); - - for arg in &call.args { - if arg.expr.may_have_side_effects() { - return; - } - } - - let callee = match &mut call.callee { - ExprOrSuper::Super(_) => return, - ExprOrSuper::Expr(e) => &mut **e, - }; - - match callee { - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(obj), - prop, - computed: false, - .. - }) => { - if obj.may_have_side_effects() { - return; - } - - let _f = match &mut **obj { - Expr::Fn(v) => v, - _ => return, - }; - - let method_name = match &**prop { - Expr::Ident(i) => i, - _ => return, - }; - - match &*method_name.sym { - "valueOf" => { - if has_spread { - return; - } - - self.changed = true; - log::debug!( - "evaludate: Reduced `funtion.valueOf()` into a function expression" - ); - - *e = *obj.take(); - return; - } - _ => {} - } - } - _ => {} - } - } - /// /// - `Object(1) && 1 && 2` => `Object(1) && 2`. pub(super) fn optimize_bin_and_or(&mut self, e: &mut BinExpr) { @@ -995,79 +472,4 @@ impl Optimizer<'_> { _ => return, } } - - fn eval_opt_chain(&mut self, e: &mut Expr) { - let opt = match e { - Expr::OptChain(e) => e, - _ => return, - }; - - match &mut *opt.expr { - Expr::Member(MemberExpr { - span, - obj: ExprOrSuper::Expr(obj), - .. - }) => { - // - if is_pure_undefined_or_null(&obj) { - self.changed = true; - log::debug!( - "evaluate: Reduced an optioanl chaining operation because object is \ - always null or undefined" - ); - - *e = *undefined(*span); - return; - } - } - - Expr::Call(CallExpr { - span, - callee: ExprOrSuper::Expr(callee), - .. - }) => { - if is_pure_undefined_or_null(&callee) { - self.changed = true; - log::debug!( - "evaluate: Reduced a call expression with optioanl chaining operation \ - because object is always null or undefined" - ); - - *e = *undefined(*span); - return; - } - } - - _ => {} - } - } -} - -/// https://stackoverflow.com/questions/60497397/how-do-you-format-a-float-to-the-first-significant-decimal-and-with-specified-pr -fn num_to_fixed(float: f64, precision: usize) -> String { - // compute absolute value - let a = float.abs(); - - // if abs value is greater than 1, then precision becomes less than "standard" - let precision = if a >= 1. { - // reduce by number of digits, minimum 0 - let n = (1. + a.log10().floor()) as usize; - if n <= precision { - precision - n - } else { - 0 - } - // if precision is less than 1 (but non-zero), then precision becomes - // greater than "standard" - } else if a > 0. { - // increase number of digits - let n = -(1. + a.log10().floor()) as usize; - precision + n - // special case for 0 - } else { - 0 - }; - - // format with the given computed precision - format!("{0:.1$}", float, precision) } diff --git a/ecmascript/minifier/src/compress/optimize/fns.rs b/ecmascript/minifier/src/compress/optimize/fns.rs index b8a2007baec..6b5d11151b1 100644 --- a/ecmascript/minifier/src/compress/optimize/fns.rs +++ b/ecmascript/minifier/src/compress/optimize/fns.rs @@ -1,6 +1,6 @@ use super::Optimizer; use crate::{ - compress::optimize::util::is_directive, + compress::util::is_directive, util::{sort::is_sorted_by, MoudleItemExt}, DISABLE_BUGGY_PASSES, }; diff --git a/ecmascript/minifier/src/compress/optimize/if_return.rs b/ecmascript/minifier/src/compress/optimize/if_return.rs index 8388f488fa2..9fffd665220 100644 --- a/ecmascript/minifier/src/compress/optimize/if_return.rs +++ b/ecmascript/minifier/src/compress/optimize/if_return.rs @@ -1,5 +1,5 @@ use super::Optimizer; -use crate::compress::optimize::is_pure_undefined; +use crate::compress::util::is_pure_undefined; use crate::debug::dump; use crate::util::ExprOptExt; use swc_common::Spanned; @@ -17,19 +17,6 @@ use swc_ecma_visit::VisitWith; /// Methods related to the option `if_return`. All methods are noop if /// `if_return` is false. impl Optimizer<'_> { - pub(super) fn drop_undefined_from_return_arg(&mut self, s: &mut ReturnStmt) { - match s.arg.as_deref() { - Some(e) => { - if is_pure_undefined(e) { - self.changed = true; - log::debug!("Dropped `undefined` from `return undefined`"); - s.arg.take(); - } - } - None => {} - } - } - /// # Input /// /// ```js diff --git a/ecmascript/minifier/src/compress/optimize/join_vars.rs b/ecmascript/minifier/src/compress/optimize/join_vars.rs index 49cad308be4..c8b35f636a3 100644 --- a/ecmascript/minifier/src/compress/optimize/join_vars.rs +++ b/ecmascript/minifier/src/compress/optimize/join_vars.rs @@ -1,4 +1,4 @@ -use crate::compress::optimize::util::is_directive; +use crate::compress::util::is_directive; use super::Optimizer; use swc_ecma_ast::*; diff --git a/ecmascript/minifier/src/compress/optimize/loops.rs b/ecmascript/minifier/src/compress/optimize/loops.rs index c42b06b4c6c..ea920cb987a 100644 --- a/ecmascript/minifier/src/compress/optimize/loops.rs +++ b/ecmascript/minifier/src/compress/optimize/loops.rs @@ -1,6 +1,5 @@ use crate::compress::optimize::unused::UnreachableHandler; use crate::compress::optimize::Optimizer; -use swc_common::Spanned; use swc_common::DUMMY_SP; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; @@ -9,45 +8,6 @@ use swc_ecma_utils::Value::Known; /// Methods related to the option `loops`. impl Optimizer<'_> { - /// - /// - `while(test);` => `for(;;test); - /// - `do; while(true)` => `for(;;); - pub(super) fn loop_to_for_stmt(&mut self, s: &mut Stmt) { - if !self.options.loops { - return; - } - - match s { - Stmt::While(stmt) => { - self.changed = true; - log::debug!("loops: Converting a while loop to a for loop"); - *s = Stmt::For(ForStmt { - span: stmt.span, - init: None, - test: Some(stmt.test.take()), - update: None, - body: stmt.body.take(), - }); - } - Stmt::DoWhile(stmt) => { - let val = stmt.test.as_pure_bool(); - if let Known(true) = val { - self.changed = true; - log::debug!("loops: Converting an always-true do-while loop to a for loop"); - - *s = Stmt::For(ForStmt { - span: stmt.span, - init: None, - test: Some(stmt.test.take()), - update: None, - body: stmt.body.take(), - }); - } - } - _ => {} - } - } - /// `for(a;b;c;) break;` => `a;b;` pub(super) fn optimize_loops_with_break(&mut self, s: &mut Stmt) { if !self.options.loops { @@ -98,70 +58,6 @@ impl Optimizer<'_> { *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }) } - /// # Input - /// - /// ```js - /// for(; size--;) - /// if (!(result = eq(a[size], b[size], aStack, bStack))) - /// break; - /// ``` - /// - /// - /// # Output - /// - /// ```js - /// for (; size-- && (result = eq(a[size], b[size], aStack, bStack));); - /// ``` - pub(super) fn merge_for_if_break(&mut self, s: &mut ForStmt) { - match &mut *s.body { - Stmt::If(IfStmt { - test, - cons, - alt: None, - .. - }) => { - match &**cons { - Stmt::Break(BreakStmt { label: None, .. }) => { - // We only care about instant breaks. - // - // Note: As the minifier of swc is very fast, we don't - // care about block statements with a single break as a - // body. - // - // If it's optimizable, other pass for if statements - // will remove block and with the next pass we can apply - // this pass. - self.changed = true; - log::debug!("loops: Compressing for-if-break into a for statement"); - - // We negate because this `test` is used as a condition for `break`. - self.negate(test); - - match s.test.take() { - Some(left) => { - s.test = Some(Box::new(Expr::Bin(BinExpr { - span: s.test.span(), - op: op!("&&"), - left, - right: test.take(), - }))); - } - None => { - s.test = Some(test.take()); - } - } - - // Remove body - s.body.take(); - } - _ => {} - } - } - - _ => {} - } - } - /// /// - `while(false) { var a; foo() }` => `var a;` pub(super) fn optiimze_loops_if_cond_is_false(&mut self, stmt: &mut Stmt) { diff --git a/ecmascript/minifier/src/compress/optimize/misc.rs b/ecmascript/minifier/src/compress/optimize/misc.rs deleted file mode 100644 index 626cc40cb8e..00000000000 --- a/ecmascript/minifier/src/compress/optimize/misc.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::{util::is_valid_identifier, Optimizer}; -use swc_ecma_ast::*; - -impl Optimizer<'_> { - pub(super) fn optimize_prop_name(&mut self, name: &mut PropName) { - match name { - PropName::Str(s) => { - if s.value.is_reserved() || s.value.is_reserved_in_es3() { - return; - } - - if is_valid_identifier(&s.value, false) { - self.changed = true; - log::debug!("misc: Optimizing string property name"); - *name = PropName::Ident(Ident { - span: s.span, - sym: s.value.clone(), - optional: false, - }); - return; - } - } - _ => {} - } - } - - pub(super) fn remove_useless_return(&mut self, stmts: &mut Vec) { - if let Some(Stmt::Return(ReturnStmt { arg: None, .. })) = stmts.last() { - self.changed = true; - log::debug!("misc: Removing useless return"); - stmts.pop(); - } - } -} diff --git a/ecmascript/minifier/src/compress/optimize/mod.rs b/ecmascript/minifier/src/compress/optimize/mod.rs index 6deed694b45..6bb364769a3 100644 --- a/ecmascript/minifier/src/compress/optimize/mod.rs +++ b/ecmascript/minifier/src/compress/optimize/mod.rs @@ -1,5 +1,6 @@ use crate::analyzer::ProgramData; use crate::analyzer::UsageAnalyzer; +use crate::compress::util::is_pure_undefined; use crate::marks::Marks; use crate::option::CompressOptions; use crate::util::contains_leaping_yield; @@ -12,9 +13,7 @@ use swc_atoms::js_word; use swc_atoms::JsWord; use swc_common::iter::IdentifyLast; use swc_common::pass::Repeated; -use swc_common::sync::Lrc; use swc_common::Mark; -use swc_common::SourceMap; use swc_common::Spanned; use swc_common::SyntaxContext; use swc_common::DUMMY_SP; @@ -39,10 +38,8 @@ use Value::Known; use self::util::replace_id_with_expr; mod arguments; -mod arrows; mod bools; mod collapse_vars; -mod computed_props; mod conditionals; mod dead_code; mod evaluate; @@ -53,14 +50,10 @@ mod iife; mod inline; mod join_vars; mod loops; -mod misc; -mod numbers; mod ops; -mod properties; mod sequences; mod strings; mod switches; -mod unsafes; mod unused; mod util; @@ -72,7 +65,6 @@ pub(super) struct OptimizerState { /// This pass is simillar to `node.optimize` of terser. pub(super) fn optimizer<'a>( - cm: Lrc, marks: Marks, options: &'a CompressOptions, data: &'a ProgramData, @@ -86,7 +78,6 @@ pub(super) fn optimizer<'a>( let done = Mark::fresh(Mark::root()); let done_ctxt = SyntaxContext::empty().apply_mark(done); Optimizer { - cm, marks, changed: false, options, @@ -111,6 +102,9 @@ pub(super) fn optimizer<'a>( /// This should not be modified directly. Use `.with_ctx()` instead. #[derive(Debug, Default, Clone, Copy)] struct Ctx { + /// See [crate::marks::Marks] + skip_standalone: bool, + /// `true` if the [VarDecl] has const annotation. has_const_ann: bool, @@ -198,8 +192,6 @@ impl Ctx { } struct Optimizer<'a> { - cm: Lrc, - marks: Marks, changed: bool, @@ -250,11 +242,7 @@ impl Optimizer<'_> { fn handle_stmt_likes(&mut self, stmts: &mut Vec) where T: StmtLike + ModuleItemLike + MoudleItemExt + VisitMutWith, - Vec: VisitMutWith - + VisitWith - + VisitWith - + VisitMutWith - + VisitMutWith, + Vec: VisitMutWith + VisitWith, { match self.data { Some(..) => {} @@ -314,8 +302,6 @@ impl Optimizer<'_> { self.ctx.in_asm |= use_asm; - self.drop_useless_blocks(stmts); - self.reorder_stmts(stmts); self.merge_simillar_ifs(stmts); @@ -323,8 +309,6 @@ impl Optimizer<'_> { self.make_sequences(stmts); - self.collapse_vars_without_init(stmts); - self.drop_else_token(stmts); self.break_assignments_in_seqs(stmts); @@ -1166,7 +1150,7 @@ impl Optimizer<'_> { if s.value.contains(|c: char| !c.is_ascii()) { return true; } - if s.value.contains("\\\0") { + if s.value.contains("\\\0") || s.value.contains("//") { return true; } @@ -1514,14 +1498,10 @@ impl VisitMut for Optimizer<'_> { self.compress_typeof_undefined(n); - self.compress_comparsion_of_typeof(n); - self.optimize_bin_operator(n); self.optimize_bin_and_or(n); - self.optimize_null_or_undefined_cmp(n); - if n.op == op!(bin, "+") { if let Known(Type::Str) = n.left.get_type() { self.optimize_expr_in_str_ctx(&mut n.right); @@ -1531,10 +1511,6 @@ impl VisitMut for Optimizer<'_> { self.optimize_expr_in_str_ctx(&mut n.left); } } - - if n.op == op!(bin, "+") { - self.concat_tpl(&mut n.left, &mut n.right); - } } fn visit_mut_block_stmt(&mut self, n: &mut BlockStmt) { @@ -1548,12 +1524,6 @@ impl VisitMut for Optimizer<'_> { n.visit_mut_children_with(&mut *self.with_ctx(ctx)); } - fn visit_mut_block_stmt_or_expr(&mut self, body: &mut BlockStmtOrExpr) { - body.visit_mut_children_with(self); - - self.optimize_arrow_body(body); - } - fn visit_mut_call_expr(&mut self, e: &mut CallExpr) { let inline_prevented = self.ctx.inline_prevented || self.has_noinline(e.span); @@ -1607,8 +1577,6 @@ impl VisitMut for Optimizer<'_> { e.args.visit_mut_with(&mut *self.with_ctx(ctx)); } - self.optimize_symbol_call_unsafely(e); - self.inline_args_of_iife(e); } @@ -1640,14 +1608,6 @@ impl VisitMut for Optimizer<'_> { e.visit_mut_children_with(self); } - fn visit_mut_cond_expr(&mut self, n: &mut CondExpr) { - n.visit_mut_children_with(self); - - self.negate_cond_expr(n); - - self.optimize_expr_in_bool_ctx(&mut n.test); - } - fn visit_mut_decl(&mut self, decl: &mut Decl) { decl.visit_mut_children_with(self); @@ -1706,20 +1666,10 @@ impl VisitMut for Optimizer<'_> { self.remove_invalid(e); - self.concat_str(e); - - self.lift_minus(e); - - self.convert_tpl_to_str(e); - self.optimize_str_access_to_arguments(e); self.replace_props(e); - self.swap_bin_operands(e); - - self.collapse_seq_exprs(e); - self.drop_unused_assignments(e); self.compress_regexp(e); @@ -1728,20 +1678,12 @@ impl VisitMut for Optimizer<'_> { self.compress_typeofs(e); - self.compress_useless_deletes(e); - self.optimize_nullish_coalescing(e); self.compress_logical_exprs_as_bang_bang(e, false); self.compress_useless_cond_expr(e); - self.compress_conds_as_logical(e); - - self.drop_logical_operands(e); - - self.drop_useless_addition_of_str(e); - self.inline(e); match e { @@ -1757,22 +1699,10 @@ impl VisitMut for Optimizer<'_> { } self.compress_cond_expr_if_simillar(e); - self.compress_cond_with_logical_as_logical(e); self.compress_negated_bin_eq(e); - self.handle_negated_seq(e); self.compress_array_join(e); - self.remove_useless_pipes(e); - - self.optimize_bools(e); - - self.handle_property_access(e); - - self.lift_seqs_of_bin(e); - - self.lift_seqs_of_cond_assign(e); - if self.options.negate_iife { self.negate_iife_in_cond(e); } @@ -1926,16 +1856,10 @@ impl VisitMut for Optimizer<'_> { s.visit_mut_children_with(&mut *self.with_ctx(ctx)); - self.with_ctx(ctx).merge_for_if_break(s); - self.with_ctx(ctx).optimize_init_of_for_stmt(s); self.with_ctx(ctx).drop_if_break(s); - if let Some(test) = &mut s.test { - self.with_ctx(ctx).optimize_expr_in_bool_ctx(test); - } - match &mut *s.body { Stmt::Block(body) => { self.negate_if_terminate(&mut body.stmts, false, true); @@ -1953,10 +1877,19 @@ impl VisitMut for Optimizer<'_> { n.decorators.visit_mut_with(&mut *self.with_ctx(ctx)); } + let is_standalone = n.span.has_mark(self.marks.standalone); + + // We don't dig into standalone function, as it does not share any variable with + // outer scope. + if self.ctx.skip_standalone && is_standalone { + return; + } + let old_in_asm = self.ctx.in_asm; { let ctx = Ctx { + skip_standalone: self.ctx.skip_standalone || is_standalone, stmt_lablled: false, in_fn_like: true, scope: n.span.ctxt, @@ -1970,13 +1903,8 @@ impl VisitMut for Optimizer<'_> { Some(body) => { // Bypass block scope handler. body.visit_mut_children_with(optimizer); - optimizer.remove_useless_return(&mut body.stmts); optimizer.negate_if_terminate(&mut body.stmts, true, false); - - if let Some(last) = body.stmts.last_mut() { - optimizer.drop_unused_stmt_at_end_of_fn(last); - } } None => {} } @@ -2011,8 +1939,6 @@ impl VisitMut for Optimizer<'_> { self.negate_if_stmt(n); - self.optimize_expr_in_bool_ctx(&mut n.test); - self.merge_nested_if(n); self.merge_else_if(n); @@ -2045,15 +1971,12 @@ impl VisitMut for Optimizer<'_> { }; n.prop.visit_mut_with(&mut *self.with_ctx(ctx)); } - - self.optimize_property_of_member_expr(n); - - self.handle_known_computed_member_expr(n); } fn visit_mut_module_items(&mut self, stmts: &mut Vec) { let ctx = Ctx { top_level: true, + skip_standalone: true, ..self.ctx }; self.with_ctx(ctx).handle_stmt_likes(stmts); @@ -2133,24 +2056,9 @@ impl VisitMut for Optimizer<'_> { n.retain(|p| !p.pat.is_invalid()); } - fn visit_mut_prop(&mut self, p: &mut Prop) { - p.visit_mut_children_with(self); - - self.optimize_arrow_method_prop(p); - } - - fn visit_mut_prop_name(&mut self, p: &mut PropName) { - p.visit_mut_children_with(self); - - self.optimize_computed_prop_name_as_normal(p); - self.optimize_prop_name(p); - } - fn visit_mut_return_stmt(&mut self, n: &mut ReturnStmt) { n.visit_mut_children_with(self); - self.drop_undefined_from_return_arg(n); - if let Some(arg) = &mut n.arg { self.optimize_in_fn_termiation(&mut **arg); } @@ -2170,8 +2078,6 @@ impl VisitMut for Optimizer<'_> { self.shift_assignment(n); - self.merge_seq_call(n); - { let exprs = n .exprs @@ -2260,19 +2166,6 @@ impl VisitMut for Optimizer<'_> { _ => {} } - if self.options.drop_debugger { - match s { - Stmt::Debugger(..) => { - self.changed = true; - *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); - log::debug!("drop_debugger: Dropped a debugger statement"); - return; - } - _ => {} - } - } - - self.loop_to_for_stmt(s); self.optiimze_loops_if_cond_is_false(s); self.optimize_loops_with_break(s); @@ -2384,14 +2277,6 @@ impl VisitMut for Optimizer<'_> { n.exprs .iter_mut() .for_each(|expr| self.optimize_expr_in_str_ctx(&mut **expr)); - - self.compress_tpl(n); - - debug_assert_eq!( - n.exprs.len() + 1, - n.quasis.len(), - "tagged template literal compressor created an invalid template literal" - ); } fn visit_mut_try_stmt(&mut self, n: &mut TryStmt) { @@ -2414,12 +2299,6 @@ impl VisitMut for Optimizer<'_> { }; n.visit_mut_children_with(&mut *self.with_ctx(ctx)); - - if n.op == op!("!") { - self.with_ctx(ctx).optimize_expr_in_bool_ctx(&mut n.arg); - } else if n.op == op!(unary, "+") || n.op == op!(unary, "-") { - self.with_ctx(ctx).optimize_expr_in_num_ctx(&mut n.arg); - } } fn visit_mut_update_expr(&mut self, n: &mut UpdateExpr) { @@ -2519,8 +2398,6 @@ impl VisitMut for Optimizer<'_> { }; n.visit_mut_children_with(&mut *self.with_ctx(ctx)); } - - self.optimize_expr_in_bool_ctx(&mut n.test); } fn visit_mut_yield_expr(&mut self, n: &mut YieldExpr) { @@ -2538,31 +2415,6 @@ impl VisitMut for Optimizer<'_> { } } -fn is_pure_undefined(e: &Expr) -> bool { - match e { - Expr::Ident(Ident { - sym: js_word!("undefined"), - .. - }) => true, - - Expr::Unary(UnaryExpr { - op: UnaryOp::Void, - arg, - .. - }) if !arg.may_have_side_effects() => true, - - _ => false, - } -} - -fn is_pure_undefined_or_null(e: &Expr) -> bool { - is_pure_undefined(e) - || match e { - Expr::Lit(Lit::Null(..)) => true, - _ => false, - } -} - /// If true, `0` in `(0, foo.bar)()` is preserved. fn is_callee_this_aware(callee: &Expr) -> bool { match &*callee { diff --git a/ecmascript/minifier/src/compress/optimize/ops.rs b/ecmascript/minifier/src/compress/optimize/ops.rs index 313ea7c5933..97c596cc0b5 100644 --- a/ecmascript/minifier/src/compress/optimize/ops.rs +++ b/ecmascript/minifier/src/compress/optimize/ops.rs @@ -1,16 +1,9 @@ use super::Optimizer; -use crate::compress::optimize::bools::is_ok_to_negate_for_cond; -use crate::compress::optimize::bools::is_ok_to_negate_rhs; -use crate::compress::optimize::is_pure_undefined; -use crate::debug::dump; +use crate::compress::util::negate; use crate::util::make_bool; use crate::util::ValueExt; -use std::mem::swap; use swc_atoms::js_word; use swc_common::EqIgnoreSpan; -use swc_common::Span; -use swc_common::Spanned; -use swc_common::DUMMY_SP; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::ident::IdentLike; @@ -20,124 +13,6 @@ use swc_ecma_utils::Value; use Value::Known; impl Optimizer<'_> { - /// - /// - `a === undefined || a === null` => `a == null` - pub(super) fn optimize_null_or_undefined_cmp(&mut self, e: &mut BinExpr) { - fn opt( - span: Span, - top_op: BinaryOp, - e_left: &mut Expr, - e_right: &mut Expr, - ) -> Option { - let (cmp, op, left, right) = match &mut *e_left { - Expr::Bin(left_bin) => { - if left_bin.op != op!("===") && left_bin.op != op!("!==") { - return None; - } - - if top_op == op!("&&") && left_bin.op == op!("===") { - return None; - } - if top_op == op!("||") && left_bin.op == op!("!==") { - return None; - } - - if !left_bin.right.is_ident() { - return None; - } - - let right = match &mut *e_right { - Expr::Bin(right_bin) => { - if right_bin.op != left_bin.op { - return None; - } - - if !right_bin.right.eq_ignore_span(&left_bin.right) { - return None; - } - - &mut *right_bin.left - } - _ => return None, - }; - - ( - &mut left_bin.right, - left_bin.op, - &mut *left_bin.left, - &mut *right, - ) - } - _ => return None, - }; - - let lt = left.get_type(); - let rt = right.get_type(); - if let Known(lt) = lt { - if let Known(rt) = rt { - match (lt, rt) { - (Type::Undefined, Type::Null) | (Type::Null, Type::Undefined) => { - if op == op!("===") { - log::debug!( - "Reducing `!== null || !== undefined` check to `!= null`" - ); - return Some(BinExpr { - span, - op: op!("=="), - left: cmp.take(), - right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), - }); - } else { - log::debug!( - "Reducing `=== null || === undefined` check to `== null`" - ); - return Some(BinExpr { - span, - op: op!("!="), - left: cmp.take(), - right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), - }); - } - } - _ => {} - } - } - } - - None - } - - if e.op == op!("||") || e.op == op!("&&") { - { - let res = opt(e.span, e.op, &mut e.left, &mut e.right); - if let Some(res) = res { - self.changed = true; - *e = res; - return; - } - } - - match (&mut *e.left, &mut *e.right) { - (Expr::Bin(left), right) => { - if e.op == left.op { - let res = opt(right.span(), e.op, &mut left.right, &mut *right); - if let Some(res) = res { - self.changed = true; - *e = BinExpr { - span: e.span, - op: e.op, - left: left.left.take(), - right: Box::new(Expr::Bin(res)), - }; - return; - } - } - } - _ => {} - } - } - } - /// /// - `'12' === `foo` => '12' == 'foo'` pub(super) fn optimize_bin_operator(&mut self, e: &mut BinExpr) { @@ -274,168 +149,9 @@ impl Optimizer<'_> { self.negate(e); } - /// Creates `!e` where e is the expression passed as an argument. - /// - /// # Note - /// - /// This method returns `!e` if `!!e` is given as a argument. - /// - /// TODO: Handle special cases like !1 or !0 pub(super) fn negate(&mut self, e: &mut Expr) { - let start_str = dump(&*e); - self.changed = true; - match e { - Expr::Bin(bin @ BinExpr { op: op!("=="), .. }) - | Expr::Bin(bin @ BinExpr { op: op!("!="), .. }) - | Expr::Bin(bin @ BinExpr { op: op!("==="), .. }) - | Expr::Bin(bin @ BinExpr { op: op!("!=="), .. }) => { - bin.op = match bin.op { - op!("==") => { - op!("!=") - } - op!("!=") => { - op!("==") - } - op!("===") => { - op!("!==") - } - op!("!==") => { - op!("===") - } - _ => { - unreachable!() - } - }; - self.changed = true; - log::debug!("negate: binary"); - return; - } - - Expr::Bin(BinExpr { - left, - right, - op: op @ op!("&&"), - .. - }) if is_ok_to_negate_rhs(&right) => { - log::debug!("negate: a && b => !a || !b"); - - self.negate(&mut **left); - self.negate(&mut **right); - *op = op!("||"); - return; - } - - Expr::Bin(BinExpr { - left, - right, - op: op @ op!("||"), - .. - }) if is_ok_to_negate_rhs(&right) => { - log::debug!("negate: a || b => !a && !b"); - - self.negate(&mut **left); - self.negate(&mut **right); - *op = op!("&&"); - return; - } - - Expr::Cond(CondExpr { cons, alt, .. }) - if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) => - { - log::debug!("negate: cond"); - - self.negate(&mut **cons); - self.negate(&mut **alt); - return; - } - - Expr::Seq(SeqExpr { exprs, .. }) => { - if let Some(last) = exprs.last_mut() { - log::debug!("negate: seq"); - - self.negate(&mut **last); - return; - } - } - - _ => {} - } - - let mut arg = Box::new(e.take()); - - match &mut *arg { - Expr::Unary(UnaryExpr { - op: op!("!"), arg, .. - }) => match &mut **arg { - Expr::Unary(UnaryExpr { op: op!("!"), .. }) => { - log::debug!("negate: !!bool => !bool"); - *e = *arg.take(); - return; - } - Expr::Bin(BinExpr { op: op!("in"), .. }) - | Expr::Bin(BinExpr { - op: op!("instanceof"), - .. - }) => { - log::debug!("negate: !bool => bool"); - *e = *arg.take(); - return; - } - _ => { - if self.ctx.in_bool_ctx { - log::debug!("negate: !expr => expr (in bool context)"); - *e = *arg.take(); - return; - } - } - }, - - _ => {} - } - - log::debug!("negate: e => !e"); - - *e = Expr::Unary(UnaryExpr { - span: DUMMY_SP, - op: op!("!"), - arg, - }); - - if cfg!(feature = "debug") { - log::trace!("[Change] Negated `{}` as `{}`", start_str, dump(&*e)); - } - } - - pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) { - match &mut *n { - Expr::Unary(e @ UnaryExpr { op: op!("!"), .. }) - | Expr::Unary( - e @ UnaryExpr { - op: op!("delete"), .. - }, - ) => { - match &mut *e.arg { - Expr::Seq(SeqExpr { exprs, .. }) => { - if exprs.is_empty() { - return; - } - log::debug!("optimizing negated sequences"); - - { - let last = exprs.last_mut().unwrap(); - self.optimize_expr_in_bool_ctx(last); - // Negate last element. - self.negate(last); - } - - *n = *e.arg.take(); - } - _ => {} - } - } - _ => {} - } + negate(e, self.ctx.in_bool_ctx) } /// This method does @@ -502,123 +218,6 @@ impl Optimizer<'_> { e.right = left.take(); } - /// Rules: - /// - `l > i` => `i < l` - fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool { - match (l, r) { - (Expr::Member(_), _) if is_for_rel => false, - - (Expr::Update(..) | Expr::Assign(..), Expr::Lit(..)) if is_for_rel => false, - - ( - Expr::Member(..) - | Expr::Call(..) - | Expr::Assign(..) - | Expr::Update(..) - | Expr::Bin(BinExpr { - op: op!("&&") | op!("||"), - .. - }), - Expr::Lit(..), - ) => true, - - ( - Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), - Expr::Unary(UnaryExpr { - op: op!("!"), arg, .. - }), - ) if match &**arg { - Expr::Lit(..) => true, - _ => false, - } => - { - true - } - - (Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), r) if is_pure_undefined(r) => { - true - } - - (Expr::Ident(..), Expr::Lit(..)) if is_for_rel => false, - - (Expr::Ident(..), Expr::Ident(..)) => false, - - (Expr::Ident(..), Expr::Lit(..)) - | ( - Expr::Ident(..) | Expr::Member(..), - Expr::Unary(UnaryExpr { - op: op!("void") | op!("!"), - .. - }), - ) - | ( - Expr::This(..), - Expr::Unary(UnaryExpr { - op: op!("void"), .. - }), - ) - | (Expr::Unary(..), Expr::Lit(..)) - | (Expr::Tpl(..), Expr::Lit(..)) => true, - _ => false, - } - } - - fn try_swap_bin(&mut self, op: BinaryOp, left: &mut Expr, right: &mut Expr) -> bool { - fn is_supported(op: BinaryOp) -> bool { - match op { - op!("===") - | op!("!==") - | op!("==") - | op!("!=") - | op!("&") - | op!("^") - | op!("|") - | op!("*") => true, - _ => false, - } - } - - if !is_supported(op) { - return false; - } - - if self.can_swap_bin_operands(&left, &right, false) { - log::debug!("Swapping operands of binary exprssion"); - swap(left, right); - return true; - } - - false - } - - /// Swap lhs and rhs in certain conditions. - pub(super) fn swap_bin_operands(&mut self, expr: &mut Expr) { - match expr { - Expr::Bin(e @ BinExpr { op: op!("<="), .. }) - | Expr::Bin(e @ BinExpr { op: op!("<"), .. }) => { - if self.options.comparisons && self.can_swap_bin_operands(&e.left, &e.right, true) { - self.changed = true; - log::debug!("comparisons: Swapping operands of {}", e.op); - - e.op = if e.op == op!("<=") { - op!(">=") - } else { - op!(">") - }; - - swap(&mut e.left, &mut e.right); - } - } - - Expr::Bin(bin) => { - if self.try_swap_bin(bin.op, &mut bin.left, &mut bin.right) { - self.changed = true; - } - } - _ => {} - } - } - /// Remove meaningless literals in a binary expressions. /// /// # Parameters diff --git a/ecmascript/minifier/src/compress/optimize/sequences.rs b/ecmascript/minifier/src/compress/optimize/sequences.rs index 67c2c8ffb6a..934c82274d2 100644 --- a/ecmascript/minifier/src/compress/optimize/sequences.rs +++ b/ecmascript/minifier/src/compress/optimize/sequences.rs @@ -1,18 +1,17 @@ use super::{is_pure_undefined, Optimizer}; -use crate::compress::optimize::util::{ - get_lhs_ident, get_lhs_ident_mut, is_directive, replace_id_with_expr, -}; +use crate::compress::optimize::util::replace_id_with_expr; +use crate::compress::util::{get_lhs_ident, get_lhs_ident_mut, is_directive}; use crate::debug::dump; use crate::util::{idents_used_by, idents_used_by_ignoring_nested, ExprOptExt}; use retain_mut::RetainMut; use std::mem::take; use swc_atoms::js_word; +use swc_common::Spanned; use swc_common::DUMMY_SP; -use swc_common::{Spanned, SyntaxContext}; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::ident::IdentLike; -use swc_ecma_utils::{contains_this_expr, undefined, ExprExt, ExprFactory, Id, StmtLike}; +use swc_ecma_utils::{contains_this_expr, undefined, ExprExt, Id, StmtLike}; use swc_ecma_visit::noop_visit_type; use swc_ecma_visit::Node; use swc_ecma_visit::Visit; @@ -333,210 +332,6 @@ impl Optimizer<'_> { *stmts = new_stmts; } - /// `(a = foo, a.apply())` => `(a = foo).apply()` - /// - /// This is useful for outputs of swc/babel - pub(super) fn merge_seq_call(&mut self, e: &mut SeqExpr) { - if !self.options.sequences() { - return; - } - - for idx in 0..e.exprs.len() { - let (e1, e2) = e.exprs.split_at_mut(idx); - - let a = match e1.last_mut() { - Some(v) => &mut **v, - None => continue, - }; - - let b = match e2.first_mut() { - Some(v) => &mut **v, - None => continue, - }; - - match (&mut *a, &mut *b) { - ( - Expr::Assign(a_assign), - Expr::Call(CallExpr { - callee: ExprOrSuper::Expr(b_callee), - args, - .. - }), - ) => { - let var_name = get_lhs_ident(&a_assign.left); - let var_name = match var_name { - Some(v) => v, - None => continue, - }; - - match &mut **b_callee { - Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(b_callee_obj), - computed: false, - prop, - .. - }) => { - // - if !b_callee_obj.is_ident_ref_to(var_name.sym.clone()) { - continue; - } - - match &**prop { - Expr::Ident(Ident { sym, .. }) => match &**sym { - "apply" | "call" => {} - _ => continue, - }, - _ => {} - } - - let span = a_assign.span.with_ctxt(SyntaxContext::empty()); - - let obj = a.take(); - - let new = Expr::Call(CallExpr { - span, - callee: MemberExpr { - span: DUMMY_SP, - obj: obj.as_obj(), - prop: prop.take(), - computed: false, - } - .as_callee(), - args: args.take(), - type_args: Default::default(), - }); - b.take(); - self.changed = true; - log::debug!( - "sequences: Reducing `(a = foo, a.call())` to `((a = foo).call())`" - ); - - *a = new; - } - _ => {} - }; - } - - _ => {} - } - } - } - - /// - /// - `(a, b, c) && d` => `a, b, c && d` - pub(super) fn lift_seqs_of_bin(&mut self, e: &mut Expr) { - let bin = match e { - Expr::Bin(b) => b, - _ => return, - }; - - match &mut *bin.left { - Expr::Seq(left) => { - if left.exprs.is_empty() { - return; - } - - self.changed = true; - log::debug!("sequences: Lifting sequence in a binary expression"); - - let left_last = left.exprs.pop().unwrap(); - - let mut exprs = left.exprs.take(); - - exprs.push(Box::new(Expr::Bin(BinExpr { - span: left.span, - op: bin.op, - left: left_last, - right: bin.right.take(), - }))); - - *e = Expr::Seq(SeqExpr { - span: bin.span, - exprs, - }) - } - _ => {} - } - } - - pub(super) fn normalize_sequences(&self, seq: &mut SeqExpr) { - for e in &mut seq.exprs { - match &mut **e { - Expr::Seq(e) => { - self.normalize_sequences(&mut *e); - } - _ => {} - } - } - - if seq.exprs.iter().any(|v| v.is_seq()) { - let mut new = vec![]; - - for e in seq.exprs.take() { - match *e { - Expr::Seq(s) => { - new.extend(s.exprs); - } - _ => new.push(e), - } - } - - seq.exprs = new; - } - } - - /// - /// - `x = (foo(), bar(), baz()) ? 10 : 20` => `foo(), bar(), x = baz() ? 10 - /// : 20;` - pub(super) fn lift_seqs_of_cond_assign(&mut self, e: &mut Expr) { - if !self.options.sequences() { - return; - } - - let assign = match e { - Expr::Assign(v) => v, - _ => return, - }; - - let cond = match &mut *assign.right { - Expr::Cond(v) => v, - _ => return, - }; - - match &mut *cond.test { - Expr::Seq(test) => { - // - if test.exprs.len() >= 2 { - let mut new_seq = vec![]; - new_seq.extend(test.exprs.drain(..test.exprs.len() - 1)); - - self.changed = true; - log::debug!("sequences: Lifting sequences in a assignment with cond expr"); - let new_cond = CondExpr { - span: cond.span, - test: test.exprs.pop().unwrap(), - cons: cond.cons.take(), - alt: cond.alt.take(), - }; - - new_seq.push(Box::new(Expr::Assign(AssignExpr { - span: assign.span, - op: assign.op, - left: assign.left.take(), - right: Box::new(Expr::Cond(new_cond)), - }))); - - *e = Expr::Seq(SeqExpr { - span: assign.span, - exprs: new_seq, - }); - return; - } - } - _ => {} - } - } - /// Break assignments in sequences. /// /// This may result in less parenthesis. @@ -865,6 +660,32 @@ impl Optimizer<'_> { }); } + pub(super) fn normalize_sequences(&self, seq: &mut SeqExpr) { + for e in &mut seq.exprs { + match &mut **e { + Expr::Seq(e) => { + self.normalize_sequences(&mut *e); + } + _ => {} + } + } + + if seq.exprs.iter().any(|v| v.is_seq()) { + let mut new = vec![]; + + for e in seq.exprs.take() { + match *e { + Expr::Seq(s) => { + new.extend(s.exprs); + } + _ => new.push(e), + } + } + + seq.exprs = new; + } + } + pub(super) fn merge_sequences_in_seq_expr(&mut self, e: &mut SeqExpr) { self.normalize_sequences(e); diff --git a/ecmascript/minifier/src/compress/optimize/strings.rs b/ecmascript/minifier/src/compress/optimize/strings.rs index 0ba4831a68d..bf889de9b33 100644 --- a/ecmascript/minifier/src/compress/optimize/strings.rs +++ b/ecmascript/minifier/src/compress/optimize/strings.rs @@ -1,34 +1,13 @@ use super::Optimizer; -use std::mem::take; use swc_atoms::js_word; -use swc_atoms::JsWord; use swc_common::Spanned; -use swc_common::DUMMY_SP; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ExprExt; -use swc_ecma_utils::Type; use swc_ecma_utils::Value::Known; impl Optimizer<'_> { - /// Converts template literals to string if `exprs` of [Tpl] is empty. - pub(super) fn convert_tpl_to_str(&mut self, e: &mut Expr) { - match e { - Expr::Tpl(t) if t.quasis.len() == 1 && t.exprs.is_empty() => { - if let Some(c) = &t.quasis[0].cooked { - if c.value.chars().all(|c| match c { - '\u{0020}'..='\u{007e}' => true, - _ => false, - }) { - *e = Expr::Lit(Lit::Str(c.clone())); - } - } - } - _ => {} - } - } - pub(super) fn optimize_expr_in_str_ctx_unsafely(&mut self, e: &mut Expr) { if !self.options.unsafe_passes { return; @@ -182,268 +161,4 @@ impl Optimizer<'_> { _ => {} } } - - /// This compresses a template literal by inlining string literals in - /// expresions into quasis. - /// - /// Note that this pass only cares about string literals and conversion to a - /// string literal should be done before calling this pass. - pub(super) fn compress_tpl(&mut self, tpl: &mut Tpl) { - debug_assert_eq!(tpl.exprs.len() + 1, tpl.quasis.len()); - let has_str_lit = tpl.exprs.iter().any(|expr| match &**expr { - Expr::Lit(Lit::Str(..)) => true, - _ => false, - }); - if !has_str_lit { - return; - } - - let mut quasis = vec![]; - let mut exprs = vec![]; - let mut cur = String::new(); - let mut cur_raw = String::new(); - - for i in 0..(tpl.exprs.len() + tpl.quasis.len()) { - if i % 2 == 0 { - let i = i / 2; - let q = tpl.quasis[i].take(); - - cur.push_str(&q.cooked.unwrap().value); - cur_raw.push_str(&q.raw.value); - } else { - let i = i / 2; - let e = tpl.exprs[i].take(); - - match *e { - Expr::Lit(Lit::Str(s)) => { - cur.push_str(&s.value); - cur_raw.push_str(&s.value); - } - _ => { - quasis.push(TplElement { - span: DUMMY_SP, - tail: true, - cooked: Some(Str { - span: DUMMY_SP, - value: take(&mut cur).into(), - has_escape: false, - kind: Default::default(), - }), - raw: Str { - span: DUMMY_SP, - value: take(&mut cur_raw).into(), - has_escape: false, - kind: Default::default(), - }, - }); - - exprs.push(e); - } - } - } - } - - quasis.push(TplElement { - span: DUMMY_SP, - tail: true, - cooked: Some(Str { - span: DUMMY_SP, - value: cur.into(), - has_escape: false, - kind: Default::default(), - }), - raw: Str { - span: DUMMY_SP, - value: cur_raw.into(), - has_escape: false, - kind: Default::default(), - }, - }); - - debug_assert_eq!(exprs.len() + 1, quasis.len()); - - tpl.quasis = quasis; - tpl.exprs = exprs; - } - - /// Called for binary operations with `+`. - pub(super) fn concat_tpl(&mut self, l: &mut Expr, r: &mut Expr) { - match (&mut *l, &mut *r) { - (Expr::Tpl(l), Expr::Lit(Lit::Str(rs))) => { - // Append - if let Some(l_last) = l.quasis.last_mut() { - self.changed = true; - - log::debug!( - "template: Concatted a string (`{}`) on rhs of `+` to a template literal", - rs.value - ); - let l_str = l_last.cooked.as_mut().unwrap(); - - let new: JsWord = format!("{}{}", l_str.value, rs.value).into(); - l_str.value = new.clone(); - l_last.raw.value = new; - - r.take(); - return; - } - } - - (Expr::Lit(Lit::Str(ls)), Expr::Tpl(r)) => { - // Append - if let Some(r_first) = r.quasis.first_mut() { - self.changed = true; - - log::debug!( - "template: Prepended a string (`{}`) on lhs of `+` to a template literal", - ls.value - ); - let r_str = r_first.cooked.as_mut().unwrap(); - - let new: JsWord = format!("{}{}", ls.value, r_str.value).into(); - r_str.value = new.clone(); - r_first.raw.value = new; - - l.take(); - return; - } - } - - (Expr::Tpl(l), Expr::Tpl(rt)) => { - // We prepend the last quasis of l to the first quasis of r. - // After doing so, we can append all data of r to l. - - { - let l_last = l.quasis.pop().unwrap(); - let mut r_first = rt.quasis.first_mut().unwrap(); - - let r_str = r_first.cooked.as_mut().unwrap(); - - let new: JsWord = - format!("{}{}", l_last.cooked.unwrap().value, r_str.value).into(); - r_str.value = new.clone(); - r_first.raw.value = new; - } - - l.quasis.extend(rt.quasis.take()); - l.exprs.extend(rt.exprs.take()); - // Remove r - r.take(); - - debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{:?} is invalid", l); - self.changed = true; - log::debug!("strings: Merged to template literals"); - } - - _ => {} - } - } - - /// - /// - `a + 'foo' + 'bar'` => `a + 'foobar'` - pub(super) fn concat_str(&mut self, e: &mut Expr) { - match e { - Expr::Bin( - bin @ BinExpr { - op: op!(bin, "+"), .. - }, - ) => match &mut *bin.left { - Expr::Bin( - left - @ BinExpr { - op: op!(bin, "+"), .. - }, - ) => { - let type_of_second = left.right.get_type(); - let type_of_third = bin.right.get_type(); - - if let Known(Type::Str) = type_of_second { - if let Known(Type::Str) = type_of_third { - if let Known(second_str) = left.right.as_string() { - if let Known(third_str) = bin.right.as_string() { - let new_str = format!("{}{}", second_str, third_str); - let left_span = left.span; - - self.changed = true; - log::debug!( - "string: Concatting `{} + {}` to `{}`", - second_str, - third_str, - new_str - ); - - *e = Expr::Bin(BinExpr { - span: bin.span, - op: op!(bin, "+"), - left: left.left.take(), - right: Box::new(Expr::Lit(Lit::Str(Str { - span: left_span, - value: new_str.into(), - has_escape: false, - kind: Default::default(), - }))), - }); - return; - } - } - } - } - } - _ => {} - }, - _ => {} - } - } - - pub(super) fn drop_useless_addition_of_str(&mut self, e: &mut Expr) { - match e { - Expr::Bin(BinExpr { - op: op!(bin, "+"), - left, - right, - .. - }) => { - let lt = left.get_type(); - let rt = right.get_type(); - if let Known(Type::Str) = lt { - if let Known(Type::Str) = rt { - match &**left { - Expr::Lit(Lit::Str(Str { - value: js_word!(""), - .. - })) => { - self.changed = true; - log::debug!( - "string: Dropping empty string literal (in lhs) because it \ - does not changes type" - ); - - *e = *right.take(); - return; - } - _ => {} - } - - match &**right { - Expr::Lit(Lit::Str(Str { - value: js_word!(""), - .. - })) => { - self.changed = true; - log::debug!( - "string: Dropping empty string literal (in rhs) because it \ - does not changes type" - ); - - *e = *left.take(); - return; - } - _ => {} - } - } - } - } - _ => {} - } - } } diff --git a/ecmascript/minifier/src/compress/optimize/unused.rs b/ecmascript/minifier/src/compress/optimize/unused.rs index 8193b6e0c93..fffc597a6fc 100644 --- a/ecmascript/minifier/src/compress/optimize/unused.rs +++ b/ecmascript/minifier/src/compress/optimize/unused.rs @@ -9,90 +9,12 @@ use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_utils::contains_ident_ref; use swc_ecma_utils::ident::IdentLike; -use swc_ecma_utils::StmtLike; use swc_ecma_visit::noop_visit_mut_type; use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMutWith; /// Methods related to the option `unused`. impl Optimizer<'_> { - pub(super) fn drop_useless_blocks(&mut self, stmts: &mut Vec) - where - T: StmtLike, - { - fn is_inliable(b: &BlockStmt) -> bool { - b.stmts.iter().all(|s| match s { - Stmt::Decl(Decl::Fn(FnDecl { - ident: - Ident { - sym: js_word!("undefined"), - .. - }, - .. - })) => false, - - Stmt::Decl( - Decl::Var(VarDecl { - kind: VarDeclKind::Var, - .. - }) - | Decl::Fn(..), - ) => true, - Stmt::Decl(..) => false, - _ => true, - }) - } - - if stmts.iter().all(|stmt| match stmt.as_stmt() { - Some(Stmt::Block(b)) if is_inliable(b) => false, - _ => true, - }) { - return; - } - - self.changed = true; - log::debug!("Dropping useless block"); - - let mut new = vec![]; - for stmt in stmts.take() { - match stmt.try_into_stmt() { - Ok(v) => match v { - Stmt::Block(v) if is_inliable(&v) => { - new.extend(v.stmts.into_iter().map(T::from_stmt)); - } - _ => new.push(T::from_stmt(v)), - }, - Err(v) => { - new.push(v); - } - } - } - - *stmts = new; - } - - pub(super) fn drop_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) { - match s { - Stmt::Return(r) => match r.arg.as_deref_mut() { - Some(Expr::Unary(UnaryExpr { - span, - op: op!("void"), - arg, - })) => { - log::debug!("unused: Removing `return void` in end of a function"); - self.changed = true; - *s = Stmt::Expr(ExprStmt { - span: *span, - expr: arg.take(), - }); - return; - } - _ => {} - }, - _ => {} - } - } - pub(super) fn drop_unused_var_declarator(&mut self, var: &mut VarDeclarator) { match &mut var.init { Some(init) => match &**init { diff --git a/ecmascript/minifier/src/compress/optimize/util.rs b/ecmascript/minifier/src/compress/optimize/util.rs index 6052dfb2989..9189f35a42f 100644 --- a/ecmascript/minifier/src/compress/optimize/util.rs +++ b/ecmascript/minifier/src/compress/optimize/util.rs @@ -11,15 +11,8 @@ use swc_ecma_utils::Id; use swc_ecma_visit::noop_visit_mut_type; use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMutWith; -use unicode_xid::UnicodeXID; impl<'b> Optimizer<'b> { - pub(super) fn line_col(&self, span: Span) -> String { - let loc = self.cm.lookup_char_pos(span.lo); - - format!("{}:{}", loc.line, loc.col_display) - } - pub(super) fn access_property<'e>( &mut self, expr: &'e mut Expr, @@ -170,40 +163,6 @@ pub(crate) fn class_has_side_effect(c: &Class) -> bool { false } -pub(crate) fn get_lhs_ident(e: &PatOrExpr) -> Option<&Ident> { - match e { - PatOrExpr::Expr(v) => match &**v { - Expr::Ident(i) => Some(i), - _ => None, - }, - PatOrExpr::Pat(v) => match &**v { - Pat::Ident(i) => Some(&i.id), - Pat::Expr(v) => match &**v { - Expr::Ident(i) => Some(i), - _ => None, - }, - _ => None, - }, - } -} - -pub(crate) fn get_lhs_ident_mut(e: &mut PatOrExpr) -> Option<&mut Ident> { - match e { - PatOrExpr::Expr(v) => match &mut **v { - Expr::Ident(i) => Some(i), - _ => None, - }, - PatOrExpr::Pat(v) => match &mut **v { - Pat::Ident(i) => Some(&mut i.id), - Pat::Expr(v) => match &mut **v { - Expr::Ident(i) => Some(i), - _ => None, - }, - _ => None, - }, - } -} - pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool { match e { Expr::Lit(..) => return false, @@ -212,29 +171,6 @@ pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool { } } -pub(crate) fn is_directive(e: &Stmt) -> bool { - match e { - Stmt::Expr(s) => match &*s.expr { - Expr::Lit(Lit::Str(Str { value, .. })) => value.starts_with("use "), - _ => false, - }, - _ => false, - } -} - -pub(crate) fn is_valid_identifier(s: &str, ascii_only: bool) -> bool { - if ascii_only { - if s.chars().any(|c| !c.is_ascii()) { - return false; - } - } - - s.starts_with(|c: char| c.is_xid_start()) - && s.chars().all(|c: char| c.is_xid_continue()) - && !s.contains("𝒶") - && !s.is_reserved() -} - pub(crate) fn replace_id_with_expr(node: &mut N, from: Id, to: Box) where N: VisitMutWith, diff --git a/ecmascript/minifier/src/compress/optimize/arrows.rs b/ecmascript/minifier/src/compress/pure/arrows.rs similarity index 81% rename from ecmascript/minifier/src/compress/optimize/arrows.rs rename to ecmascript/minifier/src/compress/pure/arrows.rs index 8b5ea23be1e..f49035b8bcf 100644 --- a/ecmascript/minifier/src/compress/optimize/arrows.rs +++ b/ecmascript/minifier/src/compress/pure/arrows.rs @@ -1,15 +1,11 @@ -use super::Optimizer; +use super::Pure; use swc_common::Spanned; -use swc_common::DUMMY_SP; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; -use swc_ecma_visit::noop_visit_type; -use swc_ecma_visit::Node; -use swc_ecma_visit::Visit; -use swc_ecma_visit::VisitWith; +use swc_ecma_utils::contains_this_expr; /// Methods related to the option `arrows`. -impl Optimizer<'_> { +impl Pure<'_> { pub(super) fn optimize_arrow_body(&mut self, b: &mut BlockStmtOrExpr) { if !self.options.arrows { return; @@ -43,12 +39,8 @@ impl Optimizer<'_> { match p { Prop::KeyValue(kv) => { // - { - let mut v = ThisVisitor { found: false }; - kv.value.visit_with(&Invalid { span: DUMMY_SP }, &mut v); - if v.found { - return; - } + if contains_this_expr(&kv.value) { + return; } match &mut *kv.value { @@ -90,15 +82,3 @@ impl Optimizer<'_> { } } } - -struct ThisVisitor { - found: bool, -} - -impl Visit for ThisVisitor { - noop_visit_type!(); - - fn visit_this_expr(&mut self, _: &ThisExpr, _: &dyn Node) { - self.found = true; - } -} diff --git a/ecmascript/minifier/src/compress/pure/bools.rs b/ecmascript/minifier/src/compress/pure/bools.rs new file mode 100644 index 00000000000..440280e1c7f --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/bools.rs @@ -0,0 +1,681 @@ +use std::mem::swap; + +use super::Pure; +use crate::compress::util::is_pure_undefined; +use crate::compress::util::negate; +use crate::compress::util::negate_cost; +use crate::util::make_bool; +use swc_atoms::js_word; +use swc_common::EqIgnoreSpan; +use swc_common::Span; +use swc_common::Spanned; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::ExprExt; +use swc_ecma_utils::Type; +use swc_ecma_utils::Value; + +impl Pure<'_> { + pub(super) fn negate_twice(&mut self, e: &mut Expr) { + self.changed = true; + negate(e, false); + negate(e, false); + } + + pub(super) fn negate(&mut self, e: &mut Expr, in_bool_ctx: bool) { + self.changed = true; + negate(e, in_bool_ctx) + } + + /// `!(a && b)` => `!a || !b` + pub(super) fn optimize_bools(&mut self, e: &mut Expr) { + if !self.options.bools { + return; + } + + match e { + Expr::Unary(UnaryExpr { + op: op!("!"), arg, .. + }) => match &mut **arg { + Expr::Bin(BinExpr { + op: op!("&&"), + left, + right, + .. + }) => { + if negate_cost(&left, false, false).unwrap_or(isize::MAX) >= 0 + || negate_cost(&right, false, false).unwrap_or(isize::MAX) >= 0 + { + return; + } + log::debug!("Optimizing `!(a && b)` as `!a || !b`"); + self.changed = true; + self.negate(arg, false); + *e = *arg.take(); + return; + } + + Expr::Unary(UnaryExpr { + op: op!("!"), + arg: arg_of_arg, + .. + }) => match &mut **arg_of_arg { + Expr::Bin(BinExpr { + op: op!("||"), + left, + right, + .. + }) => { + if negate_cost(&left, false, false).unwrap_or(isize::MAX) > 0 + && negate_cost(&right, false, false).unwrap_or(isize::MAX) > 0 + { + return; + } + log::debug!("Optimizing `!!(a || b)` as `!a && !b`"); + self.negate(arg_of_arg, false); + *e = *arg.take(); + return; + } + + _ => {} + }, + + _ => {} + }, + _ => {} + } + } + + pub(super) fn compress_cmp_of_typeof_with_lit(&mut self, e: &mut BinExpr) { + fn should_optimize(l: &Expr, r: &Expr) -> bool { + match (l, r) { + ( + Expr::Unary(UnaryExpr { + op: op!("typeof"), .. + }), + Expr::Lit(..), + ) => true, + _ => false, + } + } + + match e.op { + op!("===") | op!("!==") => {} + _ => return, + } + + if should_optimize(&e.left, &e.right) || should_optimize(&e.right, &e.left) { + log::debug!("bools: Compressing comparison of `typeof` with literal"); + self.changed = true; + e.op = match e.op { + op!("===") => { + op!("==") + } + op!("!==") => { + op!("!=") + } + _ => { + unreachable!() + } + } + } + } + + /// + /// - `!condition() || !-3.5` => `!condition()` + /// + /// In this case, if lhs is false, rhs is also false so it's removable. + pub(super) fn remove_useless_logical_rhs(&mut self, e: &mut Expr) { + if !self.options.bools { + return; + } + + match e { + Expr::Bin(BinExpr { + left, + op: op @ op!("&&"), + right, + .. + }) + | Expr::Bin(BinExpr { + left, + op: op @ op!("||"), + right, + .. + }) => { + let lt = left.get_type(); + let rt = right.get_type(); + + match (lt, rt) { + (Value::Known(Type::Bool), Value::Known(Type::Bool)) => { + let rb = right.as_pure_bool(); + let rb = match rb { + Value::Known(v) => v, + Value::Unknown => return, + }; + + // + let can_remove = if *op == op!("&&") { rb } else { !rb }; + + if can_remove { + if *op == op!("&&") { + log::debug!("booleans: Compressing `!foo && true` as `!foo`"); + } else { + log::debug!("booleans: Compressing `!foo || false` as `!foo`"); + } + self.changed = true; + *e = *left.take(); + return; + } + } + _ => {} + } + } + _ => {} + } + } + + /// Note: This should be invoked before calling `handle_negated_seq`. + pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) { + if !self.options.bools { + return; + } + + let delete = match e { + Expr::Unary( + u @ UnaryExpr { + op: op!("delete"), .. + }, + ) => u, + _ => return, + }; + + if delete.arg.may_have_side_effects() { + return; + } + + let convert_to_true = match &*delete.arg { + Expr::Seq(..) + | Expr::Cond(..) + | Expr::Bin(BinExpr { op: op!("&&"), .. }) + | Expr::Bin(BinExpr { op: op!("||"), .. }) => true, + // V8 and terser test ref have different opinion. + Expr::Ident(Ident { + sym: js_word!("Infinity"), + .. + }) => false, + Expr::Ident(Ident { + sym: js_word!("undefined"), + .. + }) => false, + Expr::Ident(Ident { + sym: js_word!("NaN"), + .. + }) => false, + + e if is_pure_undefined(&e) => true, + + Expr::Ident(..) => true, + + // NaN + Expr::Bin(BinExpr { + op: op!("/"), + right, + .. + }) => { + let rn = right.as_number(); + let v = if let Value::Known(rn) = rn { + if rn != 0.0 { + true + } else { + false + } + } else { + false + }; + + if v { + true + } else { + self.changed = true; + let span = delete.arg.span(); + log::debug!("booleans: Compressing `delete` as sequence expression"); + *e = Expr::Seq(SeqExpr { + span, + exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))], + }); + return; + } + } + + _ => false, + }; + + if convert_to_true { + self.changed = true; + let span = delete.arg.span(); + log::debug!("booleans: Compressing `delete` => true"); + *e = make_bool(span, true); + return; + } + } + + pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) { + match &mut *n { + Expr::Unary(e @ UnaryExpr { op: op!("!"), .. }) + | Expr::Unary( + e @ UnaryExpr { + op: op!("delete"), .. + }, + ) => { + match &mut *e.arg { + Expr::Seq(SeqExpr { exprs, .. }) => { + if exprs.is_empty() { + return; + } + log::debug!("bools: Optimizing negated sequences"); + + { + let last = exprs.last_mut().unwrap(); + self.optimize_expr_in_bool_ctx(last); + // Negate last element. + negate(last, false); + } + + *n = *e.arg.take(); + } + _ => {} + } + } + _ => {} + } + } + + /// This method converts `!1` to `0`. + pub(super) fn optimize_expr_in_bool_ctx(&mut self, n: &mut Expr) { + if !self.options.bools { + return; + } + + match n { + Expr::Bin(BinExpr { + op: op!("&&") | op!("||"), + left, + right, + .. + }) => { + // Regardless if it's truthy or falsy, we can optimize it because it will be + // casted as bool anyway. + self.optimize_expr_in_bool_ctx(&mut **left); + self.optimize_expr_in_bool_ctx(&mut **right); + return; + } + + Expr::Seq(e) => { + if let Some(last) = e.exprs.last_mut() { + self.optimize_expr_in_bool_ctx(&mut **last); + } + } + + _ => {} + } + + match n { + Expr::Unary(UnaryExpr { + span, + op: op!("!"), + arg, + }) => match &mut **arg { + Expr::Lit(Lit::Num(Number { value, .. })) => { + log::debug!("Optimizing: number => number (in bool context)"); + + self.changed = true; + *n = Expr::Lit(Lit::Num(Number { + span: *span, + value: if *value == 0.0 { 1.0 } else { 0.0 }, + })) + } + + Expr::Unary(UnaryExpr { + op: op!("!"), arg, .. + }) => { + log::debug!("bools: !!expr => expr (in bool ctx)"); + self.changed = true; + *n = *arg.take(); + return; + } + _ => {} + }, + + Expr::Unary(UnaryExpr { + span, + op: op!("typeof"), + arg, + }) => { + log::debug!("Optimizing: typeof => true (in bool context)"); + self.changed = true; + + match &**arg { + Expr::Ident(..) => { + *n = Expr::Lit(Lit::Num(Number { + span: *span, + value: 1.0, + })) + } + _ => { + // Return value of typeof is always truthy + let true_expr = Box::new(Expr::Lit(Lit::Num(Number { + span: *span, + value: 1.0, + }))); + *n = Expr::Seq(SeqExpr { + span: *span, + exprs: vec![arg.take(), true_expr], + }) + } + } + } + + Expr::Lit(Lit::Str(s)) => { + log::debug!("Converting string as boolean expressions"); + self.changed = true; + *n = Expr::Lit(Lit::Num(Number { + span: s.span, + value: if s.value.is_empty() { 0.0 } else { 1.0 }, + })); + } + + Expr::Lit(Lit::Num(num)) => { + if num.value == 1.0 || num.value == 0.0 { + return; + } + if self.options.bools { + log::debug!("booleans: Converting number as boolean expressions"); + self.changed = true; + *n = Expr::Lit(Lit::Num(Number { + span: num.span, + value: if num.value == 0.0 { 0.0 } else { 1.0 }, + })); + } + } + + Expr::Bin(BinExpr { + op: op!("??"), + left, + right, + .. + }) => { + // Optimize if (a ?? false); as if (a); + if let Value::Known(false) = right.as_pure_bool() { + log::debug!( + "Dropping right operand of `??` as it's always false (in bool context)" + ); + self.changed = true; + *n = *left.take(); + } + } + + Expr::Bin(BinExpr { + op: op!("||"), + left, + right, + .. + }) => { + // `a || false` => `a` (as it will be casted to boolean anyway) + + if let Value::Known(false) = right.as_pure_bool() { + log::debug!("bools: `expr || false` => `expr` (in bool context)"); + self.changed = true; + *n = *left.take(); + return; + } + } + + _ => { + let span = n.span(); + let v = n.as_pure_bool(); + if let Value::Known(v) = v { + log::debug!("Optimizing expr as {} (in bool context)", v); + *n = make_bool(span, v); + return; + } + } + } + } + + /// Rules: + /// - `l > i` => `i < l` + fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool { + match (l, r) { + (Expr::Member(_), _) if is_for_rel => false, + + (Expr::Update(..) | Expr::Assign(..), Expr::Lit(..)) if is_for_rel => false, + + ( + Expr::Member(..) + | Expr::Call(..) + | Expr::Assign(..) + | Expr::Update(..) + | Expr::Bin(BinExpr { + op: op!("&&") | op!("||"), + .. + }), + Expr::Lit(..), + ) => true, + + ( + Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), + Expr::Unary(UnaryExpr { + op: op!("!"), arg, .. + }), + ) if match &**arg { + Expr::Lit(..) => true, + _ => false, + } => + { + true + } + + (Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), r) if is_pure_undefined(r) => { + true + } + + (Expr::Ident(..), Expr::Lit(..)) if is_for_rel => false, + + (Expr::Ident(..), Expr::Ident(..)) => false, + + (Expr::Ident(..), Expr::Lit(..)) + | ( + Expr::Ident(..) | Expr::Member(..), + Expr::Unary(UnaryExpr { + op: op!("void") | op!("!"), + .. + }), + ) + | ( + Expr::This(..), + Expr::Unary(UnaryExpr { + op: op!("void"), .. + }), + ) + | (Expr::Unary(..), Expr::Lit(..)) + | (Expr::Tpl(..), Expr::Lit(..)) => true, + _ => false, + } + } + + fn try_swap_bin(&mut self, op: BinaryOp, left: &mut Expr, right: &mut Expr) -> bool { + fn is_supported(op: BinaryOp) -> bool { + match op { + op!("===") + | op!("!==") + | op!("==") + | op!("!=") + | op!("&") + | op!("^") + | op!("|") + | op!("*") => true, + _ => false, + } + } + + if !is_supported(op) { + return false; + } + + if self.can_swap_bin_operands(&left, &right, false) { + log::debug!("Swapping operands of binary exprssion"); + swap(left, right); + return true; + } + + false + } + + /// Swap lhs and rhs in certain conditions. + pub(super) fn swap_bin_operands(&mut self, expr: &mut Expr) { + match expr { + Expr::Bin(e @ BinExpr { op: op!("<="), .. }) + | Expr::Bin(e @ BinExpr { op: op!("<"), .. }) => { + if self.options.comparisons && self.can_swap_bin_operands(&e.left, &e.right, true) { + self.changed = true; + log::debug!("comparisons: Swapping operands of {}", e.op); + + e.op = if e.op == op!("<=") { + op!(">=") + } else { + op!(">") + }; + + swap(&mut e.left, &mut e.right); + } + } + + Expr::Bin(bin) => { + if self.try_swap_bin(bin.op, &mut bin.left, &mut bin.right) { + self.changed = true; + } + } + _ => {} + } + } + + /// + /// - `a === undefined || a === null` => `a == null` + pub(super) fn optimize_cmp_with_null_or_undefined(&mut self, e: &mut BinExpr) { + fn opt( + span: Span, + top_op: BinaryOp, + e_left: &mut Expr, + e_right: &mut Expr, + ) -> Option { + let (cmp, op, left, right) = match &mut *e_left { + Expr::Bin(left_bin) => { + if left_bin.op != op!("===") && left_bin.op != op!("!==") { + return None; + } + + if top_op == op!("&&") && left_bin.op == op!("===") { + return None; + } + if top_op == op!("||") && left_bin.op == op!("!==") { + return None; + } + + if !left_bin.right.is_ident() { + return None; + } + + let right = match &mut *e_right { + Expr::Bin(right_bin) => { + if right_bin.op != left_bin.op { + return None; + } + + if !right_bin.right.eq_ignore_span(&left_bin.right) { + return None; + } + + &mut *right_bin.left + } + _ => return None, + }; + + ( + &mut left_bin.right, + left_bin.op, + &mut *left_bin.left, + &mut *right, + ) + } + _ => return None, + }; + + let lt = left.get_type(); + let rt = right.get_type(); + if let Value::Known(lt) = lt { + if let Value::Known(rt) = rt { + match (lt, rt) { + (Type::Undefined, Type::Null) | (Type::Null, Type::Undefined) => { + if op == op!("===") { + log::debug!( + "Reducing `!== null || !== undefined` check to `!= null`" + ); + return Some(BinExpr { + span, + op: op!("=="), + left: cmp.take(), + right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), + }); + } else { + log::debug!( + "Reducing `=== null || === undefined` check to `== null`" + ); + return Some(BinExpr { + span, + op: op!("!="), + left: cmp.take(), + right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), + }); + } + } + _ => {} + } + } + } + + None + } + + if e.op == op!("||") || e.op == op!("&&") { + { + let res = opt(e.span, e.op, &mut e.left, &mut e.right); + if let Some(res) = res { + self.changed = true; + *e = res; + return; + } + } + + match (&mut *e.left, &mut *e.right) { + (Expr::Bin(left), right) => { + if e.op == left.op { + let res = opt(right.span(), e.op, &mut left.right, &mut *right); + if let Some(res) = res { + self.changed = true; + *e = BinExpr { + span: e.span, + op: e.op, + left: left.left.take(), + right: Box::new(Expr::Bin(res)), + }; + return; + } + } + } + _ => {} + } + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/conds.rs b/ecmascript/minifier/src/compress/pure/conds.rs new file mode 100644 index 00000000000..a6b6a96fc92 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/conds.rs @@ -0,0 +1,184 @@ +use super::Pure; +use crate::{compress::util::negate_cost, debug::dump, util::make_bool}; +use std::mem::swap; +use swc_common::{EqIgnoreSpan, Spanned}; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{ExprExt, Type, Value}; + +impl Pure<'_> { + /// + /// - `foo ? bar : false` => `!!foo && bar` + /// - `!foo ? true : bar` => `!foo || bar` + /// - `foo ? false : bar` => `!foo && bar` + pub(super) fn compress_conds_as_logical(&mut self, e: &mut Expr) { + let cond = match e { + Expr::Cond(cond) => cond, + _ => return, + }; + + let lt = cond.cons.get_type(); + if let Value::Known(Type::Bool) = lt { + let lb = cond.cons.as_pure_bool(); + if let Value::Known(true) = lb { + log::debug!("conditionals: `foo ? true : bar` => `!!foo || bar`"); + + // Negate twice to convert `test` to boolean. + self.negate_twice(&mut cond.test); + + self.changed = true; + *e = Expr::Bin(BinExpr { + span: cond.span, + op: op!("||"), + left: cond.test.take(), + right: cond.alt.take(), + }); + return; + } + + // TODO: Verify this rule. + if let Value::Known(false) = lb { + log::debug!("conditionals: `foo ? false : bar` => `!foo && bar`"); + + self.changed = true; + self.negate(&mut cond.test, false); + + *e = Expr::Bin(BinExpr { + span: cond.span, + op: op!("&&"), + left: cond.test.take(), + right: cond.alt.take(), + }); + return; + } + } + + let rt = cond.alt.get_type(); + if let Value::Known(Type::Bool) = rt { + let rb = cond.alt.as_pure_bool(); + if let Value::Known(false) = rb { + log::debug!("conditionals: `foo ? bar : false` => `!!foo && bar`"); + self.changed = true; + + // Negate twice to convert `test` to boolean. + self.negate_twice(&mut cond.test); + + *e = Expr::Bin(BinExpr { + span: cond.span, + op: op!("&&"), + left: cond.test.take(), + right: cond.cons.take(), + }); + return; + } + + if let Value::Known(true) = rb { + log::debug!("conditionals: `foo ? bar : true` => `!foo || bar"); + self.changed = true; + + self.negate(&mut cond.test, false); + + *e = Expr::Bin(BinExpr { + span: cond.span, + op: op!("||"), + left: cond.test.take(), + right: cond.cons.take(), + }); + return; + } + } + } + + pub(super) fn compress_cond_with_logical_as_logical(&mut self, e: &mut Expr) { + if !self.options.conditionals { + return; + } + + let cond = match e { + Expr::Cond(v) => v, + _ => return, + }; + + let cons_span = cond.cons.span(); + + match (&mut *cond.cons, &mut *cond.alt) { + (Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt) + if (*cons.right).eq_ignore_span(&*alt) => + { + log::debug!("conditionals: `x ? y || z : z` => `x || y && z`"); + self.changed = true; + + *e = Expr::Bin(BinExpr { + span: cond.span, + op: op!("||"), + left: Box::new(Expr::Bin(BinExpr { + span: cons_span, + op: op!("&&"), + left: cond.test.take(), + right: cons.left.take(), + })), + right: cons.right.take(), + }); + return; + } + _ => {} + } + } + + pub(super) fn negate_cond_expr(&mut self, cond: &mut CondExpr) { + if negate_cost(&cond.test, true, false).unwrap_or(isize::MAX) >= 0 { + return; + } + + self.changed = true; + log::debug!("conditionals: `a ? foo : bar` => `!a ? bar : foo` (considered cost)"); + let start_str = dump(&*cond); + + self.negate(&mut cond.test, true); + swap(&mut cond.cons, &mut cond.alt); + + if cfg!(feature = "debug") { + log::trace!( + "[Change] Negated cond: `{}` => `{}`", + start_str, + dump(&*cond) + ) + } + } + + /// Removes useless operands of an logical expressions. + pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) { + if !self.options.conditionals { + return; + } + + let bin = match e { + Expr::Bin(b) => b, + _ => return, + }; + + if bin.op != op!("||") && bin.op != op!("&&") { + return; + } + + if bin.left.may_have_side_effects() { + return; + } + + let lt = bin.left.get_type(); + let rt = bin.right.get_type(); + + let _lb = bin.left.as_pure_bool(); + let rb = bin.right.as_pure_bool(); + + if let (Value::Known(Type::Bool), Value::Known(Type::Bool)) = (lt, rt) { + // `!!b || true` => true + if let Value::Known(true) = rb { + self.changed = true; + log::debug!("conditionals: `!!foo || true` => `true`"); + *e = make_bool(bin.span, true); + return; + } + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/ctx.rs b/ecmascript/minifier/src/compress/pure/ctx.rs new file mode 100644 index 00000000000..df7f15dc0e2 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/ctx.rs @@ -0,0 +1,56 @@ +use super::Pure; +use std::ops::{Deref, DerefMut}; + +#[derive(Default, Clone, Copy)] +pub(super) struct Ctx { + pub par_depth: u8, + + pub in_delete: bool, + + /// `true` if we are in `arg` of `++arg` or `--arg`. + pub is_update_arg: bool, + + pub is_callee: bool, + + pub in_try_block: bool, + + pub is_lhs_of_assign: bool, +} + +impl<'b> Pure<'b> { + /// RAII guard to change context temporarically + #[inline] + pub(super) fn with_ctx(&mut self, ctx: Ctx) -> WithCtx<'_, 'b> { + let orig_ctx = self.ctx; + self.ctx = ctx; + WithCtx { + pass: self, + orig_ctx, + } + } +} + +pub(super) struct WithCtx<'a, 'b> { + pass: &'a mut Pure<'b>, + orig_ctx: Ctx, +} + +impl<'b> Deref for WithCtx<'_, 'b> { + type Target = Pure<'b>; + + fn deref(&self) -> &Self::Target { + &self.pass + } +} + +impl DerefMut for WithCtx<'_, '_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.pass + } +} + +impl Drop for WithCtx<'_, '_> { + fn drop(&mut self) { + self.pass.ctx = self.orig_ctx; + } +} diff --git a/ecmascript/minifier/src/compress/pure/dead_code.rs b/ecmascript/minifier/src/compress/pure/dead_code.rs new file mode 100644 index 00000000000..2ff848438ae --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/dead_code.rs @@ -0,0 +1,85 @@ +use super::Pure; +use swc_atoms::js_word; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::StmtLike; + +/// Methods related to option `dead_code`. +impl Pure<'_> { + pub(super) fn drop_useless_blocks(&mut self, stmts: &mut Vec) + where + T: StmtLike, + { + fn is_inliable(b: &BlockStmt) -> bool { + b.stmts.iter().all(|s| match s { + Stmt::Decl(Decl::Fn(FnDecl { + ident: + Ident { + sym: js_word!("undefined"), + .. + }, + .. + })) => false, + + Stmt::Decl( + Decl::Var(VarDecl { + kind: VarDeclKind::Var, + .. + }) + | Decl::Fn(..), + ) => true, + Stmt::Decl(..) => false, + _ => true, + }) + } + + if stmts.iter().all(|stmt| match stmt.as_stmt() { + Some(Stmt::Block(b)) if is_inliable(b) => false, + _ => true, + }) { + return; + } + + self.changed = true; + log::debug!("Dropping useless block"); + + let mut new = vec![]; + for stmt in stmts.take() { + match stmt.try_into_stmt() { + Ok(v) => match v { + Stmt::Block(v) if is_inliable(&v) => { + new.extend(v.stmts.into_iter().map(T::from_stmt)); + } + _ => new.push(T::from_stmt(v)), + }, + Err(v) => { + new.push(v); + } + } + } + + *stmts = new; + } + + pub(super) fn drop_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) { + match s { + Stmt::Return(r) => match r.arg.as_deref_mut() { + Some(Expr::Unary(UnaryExpr { + span, + op: op!("void"), + arg, + })) => { + log::debug!("unused: Removing `return void` in end of a function"); + self.changed = true; + *s = Stmt::Expr(ExprStmt { + span: *span, + expr: arg.take(), + }); + return; + } + _ => {} + }, + _ => {} + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/evaluate.rs b/ecmascript/minifier/src/compress/pure/evaluate.rs new file mode 100644 index 00000000000..4592cbb4e95 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/evaluate.rs @@ -0,0 +1,465 @@ +use super::Pure; +use crate::compress::util::{eval_as_number, is_pure_undefined_or_null}; +use swc_atoms::js_word; +use swc_common::{Spanned, SyntaxContext}; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{undefined, ExprExt, Value}; + +impl Pure<'_> { + pub(super) fn eval_array_method_call(&mut self, e: &mut Expr) { + if !self.options.evaluate { + return; + } + + if self.ctx.in_delete || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign { + return; + } + + let call = match e { + Expr::Call(e) => e, + _ => return, + }; + + let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); + + for arg in &call.args { + if arg.expr.may_have_side_effects() { + return; + } + } + + let callee = match &mut call.callee { + ExprOrSuper::Super(_) => return, + ExprOrSuper::Expr(e) => &mut **e, + }; + + match callee { + Expr::Member(MemberExpr { + span, + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + }) => { + if obj.may_have_side_effects() { + return; + } + + let arr = match &mut **obj { + Expr::Array(arr) => arr, + _ => return, + }; + + let has_spread_elem = arr.elems.iter().any(|s| match s { + Some(ExprOrSpread { + spread: Some(..), .. + }) => true, + _ => false, + }); + + // Ignore array + + let method_name = match &**prop { + Expr::Ident(i) => i, + _ => return, + }; + + match &*method_name.sym { + "slice" => { + if has_spread || has_spread_elem { + return; + } + + match call.args.len() { + 0 => { + self.changed = true; + log::debug!("evaluate: Dropping array.slice call"); + *e = *obj.take(); + return; + } + 1 => { + if let Value::Known(start) = call.args[0].expr.as_number() { + let start = start.floor() as usize; + + self.changed = true; + log::debug!("evaluate: Reducing array.slice({}) call", start); + + if start >= arr.elems.len() { + *e = Expr::Array(ArrayLit { + span: *span, + elems: Default::default(), + }); + return; + } + + let elems = arr.elems.drain(start..).collect(); + + *e = Expr::Array(ArrayLit { span: *span, elems }); + return; + } + } + _ => { + let start = call.args[0].expr.as_number(); + let end = call.args[1].expr.as_number(); + if let Value::Known(start) = start { + let start = start.floor() as usize; + + if let Value::Known(end) = end { + let end = end.floor() as usize; + self.changed = true; + log::debug!( + "evaluate: Reducing array.slice({}, {}) call", + start, + end + ); + let end = end.min(arr.elems.len()); + + if start >= arr.elems.len() { + *e = Expr::Array(ArrayLit { + span: *span, + elems: Default::default(), + }); + return; + } + + let elems = arr.elems.drain(start..end).collect(); + + *e = Expr::Array(ArrayLit { span: *span, elems }); + return; + } + } + } + } + } + _ => {} + } + } + _ => {} + } + } + + pub(super) fn eval_fn_method_call(&mut self, e: &mut Expr) { + if !self.options.evaluate { + return; + } + + if self.ctx.in_delete || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign { + return; + } + + let call = match e { + Expr::Call(e) => e, + _ => return, + }; + + let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); + + for arg in &call.args { + if arg.expr.may_have_side_effects() { + return; + } + } + + let callee = match &mut call.callee { + ExprOrSuper::Super(_) => return, + ExprOrSuper::Expr(e) => &mut **e, + }; + + match callee { + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + .. + }) => { + if obj.may_have_side_effects() { + return; + } + + let _f = match &mut **obj { + Expr::Fn(v) => v, + _ => return, + }; + + let method_name = match &**prop { + Expr::Ident(i) => i, + _ => return, + }; + + match &*method_name.sym { + "valueOf" => { + if has_spread { + return; + } + + self.changed = true; + log::debug!( + "evaludate: Reduced `funtion.valueOf()` into a function expression" + ); + + *e = *obj.take(); + return; + } + _ => {} + } + } + _ => {} + } + } + + /// unsafely evaulate call to `Number`. + pub(super) fn eval_number_call(&mut self, e: &mut Expr) { + if self.options.unsafe_passes && self.options.unsafe_math { + match e { + Expr::Call(CallExpr { + span, + callee: ExprOrSuper::Expr(callee), + args, + .. + }) => { + if args.len() == 1 && args[0].spread.is_none() { + match &**callee { + Expr::Ident(Ident { + sym: js_word!("Number"), + .. + }) => { + self.changed = true; + log::debug!( + "evaluate: Reducing a call to `Number` into an unary operation" + ); + + *e = Expr::Unary(UnaryExpr { + span: *span, + op: op!(unary, "+"), + arg: args.take().into_iter().next().unwrap().expr, + }); + } + _ => {} + } + } + } + _ => {} + } + } + } + + /// Evaluates method calls of a numeric constant. + + pub(super) fn eval_number_method_call(&mut self, e: &mut Expr) { + if !self.options.evaluate { + return; + } + + let (num, method, args) = match e { + Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(callee), + args, + .. + }) => match &mut **callee { + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + .. + }) => match &mut **obj { + Expr::Lit(Lit::Num(obj)) => match &mut **prop { + Expr::Ident(prop) => (obj, prop, args), + _ => return, + }, + _ => return, + }, + _ => return, + }, + _ => return, + }; + + if args.iter().any(|arg| arg.expr.may_have_side_effects()) { + return; + } + + match &*method.sym { + "toFixed" => { + if let Some(precision) = eval_as_number(&args[0].expr) { + let precision = precision.floor() as usize; + let value = num_to_fixed(num.value, precision + 1); + + self.changed = true; + log::debug!( + "evaluate: Evaluating `{}.toFixed({})` as `{}`", + num, + precision, + value + ); + + *e = Expr::Lit(Lit::Str(Str { + span: e.span(), + value: value.into(), + has_escape: false, + kind: Default::default(), + })) + } + } + _ => {} + } + } + + pub(super) fn eval_opt_chain(&mut self, e: &mut Expr) { + let opt = match e { + Expr::OptChain(e) => e, + _ => return, + }; + + match &mut *opt.expr { + Expr::Member(MemberExpr { + span, + obj: ExprOrSuper::Expr(obj), + .. + }) => { + // + if is_pure_undefined_or_null(&obj) { + self.changed = true; + log::debug!( + "evaluate: Reduced an optioanl chaining operation because object is \ + always null or undefined" + ); + + *e = *undefined(*span); + return; + } + } + + Expr::Call(CallExpr { + span, + callee: ExprOrSuper::Expr(callee), + .. + }) => { + if is_pure_undefined_or_null(&callee) { + self.changed = true; + log::debug!( + "evaluate: Reduced a call expression with optioanl chaining operation \ + because object is always null or undefined" + ); + + *e = *undefined(*span); + return; + } + } + + _ => {} + } + } +} + +/// Evaluation of strings. +impl Pure<'_> { + /// Handle calls on string literals, like `'foo'.toUpperCase()`. + pub(super) fn eval_str_method_call(&mut self, e: &mut Expr) { + if !self.options.evaluate { + return; + } + + if self.ctx.in_delete || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign { + return; + } + + let call = match e { + Expr::Call(v) => v, + _ => return, + }; + + let (s, method) = match &call.callee { + ExprOrSuper::Super(_) => return, + ExprOrSuper::Expr(callee) => match &**callee { + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + .. + }) => match (&**obj, &**prop) { + (Expr::Lit(Lit::Str(s)), Expr::Ident(prop)) => (s.clone(), prop.sym.clone()), + _ => return, + }, + _ => return, + }, + }; + + let new_val = match &*method { + "toLowerCase" => s.value.to_lowercase(), + "toUpperCase" => s.value.to_uppercase(), + "charCodeAt" => { + if call.args.len() != 1 { + return; + } + if let Expr::Lit(Lit::Num(Number { value, .. })) = &*call.args[0].expr { + if value.fract() != 0.0 { + return; + } + + let idx = value.round() as i64 as usize; + let c = s.value.chars().nth(idx); + match c { + Some(v) => { + self.changed = true; + log::debug!( + "evaluate: Evaluated `charCodeAt` of a string literal as `{}`", + v + ); + *e = Expr::Lit(Lit::Num(Number { + span: call.span, + value: v as usize as f64, + })) + } + None => { + self.changed = true; + log::debug!( + "evaluate: Evaluated `charCodeAt` of a string literal as `NaN`", + ); + *e = Expr::Ident(Ident::new( + js_word!("NaN"), + e.span().with_ctxt(SyntaxContext::empty()), + )) + } + } + } + return; + } + _ => return, + }; + + self.changed = true; + log::debug!("evaluate: Evaluated `{}` of a string literal", method); + *e = Expr::Lit(Lit::Str(Str { + value: new_val.into(), + ..s + })); + } +} + +/// https://stackoverflow.com/questions/60497397/how-do-you-format-a-float-to-the-first-significant-decimal-and-with-specified-pr +fn num_to_fixed(float: f64, precision: usize) -> String { + // compute absolute value + let a = float.abs(); + + // if abs value is greater than 1, then precision becomes less than "standard" + let precision = if a >= 1. { + // reduce by number of digits, minimum 0 + let n = (1. + a.log10().floor()) as usize; + if n <= precision { + precision - n + } else { + 0 + } + // if precision is less than 1 (but non-zero), then precision becomes + // greater than "standard" + } else if a > 0. { + // increase number of digits + let n = -(1. + a.log10().floor()) as usize; + precision + n + // special case for 0 + } else { + 0 + }; + + // format with the given computed precision + format!("{0:.1$}", float, precision) +} diff --git a/ecmascript/minifier/src/compress/pure/loops.rs b/ecmascript/minifier/src/compress/pure/loops.rs new file mode 100644 index 00000000000..ca9f1bb74fc --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/loops.rs @@ -0,0 +1,110 @@ +use super::Pure; +use swc_common::Spanned; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{ExprExt, Value}; + +impl Pure<'_> { + /// + /// - `while(test);` => `for(;;test); + /// - `do; while(true)` => `for(;;); + pub(super) fn loop_to_for_stmt(&mut self, s: &mut Stmt) { + if !self.options.loops { + return; + } + + match s { + Stmt::While(stmt) => { + self.changed = true; + log::debug!("loops: Converting a while loop to a for loop"); + *s = Stmt::For(ForStmt { + span: stmt.span, + init: None, + test: Some(stmt.test.take()), + update: None, + body: stmt.body.take(), + }); + } + Stmt::DoWhile(stmt) => { + let val = stmt.test.as_pure_bool(); + if let Value::Known(true) = val { + self.changed = true; + log::debug!("loops: Converting an always-true do-while loop to a for loop"); + + *s = Stmt::For(ForStmt { + span: stmt.span, + init: None, + test: Some(stmt.test.take()), + update: None, + body: stmt.body.take(), + }); + } + } + _ => {} + } + } + + /// # Input + /// + /// ```js + /// for(; size--;) + /// if (!(result = eq(a[size], b[size], aStack, bStack))) + /// break; + /// ``` + /// + /// + /// # Output + /// + /// ```js + /// for (; size-- && (result = eq(a[size], b[size], aStack, bStack));); + /// ``` + pub(super) fn merge_for_if_break(&mut self, s: &mut ForStmt) { + match &mut *s.body { + Stmt::If(IfStmt { + test, + cons, + alt: None, + .. + }) => { + match &**cons { + Stmt::Break(BreakStmt { label: None, .. }) => { + // We only care about instant breaks. + // + // Note: As the minifier of swc is very fast, we don't + // care about block statements with a single break as a + // body. + // + // If it's optimizable, other pass for if statements + // will remove block and with the next pass we can apply + // this pass. + self.changed = true; + log::debug!("loops: Compressing for-if-break into a for statement"); + + // We negate because this `test` is used as a condition for `break`. + self.negate(test, true); + + match s.test.take() { + Some(left) => { + s.test = Some(Box::new(Expr::Bin(BinExpr { + span: s.test.span(), + op: op!("&&"), + left, + right: test.take(), + }))); + } + None => { + s.test = Some(test.take()); + } + } + + // Remove body + s.body.take(); + } + _ => {} + } + } + + _ => {} + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/misc.rs b/ecmascript/minifier/src/compress/pure/misc.rs new file mode 100644 index 00000000000..f56fc86ffc0 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/misc.rs @@ -0,0 +1,26 @@ +use super::Pure; +use crate::compress::util::is_pure_undefined; +use swc_ecma_ast::*; + +impl Pure<'_> { + pub(super) fn drop_undefined_from_return_arg(&mut self, s: &mut ReturnStmt) { + match s.arg.as_deref() { + Some(e) => { + if is_pure_undefined(e) { + self.changed = true; + log::debug!("Dropped `undefined` from `return undefined`"); + s.arg.take(); + } + } + None => {} + } + } + + pub(super) fn remove_useless_return(&mut self, stmts: &mut Vec) { + if let Some(Stmt::Return(ReturnStmt { arg: None, .. })) = stmts.last() { + self.changed = true; + log::debug!("misc: Removing useless return"); + stmts.pop(); + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/mod.rs b/ecmascript/minifier/src/compress/pure/mod.rs new file mode 100644 index 00000000000..f854e4a5e4d --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/mod.rs @@ -0,0 +1,420 @@ +use self::ctx::Ctx; +use crate::{marks::Marks, option::CompressOptions, util::MoudleItemExt, MAX_PAR_DEPTH}; +use rayon::prelude::*; +use swc_common::{pass::Repeated, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith}; + +mod arrows; +mod bools; +mod conds; +mod ctx; +mod dead_code; +mod evaluate; +mod loops; +mod misc; +mod numbers; +mod properties; +mod sequences; +mod strings; +mod unsafes; +mod vars; + +pub(super) fn pure_optimizer<'a>( + options: &'a CompressOptions, + marks: Marks, +) -> impl 'a + VisitMut + Repeated { + Pure { + options, + marks, + ctx: Default::default(), + changed: Default::default(), + } +} + +struct Pure<'a> { + options: &'a CompressOptions, + marks: Marks, + ctx: Ctx, + changed: bool, +} + +impl Repeated for Pure<'_> { + fn changed(&self) -> bool { + self.changed + } + + fn reset(&mut self) { + self.ctx = Default::default(); + self.changed = false; + } +} + +impl Pure<'_> { + fn handle_stmt_likes(&mut self, stmts: &mut Vec) + where + T: MoudleItemExt, + Vec: VisitWith + + VisitMutWith + + VisitMutWith, + { + self.drop_useless_blocks(stmts); + + self.collapse_vars_without_init(stmts); + } + + /// Visit `nodes`, maybe in parallel. + fn visit_par(&mut self, nodes: &mut Vec) + where + N: for<'aa> VisitMutWith> + Send + Sync, + { + if self.ctx.par_depth >= MAX_PAR_DEPTH * 2 || cfg!(target_arch = "wasm32") { + for node in nodes { + let mut v = Pure { + options: self.options, + marks: self.marks, + ctx: self.ctx, + changed: false, + }; + node.visit_mut_with(&mut v); + + self.changed |= v.changed; + } + } else { + let results = nodes + .par_iter_mut() + .map(|node| { + let mut v = Pure { + options: self.options, + marks: self.marks, + ctx: Ctx { + par_depth: self.ctx.par_depth + 1, + ..self.ctx + }, + changed: false, + }; + node.visit_mut_with(&mut v); + + v.changed + }) + .collect::>(); + + for res in results { + self.changed |= res; + } + } + } +} + +impl VisitMut for Pure<'_> { + noop_visit_mut_type!(); + + fn visit_mut_assign_expr(&mut self, e: &mut AssignExpr) { + { + let ctx = Ctx { + is_lhs_of_assign: true, + ..self.ctx + }; + e.left.visit_mut_children_with(&mut *self.with_ctx(ctx)); + } + + e.right.visit_mut_with(self); + } + + fn visit_mut_bin_expr(&mut self, e: &mut BinExpr) { + e.visit_mut_children_with(self); + + self.compress_cmp_of_typeof_with_lit(e); + + self.optimize_cmp_with_null_or_undefined(e); + + if e.op == op!(bin, "+") { + self.concat_tpl(&mut e.left, &mut e.right); + } + } + + fn visit_mut_block_stmt_or_expr(&mut self, body: &mut BlockStmtOrExpr) { + body.visit_mut_children_with(self); + + self.optimize_arrow_body(body); + } + + fn visit_mut_call_expr(&mut self, e: &mut CallExpr) { + { + let ctx = Ctx { + is_callee: true, + ..self.ctx + }; + e.callee.visit_mut_with(&mut *self.with_ctx(ctx)); + } + + e.args.visit_mut_with(self); + + self.drop_arguemtns_of_symbol_call(e); + } + + fn visit_mut_cond_expr(&mut self, e: &mut CondExpr) { + e.visit_mut_children_with(self); + + self.optimize_expr_in_bool_ctx(&mut e.test); + + self.negate_cond_expr(e); + } + + fn visit_mut_expr(&mut self, e: &mut Expr) { + e.visit_mut_children_with(self); + + match e { + Expr::Seq(seq) => { + if seq.exprs.is_empty() { + *e = Expr::Invalid(Invalid { span: DUMMY_SP }); + return; + } + } + _ => {} + } + + self.eval_opt_chain(e); + + self.eval_number_call(e); + + self.eval_number_method_call(e); + + self.swap_bin_operands(e); + + self.handle_property_access(e); + + self.optimize_bools(e); + + self.drop_logical_operands(e); + + self.lift_minus(e); + + self.convert_tpl_to_str(e); + + self.drop_useless_addition_of_str(e); + + self.compress_useless_deletes(e); + + self.remove_useless_logical_rhs(e); + + self.handle_negated_seq(e); + + self.concat_str(e); + + self.eval_array_method_call(e); + + self.eval_fn_method_call(e); + + self.eval_str_method_call(e); + + self.compress_conds_as_logical(e); + + self.compress_cond_with_logical_as_logical(e); + + self.lift_seqs_of_bin(e); + + self.lift_seqs_of_cond_assign(e); + } + + fn visit_mut_exprs(&mut self, exprs: &mut Vec>) { + self.visit_par(exprs); + } + + fn visit_mut_for_stmt(&mut self, s: &mut ForStmt) { + s.visit_mut_children_with(self); + + self.merge_for_if_break(s); + + if let Some(test) = &mut s.test { + self.optimize_expr_in_bool_ctx(&mut **test); + } + } + + fn visit_mut_function(&mut self, f: &mut Function) { + { + let ctx = Ctx { + in_try_block: false, + ..self.ctx + }; + f.visit_mut_children_with(&mut *self.with_ctx(ctx)); + } + + if let Some(body) = &mut f.body { + self.remove_useless_return(&mut body.stmts); + + if let Some(last) = body.stmts.last_mut() { + self.drop_unused_stmt_at_end_of_fn(last); + } + } + } + + fn visit_mut_if_stmt(&mut self, s: &mut IfStmt) { + s.visit_mut_children_with(self); + + self.optimize_expr_in_bool_ctx(&mut s.test); + } + + fn visit_mut_member_expr(&mut self, e: &mut MemberExpr) { + e.obj.visit_mut_with(self); + if e.computed { + e.prop.visit_mut_with(self); + } + + self.optimize_property_of_member_expr(e); + + self.handle_known_computed_member_expr(e); + } + + fn visit_mut_module_items(&mut self, items: &mut Vec) { + self.visit_par(items); + + self.handle_stmt_likes(items); + } + + fn visit_mut_new_expr(&mut self, e: &mut NewExpr) { + { + let ctx = Ctx { + is_callee: true, + ..self.ctx + }; + e.callee.visit_mut_with(&mut *self.with_ctx(ctx)); + } + + e.args.visit_mut_with(self); + } + + fn visit_mut_prop(&mut self, p: &mut Prop) { + p.visit_mut_children_with(self); + + self.optimize_arrow_method_prop(p); + } + + fn visit_mut_prop_name(&mut self, p: &mut PropName) { + p.visit_mut_children_with(self); + + self.optimize_computed_prop_name_as_normal(p); + self.optimize_prop_name(p); + } + + fn visit_mut_prop_or_spreads(&mut self, exprs: &mut Vec) { + self.visit_par(exprs); + } + + fn visit_mut_return_stmt(&mut self, s: &mut ReturnStmt) { + s.visit_mut_children_with(self); + + self.drop_undefined_from_return_arg(s); + } + + fn visit_mut_seq_expr(&mut self, e: &mut SeqExpr) { + e.visit_mut_children_with(self); + + self.drop_useless_ident_ref_in_seq(e); + + self.merge_seq_call(e); + } + + fn visit_mut_stmt(&mut self, s: &mut Stmt) { + { + let ctx = Ctx { + is_update_arg: false, + is_callee: false, + in_delete: false, + ..self.ctx + }; + s.visit_mut_children_with(&mut *self.with_ctx(ctx)); + } + + if self.options.drop_debugger { + match s { + Stmt::Debugger(..) => { + self.changed = true; + *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + log::debug!("drop_debugger: Dropped a debugger statement"); + return; + } + _ => {} + } + } + + self.loop_to_for_stmt(s); + } + + fn visit_mut_stmts(&mut self, items: &mut Vec) { + self.visit_par(items); + + self.handle_stmt_likes(items); + + items.retain(|s| match s { + Stmt::Empty(..) => false, + _ => true, + }); + } + + /// We don't optimize [Tpl] contained in [TaggedTpl]. + fn visit_mut_tagged_tpl(&mut self, n: &mut TaggedTpl) { + n.tag.visit_mut_with(self); + } + + fn visit_mut_tpl(&mut self, n: &mut Tpl) { + n.visit_mut_children_with(self); + debug_assert_eq!(n.exprs.len() + 1, n.quasis.len()); + + self.compress_tpl(n); + + debug_assert_eq!( + n.exprs.len() + 1, + n.quasis.len(), + "tagged template literal compressor created an invalid template literal" + ); + } + + fn visit_mut_try_stmt(&mut self, n: &mut TryStmt) { + let ctx = Ctx { + in_try_block: true, + ..self.ctx + }; + n.block.visit_mut_with(&mut *self.with_ctx(ctx)); + + n.handler.visit_mut_with(self); + + n.finalizer.visit_mut_with(self); + } + + fn visit_mut_unary_expr(&mut self, e: &mut UnaryExpr) { + { + let ctx = Ctx { + in_delete: e.op == op!("delete"), + ..self.ctx + }; + e.visit_mut_children_with(&mut *self.with_ctx(ctx)); + } + + match e.op { + op!("!") => { + self.optimize_expr_in_bool_ctx(&mut e.arg); + } + + op!(unary, "+") | op!(unary, "-") => { + self.optimize_expr_in_num_ctx(&mut e.arg); + } + _ => {} + } + } + + fn visit_mut_update_expr(&mut self, e: &mut UpdateExpr) { + let ctx = Ctx { + is_update_arg: true, + ..self.ctx + }; + + e.visit_mut_children_with(&mut *self.with_ctx(ctx)); + } + + fn visit_mut_while_stmt(&mut self, s: &mut WhileStmt) { + s.visit_mut_children_with(self); + + self.optimize_expr_in_bool_ctx(&mut s.test); + } +} diff --git a/ecmascript/minifier/src/compress/optimize/numbers.rs b/ecmascript/minifier/src/compress/pure/numbers.rs similarity index 97% rename from ecmascript/minifier/src/compress/optimize/numbers.rs rename to ecmascript/minifier/src/compress/pure/numbers.rs index 6a14fc56411..beb21d15cda 100644 --- a/ecmascript/minifier/src/compress/optimize/numbers.rs +++ b/ecmascript/minifier/src/compress/pure/numbers.rs @@ -1,8 +1,8 @@ -use crate::compress::optimize::Optimizer; +use super::Pure; use swc_ecma_ast::*; use swc_ecma_transforms_base::ext::MapWithMut; -impl Optimizer<'_> { +impl Pure<'_> { pub(super) fn optimize_expr_in_num_ctx(&mut self, e: &mut Expr) { match e { Expr::Lit(Lit::Str(Str { span, value, .. })) => { diff --git a/ecmascript/minifier/src/compress/optimize/properties.rs b/ecmascript/minifier/src/compress/pure/properties.rs similarity index 73% rename from ecmascript/minifier/src/compress/optimize/properties.rs rename to ecmascript/minifier/src/compress/pure/properties.rs index 23657e58890..5f4478ae8cc 100644 --- a/ecmascript/minifier/src/compress/optimize/properties.rs +++ b/ecmascript/minifier/src/compress/pure/properties.rs @@ -1,5 +1,5 @@ -use super::util::is_valid_identifier; -use crate::compress::optimize::Optimizer; +use super::Pure; +use crate::compress::util::is_valid_identifier; use crate::util::deeply_contains_this_expr; use swc_atoms::js_word; use swc_common::SyntaxContext; @@ -7,7 +7,7 @@ use swc_ecma_ast::*; use swc_ecma_utils::prop_name_eq; use swc_ecma_utils::ExprExt; -impl Optimizer<'_> { +impl Pure<'_> { pub(super) fn optimize_property_of_member_expr(&mut self, e: &mut MemberExpr) { if !e.computed { return; @@ -47,6 +47,63 @@ impl Optimizer<'_> { } } + /// If a key of is `'str'` (like `{ 'str': 1 }`) change it to [Ident] like + /// (`{ str: 1, }`) + pub(super) fn optimize_computed_prop_name_as_normal(&mut self, p: &mut PropName) { + if !self.options.computed_props { + return; + } + + match p { + PropName::Computed(c) => match &mut *c.expr { + Expr::Lit(Lit::Str(s)) => { + if s.value == *"constructor" || s.value == *"__proto__" { + return; + } + + if s.value.is_empty() || s.value.starts_with(|c: char| c.is_numeric()) { + *p = PropName::Str(s.clone()); + } else { + *p = PropName::Ident(Ident::new( + s.value.clone(), + s.span.with_ctxt(SyntaxContext::empty()), + )); + } + + return; + } + Expr::Lit(Lit::Num(n)) => { + *p = PropName::Num(n.clone()); + return; + } + _ => {} + }, + _ => {} + } + } + + pub(super) fn optimize_prop_name(&mut self, name: &mut PropName) { + match name { + PropName::Str(s) => { + if s.value.is_reserved() || s.value.is_reserved_in_es3() { + return; + } + + if is_valid_identifier(&s.value, false) { + self.changed = true; + log::debug!("misc: Optimizing string property name"); + *name = PropName::Ident(Ident { + span: s.span, + sym: s.value.clone(), + optional: false, + }); + return; + } + } + _ => {} + } + } + /// Converts `{ a: 1 }.a` into `1`. pub(super) fn handle_property_access(&mut self, e: &mut Expr) { if !self.options.props { @@ -57,6 +114,10 @@ impl Optimizer<'_> { return; } + if self.ctx.is_callee { + return; + } + let me = match e { Expr::Member(m) => m, _ => return, @@ -100,10 +161,6 @@ impl Optimizer<'_> { return; } - if self.ctx.is_callee { - return; - } - if obj.props.iter().any(|prop| match prop { PropOrSpread::Spread(_) => false, PropOrSpread::Prop(p) => match &**p { diff --git a/ecmascript/minifier/src/compress/pure/sequences.rs b/ecmascript/minifier/src/compress/pure/sequences.rs new file mode 100644 index 00000000000..a95dbd126e8 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/sequences.rs @@ -0,0 +1,218 @@ +use crate::compress::util::get_lhs_ident; + +use super::Pure; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{ExprExt, ExprFactory}; + +impl Pure<'_> { + pub(super) fn drop_useless_ident_ref_in_seq(&mut self, seq: &mut SeqExpr) { + if !self.options.collapse_vars { + return; + } + + if seq.exprs.len() < 2 { + return; + } + + match ( + &*seq.exprs[seq.exprs.len() - 2], + &*seq.exprs[seq.exprs.len() - 1], + ) { + (Expr::Assign(assign), Expr::Ident(ident)) => { + // Check if lhs is same as `ident`. + match &assign.left { + PatOrExpr::Expr(_) => {} + PatOrExpr::Pat(left) => match &**left { + Pat::Ident(left) => { + if left.id.sym == ident.sym && left.id.span.ctxt == ident.span.ctxt { + seq.exprs.pop(); + } + } + _ => {} + }, + } + } + _ => {} + } + } + + /// + /// - `(a, b, c) && d` => `a, b, c && d` + pub(super) fn lift_seqs_of_bin(&mut self, e: &mut Expr) { + let bin = match e { + Expr::Bin(b) => b, + _ => return, + }; + + match &mut *bin.left { + Expr::Seq(left) => { + if left.exprs.is_empty() { + return; + } + + self.changed = true; + log::debug!("sequences: Lifting sequence in a binary expression"); + + let left_last = left.exprs.pop().unwrap(); + + let mut exprs = left.exprs.take(); + + exprs.push(Box::new(Expr::Bin(BinExpr { + span: left.span, + op: bin.op, + left: left_last, + right: bin.right.take(), + }))); + + *e = Expr::Seq(SeqExpr { + span: bin.span, + exprs, + }) + } + _ => {} + } + } + + /// + /// - `x = (foo(), bar(), baz()) ? 10 : 20` => `foo(), bar(), x = baz() ? 10 + /// : 20;` + pub(super) fn lift_seqs_of_cond_assign(&mut self, e: &mut Expr) { + if !self.options.sequences() { + return; + } + + let assign = match e { + Expr::Assign(v) => v, + _ => return, + }; + + let cond = match &mut *assign.right { + Expr::Cond(v) => v, + _ => return, + }; + + match &mut *cond.test { + Expr::Seq(test) => { + // + if test.exprs.len() >= 2 { + let mut new_seq = vec![]; + new_seq.extend(test.exprs.drain(..test.exprs.len() - 1)); + + self.changed = true; + log::debug!("sequences: Lifting sequences in a assignment with cond expr"); + let new_cond = CondExpr { + span: cond.span, + test: test.exprs.pop().unwrap(), + cons: cond.cons.take(), + alt: cond.alt.take(), + }; + + new_seq.push(Box::new(Expr::Assign(AssignExpr { + span: assign.span, + op: assign.op, + left: assign.left.take(), + right: Box::new(Expr::Cond(new_cond)), + }))); + + *e = Expr::Seq(SeqExpr { + span: assign.span, + exprs: new_seq, + }); + return; + } + } + _ => {} + } + } + + /// `(a = foo, a.apply())` => `(a = foo).apply()` + /// + /// This is useful for outputs of swc/babel + pub(super) fn merge_seq_call(&mut self, e: &mut SeqExpr) { + if !self.options.sequences() { + return; + } + + for idx in 0..e.exprs.len() { + let (e1, e2) = e.exprs.split_at_mut(idx); + + let a = match e1.last_mut() { + Some(v) => &mut **v, + None => continue, + }; + + let b = match e2.first_mut() { + Some(v) => &mut **v, + None => continue, + }; + + match (&mut *a, &mut *b) { + ( + Expr::Assign(a_assign), + Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(b_callee), + args, + .. + }), + ) => { + let var_name = get_lhs_ident(&a_assign.left); + let var_name = match var_name { + Some(v) => v, + None => continue, + }; + + match &mut **b_callee { + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(b_callee_obj), + computed: false, + prop, + .. + }) => { + // + if !b_callee_obj.is_ident_ref_to(var_name.sym.clone()) { + continue; + } + + match &**prop { + Expr::Ident(Ident { sym, .. }) => match &**sym { + "apply" | "call" => {} + _ => continue, + }, + _ => {} + } + + let span = a_assign.span.with_ctxt(SyntaxContext::empty()); + + let obj = a.take(); + + let new = Expr::Call(CallExpr { + span, + callee: MemberExpr { + span: DUMMY_SP, + obj: obj.as_obj(), + prop: prop.take(), + computed: false, + } + .as_callee(), + args: args.take(), + type_args: Default::default(), + }); + b.take(); + self.changed = true; + log::debug!( + "sequences: Reducing `(a = foo, a.call())` to `((a = foo).call())`" + ); + + *a = new; + } + _ => {} + }; + } + + _ => {} + } + } + } +} diff --git a/ecmascript/minifier/src/compress/pure/strings.rs b/ecmascript/minifier/src/compress/pure/strings.rs new file mode 100644 index 00000000000..141fd69e888 --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/strings.rs @@ -0,0 +1,291 @@ +use std::mem::take; + +use super::Pure; +use swc_atoms::{js_word, JsWord}; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{ExprExt, Type, Value}; + +impl Pure<'_> { + /// Converts template literals to string if `exprs` of [Tpl] is empty. + pub(super) fn convert_tpl_to_str(&mut self, e: &mut Expr) { + match e { + Expr::Tpl(t) if t.quasis.len() == 1 && t.exprs.is_empty() => { + if let Some(c) = &t.quasis[0].cooked { + if c.value.chars().all(|c| match c { + '\u{0020}'..='\u{007e}' => true, + _ => false, + }) { + *e = Expr::Lit(Lit::Str(c.clone())); + } + } + } + _ => {} + } + } + + /// This compresses a template literal by inlining string literals in + /// expresions into quasis. + /// + /// Note that this pass only cares about string literals and conversion to a + /// string literal should be done before calling this pass. + pub(super) fn compress_tpl(&mut self, tpl: &mut Tpl) { + debug_assert_eq!(tpl.exprs.len() + 1, tpl.quasis.len()); + let has_str_lit = tpl.exprs.iter().any(|expr| match &**expr { + Expr::Lit(Lit::Str(..)) => true, + _ => false, + }); + if !has_str_lit { + return; + } + + let mut quasis = vec![]; + let mut exprs = vec![]; + let mut cur = String::new(); + let mut cur_raw = String::new(); + + for i in 0..(tpl.exprs.len() + tpl.quasis.len()) { + if i % 2 == 0 { + let i = i / 2; + let q = tpl.quasis[i].take(); + + cur.push_str(&q.cooked.unwrap().value); + cur_raw.push_str(&q.raw.value); + } else { + let i = i / 2; + let e = tpl.exprs[i].take(); + + match *e { + Expr::Lit(Lit::Str(s)) => { + cur.push_str(&s.value); + cur_raw.push_str(&s.value); + } + _ => { + quasis.push(TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Str { + span: DUMMY_SP, + value: take(&mut cur).into(), + has_escape: false, + kind: Default::default(), + }), + raw: Str { + span: DUMMY_SP, + value: take(&mut cur_raw).into(), + has_escape: false, + kind: Default::default(), + }, + }); + + exprs.push(e); + } + } + } + } + + quasis.push(TplElement { + span: DUMMY_SP, + tail: true, + cooked: Some(Str { + span: DUMMY_SP, + value: cur.into(), + has_escape: false, + kind: Default::default(), + }), + raw: Str { + span: DUMMY_SP, + value: cur_raw.into(), + has_escape: false, + kind: Default::default(), + }, + }); + + debug_assert_eq!(exprs.len() + 1, quasis.len()); + + tpl.quasis = quasis; + tpl.exprs = exprs; + } + + /// Called for binary operations with `+`. + pub(super) fn concat_tpl(&mut self, l: &mut Expr, r: &mut Expr) { + match (&mut *l, &mut *r) { + (Expr::Tpl(l), Expr::Lit(Lit::Str(rs))) => { + // Append + if let Some(l_last) = l.quasis.last_mut() { + self.changed = true; + + log::debug!( + "template: Concatted a string (`{}`) on rhs of `+` to a template literal", + rs.value + ); + let l_str = l_last.cooked.as_mut().unwrap(); + + let new: JsWord = format!("{}{}", l_str.value, rs.value).into(); + l_str.value = new.clone(); + l_last.raw.value = new; + + r.take(); + return; + } + } + + (Expr::Lit(Lit::Str(ls)), Expr::Tpl(r)) => { + // Append + if let Some(r_first) = r.quasis.first_mut() { + self.changed = true; + + log::debug!( + "template: Prepended a string (`{}`) on lhs of `+` to a template literal", + ls.value + ); + let r_str = r_first.cooked.as_mut().unwrap(); + + let new: JsWord = format!("{}{}", ls.value, r_str.value).into(); + r_str.value = new.clone(); + r_first.raw.value = new; + + l.take(); + return; + } + } + + (Expr::Tpl(l), Expr::Tpl(rt)) => { + // We prepend the last quasis of l to the first quasis of r. + // After doing so, we can append all data of r to l. + + { + let l_last = l.quasis.pop().unwrap(); + let mut r_first = rt.quasis.first_mut().unwrap(); + + let r_str = r_first.cooked.as_mut().unwrap(); + + let new: JsWord = + format!("{}{}", l_last.cooked.unwrap().value, r_str.value).into(); + r_str.value = new.clone(); + r_first.raw.value = new; + } + + l.quasis.extend(rt.quasis.take()); + l.exprs.extend(rt.exprs.take()); + // Remove r + r.take(); + + debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{:?} is invalid", l); + self.changed = true; + log::debug!("strings: Merged to template literals"); + } + + _ => {} + } + } + + /// + /// - `a + 'foo' + 'bar'` => `a + 'foobar'` + pub(super) fn concat_str(&mut self, e: &mut Expr) { + match e { + Expr::Bin( + bin @ BinExpr { + op: op!(bin, "+"), .. + }, + ) => match &mut *bin.left { + Expr::Bin( + left + @ BinExpr { + op: op!(bin, "+"), .. + }, + ) => { + let type_of_second = left.right.get_type(); + let type_of_third = bin.right.get_type(); + + if let Value::Known(Type::Str) = type_of_second { + if let Value::Known(Type::Str) = type_of_third { + if let Value::Known(second_str) = left.right.as_string() { + if let Value::Known(third_str) = bin.right.as_string() { + let new_str = format!("{}{}", second_str, third_str); + let left_span = left.span; + + self.changed = true; + log::debug!( + "strings: Concatting `{} + {}` to `{}`", + second_str, + third_str, + new_str + ); + + *e = Expr::Bin(BinExpr { + span: bin.span, + op: op!(bin, "+"), + left: left.left.take(), + right: Box::new(Expr::Lit(Lit::Str(Str { + span: left_span, + value: new_str.into(), + has_escape: false, + kind: Default::default(), + }))), + }); + return; + } + } + } + } + } + _ => {} + }, + _ => {} + } + } + + pub(super) fn drop_useless_addition_of_str(&mut self, e: &mut Expr) { + match e { + Expr::Bin(BinExpr { + op: op!(bin, "+"), + left, + right, + .. + }) => { + let lt = left.get_type(); + let rt = right.get_type(); + if let Value::Known(Type::Str) = lt { + if let Value::Known(Type::Str) = rt { + match &**left { + Expr::Lit(Lit::Str(Str { + value: js_word!(""), + .. + })) => { + self.changed = true; + log::debug!( + "string: Dropping empty string literal (in lhs) because it \ + does not changes type" + ); + + *e = *right.take(); + return; + } + _ => {} + } + + match &**right { + Expr::Lit(Lit::Str(Str { + value: js_word!(""), + .. + })) => { + self.changed = true; + log::debug!( + "string: Dropping empty string literal (in rhs) because it \ + does not changes type" + ); + + *e = *left.take(); + return; + } + _ => {} + } + } + } + } + _ => {} + } + } +} diff --git a/ecmascript/minifier/src/compress/optimize/unsafes.rs b/ecmascript/minifier/src/compress/pure/unsafes.rs similarity index 84% rename from ecmascript/minifier/src/compress/optimize/unsafes.rs rename to ecmascript/minifier/src/compress/pure/unsafes.rs index fc38ac0ba01..810954a2270 100644 --- a/ecmascript/minifier/src/compress/optimize/unsafes.rs +++ b/ecmascript/minifier/src/compress/pure/unsafes.rs @@ -1,11 +1,11 @@ -use super::Optimizer; +use super::Pure; use swc_atoms::js_word; use swc_ecma_ast::*; use swc_ecma_utils::ExprExt; -impl Optimizer<'_> { +impl Pure<'_> { /// Drop arguments of `Symbol()` call. - pub(super) fn optimize_symbol_call_unsafely(&mut self, e: &mut CallExpr) { + pub(super) fn drop_arguemtns_of_symbol_call(&mut self, e: &mut CallExpr) { if !self.options.unsafe_symbols { return; } diff --git a/ecmascript/minifier/src/compress/pure/vars.rs b/ecmascript/minifier/src/compress/pure/vars.rs new file mode 100644 index 00000000000..7ec8f43992b --- /dev/null +++ b/ecmascript/minifier/src/compress/pure/vars.rs @@ -0,0 +1,291 @@ +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{prepend, StmtLike}; +use swc_ecma_visit::{ + noop_visit_mut_type, noop_visit_type, Node, Visit, VisitMut, VisitMutWith, VisitWith, +}; + +use super::Pure; + +impl Pure<'_> { + /// Collapse single-use non-constant variables, side effects permitting. + /// + /// This merges all variables to first variable declartion with an + /// initializer. If such variable declaration is not found, variables are + /// prepended to `stmts`. + pub(super) fn collapse_vars_without_init(&mut self, stmts: &mut Vec) + where + T: StmtLike, + Vec: + VisitWith + VisitMutWith + VisitMutWith, + { + if !self.options.collapse_vars { + return; + } + + { + // let mut found_other = false; + // let mut need_work = false; + + // for stmt in &*stmts { + // match stmt.as_stmt() { + // Some(Stmt::Decl(Decl::Var( + // v + // @ + // VarDecl { + // kind: VarDeclKind::Var, + // .. + // }, + // ))) => { + // if v.decls.iter().any(|v| v.init.is_none()) { + // if found_other { + // need_work = true; + // } + // } else { + // found_other = true; + // } + // } + + // _ => { + // found_other = true; + // } + // } + // } + + // Check for nested variable declartions. + let mut v = VarWithOutInitCounter::default(); + stmts.visit_with(&Invalid { span: DUMMY_SP }, &mut v); + if !v.need_work { + return; + } + } + + self.changed = true; + log::debug!("collapse_vars: Collapsing variables without an initializer"); + + let vars = { + let mut v = VarMover { + vars: Default::default(), + var_decl_kind: Default::default(), + }; + stmts.visit_mut_with(&mut v); + + v.vars + }; + + // Prepend vars + + let mut prepender = VarPrepender { vars }; + stmts.visit_mut_with(&mut prepender); + + if !prepender.vars.is_empty() { + prepend( + stmts, + T::from_stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: Default::default(), + decls: prepender.vars, + }))), + ); + } + } +} + +/// See if there's two [VarDecl] which has [VarDeclarator] without the +/// initializer. +#[derive(Default)] +pub(super) struct VarWithOutInitCounter { + need_work: bool, + found_var_without_init: bool, + found_var_with_init: bool, +} + +impl Visit for VarWithOutInitCounter { + noop_visit_type!(); + + fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {} + + fn visit_constructor(&mut self, _: &Constructor, _: &dyn Node) {} + + fn visit_function(&mut self, _: &Function, _: &dyn Node) {} + + fn visit_var_decl(&mut self, v: &VarDecl, _: &dyn Node) { + v.visit_children_with(self); + + if v.kind != VarDeclKind::Var { + return; + } + + let mut found_init = false; + for d in &v.decls { + if d.init.is_some() { + found_init = true; + } else { + if found_init { + self.need_work = true; + return; + } + } + } + + if v.decls.iter().all(|v| v.init.is_none()) { + if self.found_var_without_init || self.found_var_with_init { + self.need_work = true; + } + self.found_var_without_init = true; + } else { + self.found_var_with_init = true; + } + } + + fn visit_var_decl_or_pat(&mut self, _: &VarDeclOrPat, _: &dyn Node) {} +} + +/// Moves all varaible without initializer. +pub(super) struct VarMover { + vars: Vec, + var_decl_kind: Option, +} + +impl VisitMut for VarMover { + noop_visit_mut_type!(); + + /// Noop + fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {} + + /// Noop + fn visit_mut_constructor(&mut self, _: &mut Constructor) {} + + /// Noop + fn visit_mut_function(&mut self, _: &mut Function) {} + + fn visit_mut_module_item(&mut self, s: &mut ModuleItem) { + s.visit_mut_children_with(self); + + match s { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: Decl::Var(d), + .. + })) if d.decls.is_empty() => { + s.take(); + return; + } + + _ => {} + } + } + + fn visit_mut_opt_var_decl_or_expr(&mut self, n: &mut Option) { + n.visit_mut_children_with(self); + + match n { + Some(VarDeclOrExpr::VarDecl(var)) => { + if var.decls.is_empty() { + *n = None; + } + } + _ => {} + } + } + + fn visit_mut_stmt(&mut self, s: &mut Stmt) { + s.visit_mut_children_with(self); + + match s { + Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => { + s.take(); + return; + } + _ => {} + } + } + + fn visit_mut_var_decl(&mut self, v: &mut VarDecl) { + let old = self.var_decl_kind.take(); + self.var_decl_kind = Some(v.kind); + v.visit_mut_children_with(self); + self.var_decl_kind = old; + } + + fn visit_mut_var_decl_or_pat(&mut self, _: &mut VarDeclOrPat) {} + + fn visit_mut_var_declarators(&mut self, d: &mut Vec) { + d.visit_mut_children_with(self); + + if self.var_decl_kind.unwrap() != VarDeclKind::Var { + return; + } + + if d.iter().all(|v| v.init.is_some()) { + return; + } + + let has_init = d.iter().any(|v| v.init.is_some()); + + if has_init { + let mut new = vec![]; + + for v in d.take() { + if v.init.is_some() { + new.push(v) + } else { + self.vars.push(v) + } + } + + *d = new; + } + + let mut new = vec![]; + + if has_init { + new.extend(self.vars.drain(..)); + } + + for v in d.take() { + if v.init.is_some() { + new.push(v) + } else { + self.vars.push(v) + } + } + + *d = new; + } +} + +pub(super) struct VarPrepender { + vars: Vec, +} + +impl VisitMut for VarPrepender { + noop_visit_mut_type!(); + + fn visit_mut_var_decl(&mut self, v: &mut VarDecl) { + if self.vars.is_empty() { + return; + } + + if v.kind != VarDeclKind::Var { + return; + } + + if v.decls.iter().any(|d| d.init.is_some()) { + let mut decls = self.vars.take(); + decls.extend(v.decls.take()); + + v.decls = decls; + } + } + + /// Noop + fn visit_mut_function(&mut self, _: &mut Function) {} + + /// Noop + fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {} + + /// Noop + fn visit_mut_constructor(&mut self, _: &mut Constructor) {} +} diff --git a/ecmascript/minifier/src/compress/util.rs b/ecmascript/minifier/src/compress/util.rs new file mode 100644 index 00000000000..5fc72cd5827 --- /dev/null +++ b/ecmascript/minifier/src/compress/util.rs @@ -0,0 +1,588 @@ +use crate::debug::dump; +use std::f64; +use swc_atoms::js_word; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_transforms_base::ext::MapWithMut; +use swc_ecma_utils::{ExprExt, Value}; +use unicode_xid::UnicodeXID; + +/// Creates `!e` where e is the expression passed as an argument. +/// +/// # Note +/// +/// This method returns `!e` if `!!e` is given as a argument. +/// +/// TODO: Handle special cases like !1 or !0 +pub(super) fn negate(e: &mut Expr, in_bool_ctx: bool) { + let start_str = dump(&*e); + + match e { + Expr::Bin(bin @ BinExpr { op: op!("=="), .. }) + | Expr::Bin(bin @ BinExpr { op: op!("!="), .. }) + | Expr::Bin(bin @ BinExpr { op: op!("==="), .. }) + | Expr::Bin(bin @ BinExpr { op: op!("!=="), .. }) => { + bin.op = match bin.op { + op!("==") => { + op!("!=") + } + op!("!=") => { + op!("==") + } + op!("===") => { + op!("!==") + } + op!("!==") => { + op!("===") + } + _ => { + unreachable!() + } + }; + log::debug!("negate: binary"); + return; + } + + Expr::Bin(BinExpr { + left, + right, + op: op @ op!("&&"), + .. + }) if is_ok_to_negate_rhs(&right) => { + log::debug!("negate: a && b => !a || !b"); + + negate(&mut **left, in_bool_ctx); + negate(&mut **right, in_bool_ctx); + *op = op!("||"); + return; + } + + Expr::Bin(BinExpr { + left, + right, + op: op @ op!("||"), + .. + }) if is_ok_to_negate_rhs(&right) => { + log::debug!("negate: a || b => !a && !b"); + + negate(&mut **left, in_bool_ctx); + negate(&mut **right, in_bool_ctx); + *op = op!("&&"); + return; + } + + Expr::Cond(CondExpr { cons, alt, .. }) + if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) => + { + log::debug!("negate: cond"); + + negate(&mut **cons, in_bool_ctx); + negate(&mut **alt, in_bool_ctx); + return; + } + + Expr::Seq(SeqExpr { exprs, .. }) => { + if let Some(last) = exprs.last_mut() { + log::debug!("negate: seq"); + + negate(&mut **last, in_bool_ctx); + return; + } + } + + _ => {} + } + + let mut arg = Box::new(e.take()); + + match &mut *arg { + Expr::Unary(UnaryExpr { + op: op!("!"), arg, .. + }) => match &mut **arg { + Expr::Unary(UnaryExpr { op: op!("!"), .. }) => { + log::debug!("negate: !!bool => !bool"); + *e = *arg.take(); + return; + } + Expr::Bin(BinExpr { op: op!("in"), .. }) + | Expr::Bin(BinExpr { + op: op!("instanceof"), + .. + }) => { + log::debug!("negate: !bool => bool"); + *e = *arg.take(); + return; + } + _ => { + if in_bool_ctx { + log::debug!("negate: !expr => expr (in bool context)"); + *e = *arg.take(); + return; + } + } + }, + + _ => {} + } + + log::debug!("negate: e => !e"); + + *e = Expr::Unary(UnaryExpr { + span: DUMMY_SP, + op: op!("!"), + arg, + }); + + if cfg!(feature = "debug") { + log::trace!("[Change] Negated `{}` as `{}`", start_str, dump(&*e)); + } +} + +pub(crate) fn is_ok_to_negate_for_cond(e: &Expr) -> bool { + match e { + Expr::Update(..) => false, + _ => true, + } +} + +pub(crate) fn is_ok_to_negate_rhs(rhs: &Expr) -> bool { + match rhs { + Expr::Member(..) => true, + Expr::Bin(BinExpr { + op: op!("===") | op!("!==") | op!("==") | op!("!="), + .. + }) => true, + + Expr::Call(..) | Expr::New(..) => false, + + Expr::Update(..) => false, + + Expr::Bin(BinExpr { + op: op!("&&") | op!("||"), + left, + right, + .. + }) => is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right), + + Expr::Bin(BinExpr { left, right, .. }) => { + is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right) + } + + Expr::Assign(e) => is_ok_to_negate_rhs(&e.right), + + Expr::Unary(UnaryExpr { + op: op!("!") | op!("delete"), + .. + }) => true, + + Expr::Seq(e) => { + if let Some(last) = e.exprs.last() { + is_ok_to_negate_rhs(&last) + } else { + true + } + } + + Expr::Cond(e) => is_ok_to_negate_rhs(&e.cons) && is_ok_to_negate_rhs(&e.alt), + + _ => { + if !rhs.may_have_side_effects() { + return true; + } + + if cfg!(feature = "debug") { + log::warn!("unimplemented: is_ok_to_negate_rhs: `{}`", dump(&*rhs)); + } + + false + } + } +} + +/// A negative value means that it's efficient to negate the expression. +pub(crate) fn negate_cost(e: &Expr, in_bool_ctx: bool, is_ret_val_ignored: bool) -> Option { + fn cost( + e: &Expr, + in_bool_ctx: bool, + bin_op: Option, + is_ret_val_ignored: bool, + ) -> isize { + match e { + Expr::Unary(UnaryExpr { + op: op!("!"), arg, .. + }) => { + // TODO: Check if this argument is actually start of line. + match &**arg { + Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(callee), + .. + }) => match &**callee { + Expr::Fn(..) => return 0, + _ => {} + }, + _ => {} + } + + if in_bool_ctx { + let c = -cost(arg, true, None, is_ret_val_ignored); + return c.min(-1); + } + + match &**arg { + Expr::Unary(UnaryExpr { op: op!("!"), .. }) => -1, + + _ => 1, + } + } + Expr::Bin(BinExpr { + op: op!("===") | op!("!==") | op!("==") | op!("!="), + .. + }) => 0, + + Expr::Bin(BinExpr { + op: op @ op!("||") | op @ op!("&&"), + left, + right, + .. + }) => { + let l_cost = cost(&left, in_bool_ctx, Some(*op), false); + + if !is_ret_val_ignored && !is_ok_to_negate_rhs(&right) { + return l_cost + 3; + } + l_cost + cost(&right, in_bool_ctx, Some(*op), is_ret_val_ignored) + } + + Expr::Cond(CondExpr { cons, alt, .. }) + if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) => + { + cost(&cons, in_bool_ctx, bin_op, is_ret_val_ignored) + + cost(&alt, in_bool_ctx, bin_op, is_ret_val_ignored) + } + + Expr::Cond(..) + | Expr::Update(..) + | Expr::Bin(BinExpr { + op: op!("in") | op!("instanceof"), + .. + }) => 3, + + Expr::Assign(..) => { + if is_ret_val_ignored { + 0 + } else { + 3 + } + } + + Expr::Seq(e) => { + if let Some(last) = e.exprs.last() { + return cost(&last, in_bool_ctx, bin_op, is_ret_val_ignored); + } + + if is_ret_val_ignored { + 0 + } else { + 1 + } + } + + _ => { + if is_ret_val_ignored { + 0 + } else { + 1 + } + } + } + } + + let cost = cost(e, in_bool_ctx, None, is_ret_val_ignored); + + if cfg!(feature = "debug") { + log::trace!("negation cost of `{}` = {}", dump(&*e), cost); + } + + Some(cost) +} + +pub(crate) fn is_pure_undefined(e: &Expr) -> bool { + match e { + Expr::Ident(Ident { + sym: js_word!("undefined"), + .. + }) => true, + + Expr::Unary(UnaryExpr { + op: UnaryOp::Void, + arg, + .. + }) if !arg.may_have_side_effects() => true, + + _ => false, + } +} + +pub(crate) fn is_valid_identifier(s: &str, ascii_only: bool) -> bool { + if ascii_only { + if s.chars().any(|c| !c.is_ascii()) { + return false; + } + } + + s.starts_with(|c: char| c.is_xid_start()) + && s.chars().all(|c: char| c.is_xid_continue()) + && !s.contains("𝒶") + && !s.is_reserved() +} + +pub(crate) fn get_lhs_ident(e: &PatOrExpr) -> Option<&Ident> { + match e { + PatOrExpr::Expr(v) => match &**v { + Expr::Ident(i) => Some(i), + _ => None, + }, + PatOrExpr::Pat(v) => match &**v { + Pat::Ident(i) => Some(&i.id), + Pat::Expr(v) => match &**v { + Expr::Ident(i) => Some(i), + _ => None, + }, + _ => None, + }, + } +} + +pub(crate) fn get_lhs_ident_mut(e: &mut PatOrExpr) -> Option<&mut Ident> { + match e { + PatOrExpr::Expr(v) => match &mut **v { + Expr::Ident(i) => Some(i), + _ => None, + }, + PatOrExpr::Pat(v) => match &mut **v { + Pat::Ident(i) => Some(&mut i.id), + Pat::Expr(v) => match &mut **v { + Expr::Ident(i) => Some(i), + _ => None, + }, + _ => None, + }, + } +} + +pub(crate) fn is_directive(e: &Stmt) -> bool { + match e { + Stmt::Expr(s) => match &*s.expr { + Expr::Lit(Lit::Str(Str { value, .. })) => value.starts_with("use "), + _ => false, + }, + _ => false, + } +} + +pub(crate) fn is_pure_undefined_or_null(e: &Expr) -> bool { + is_pure_undefined(e) + || match e { + Expr::Lit(Lit::Null(..)) => true, + _ => false, + } +} + +/// This method does **not** modifies `e`. +/// +/// This method is used to test if a whole call can be replaced, while +/// preserving standalone constants. +pub(crate) fn eval_as_number(e: &Expr) -> Option { + match e { + Expr::Bin(BinExpr { + op: op!(bin, "-"), + left, + right, + .. + }) => { + let l = eval_as_number(&left)?; + let r = eval_as_number(&right)?; + + return Some(l - r); + } + + Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(callee), + args, + .. + }) => { + for arg in &*args { + if arg.spread.is_some() || arg.expr.may_have_side_effects() { + return None; + } + } + + match &**callee { + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + .. + }) => { + let prop = match &**prop { + Expr::Ident(i) => i, + _ => return None, + }; + + match &**obj { + Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym { + "cos" => { + let v = eval_as_number(&args.first()?.expr)?; + + return Some(v.cos()); + } + "sin" => { + let v = eval_as_number(&args.first()?.expr)?; + + return Some(v.sin()); + } + + "max" => { + let mut numbers = vec![]; + for arg in args { + let v = eval_as_number(&arg.expr)?; + if v.is_infinite() || v.is_nan() { + return None; + } + numbers.push(v); + } + + return Some( + numbers + .into_iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap_or(f64::NEG_INFINITY), + ); + } + + "min" => { + let mut numbers = vec![]; + for arg in args { + let v = eval_as_number(&arg.expr)?; + if v.is_infinite() || v.is_nan() { + return None; + } + numbers.push(v); + } + + return Some( + numbers + .into_iter() + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap_or(f64::INFINITY), + ); + } + + "pow" => { + if args.len() != 2 { + return None; + } + let first = eval_as_number(&args[0].expr)?; + let second = eval_as_number(&args[1].expr)?; + + return Some(first.powf(second)); + } + + _ => {} + }, + _ => {} + } + } + _ => {} + } + } + + Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(obj), + prop, + computed: false, + .. + }) => { + let prop = match &**prop { + Expr::Ident(i) => i, + _ => return None, + }; + + match &**obj { + Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym { + "PI" => return Some(f64::consts::PI), + "E" => return Some(f64::consts::E), + "LN10" => return Some(f64::consts::LN_10), + _ => {} + }, + _ => {} + } + } + + _ => { + if let Value::Known(v) = e.as_number() { + return Some(v); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::negate_cost; + use swc_common::{input::SourceFileInput, FileName}; + use swc_ecma_parser::{lexer::Lexer, Parser}; + + fn assert_negate_cost(s: &str, in_bool_ctx: bool, is_ret_val_ignored: bool, expected: isize) { + testing::run_test2(false, |cm, _| { + let fm = cm.new_source_file(FileName::Anon, s.to_string()); + + let lexer = Lexer::new( + Default::default(), + swc_ecma_ast::EsVersion::latest(), + SourceFileInput::from(&*fm), + None, + ); + + let mut parser = Parser::new_from(lexer); + + let e = parser + .parse_expr() + .expect("failed to parse input as an expression"); + + let actual = negate_cost(&e, in_bool_ctx, is_ret_val_ignored).unwrap(); + + assert_eq!( + actual, expected, + "Expected negation cost of {} to be {}, but got {}", + s, expected, actual, + ); + + Ok(()) + }) + .unwrap(); + } + + #[test] + fn logical_1() { + assert_negate_cost( + "this[key] && !this.hasOwnProperty(key) || (this[key] = value)", + false, + true, + 2, + ); + } + + #[test] + #[ignore] + fn logical_2() { + assert_negate_cost( + "(!this[key] || this.hasOwnProperty(key)) && (this[key] = value)", + false, + true, + -2, + ); + } +} diff --git a/ecmascript/minifier/src/hygiene.rs b/ecmascript/minifier/src/hygiene.rs deleted file mode 100644 index 9e75eeff93b..00000000000 --- a/ecmascript/minifier/src/hygiene.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::marks::Marks; -use swc_common::comments::Comment; -use swc_common::comments::CommentKind; -use swc_common::comments::Comments; -use swc_common::Mark; -use swc_common::Span; -use swc_common::SyntaxContext; -use swc_ecma_ast::*; -use swc_ecma_visit::noop_visit_mut_type; -use swc_ecma_visit::VisitMut; -use swc_ecma_visit::VisitMutWith; - -/// This pass analyzes the comment -/// -/// - Makes all nodes except identifiers unique in aspect of span hygiene. -/// - Convert annottatinos into [Mark]. -pub(crate) fn info_marker<'a>( - comments: Option<&'a dyn Comments>, - marks: Marks, -) -> impl 'a + VisitMut { - InfoMarker { comments, marks } -} - -struct InfoMarker<'a> { - comments: Option<&'a dyn Comments>, - marks: Marks, -} - -impl InfoMarker<'_> { - fn make_unique(&self, span: &mut Span) { - debug_assert_eq!( - span.ctxt, - SyntaxContext::empty(), - "Expected empty syntax context" - ); - - span.ctxt = span.ctxt.apply_mark(Mark::fresh(Mark::root())); - } - - /// Check for `/** @const */`. - pub(super) fn has_const_ann(&self, span: Span) -> bool { - self.find_comment(span, |c| { - if c.kind == CommentKind::Block { - if !c.text.starts_with('*') { - return false; - } - let t = c.text[1..].trim(); - // - if t.starts_with("@const") { - return true; - } - } - - false - }) - } - - /// Check for `/*#__NOINLINE__*/` - pub(super) fn has_noinline(&self, span: Span) -> bool { - self.has_flag(span, "NOINLINE") - } - - fn find_comment(&self, span: Span, mut op: F) -> bool - where - F: FnMut(&Comment) -> bool, - { - let mut found = false; - if let Some(comments) = self.comments { - let cs = comments.get_leading(span.lo); - if let Some(cs) = cs { - for c in &cs { - found |= op(&c); - if found { - break; - } - } - } - } - - found - } - - fn has_flag(&self, span: Span, text: &'static str) -> bool { - self.find_comment(span, |c| { - if c.kind == CommentKind::Block { - // - if c.text.len() == (text.len() + 5) - && c.text.starts_with("#__") - && c.text.ends_with("__") - && text == &c.text[3..c.text.len() - 2] - { - return true; - } - } - - false - }) - } -} - -impl VisitMut for InfoMarker<'_> { - noop_visit_mut_type!(); - - fn visit_mut_block_stmt(&mut self, n: &mut BlockStmt) { - n.visit_mut_children_with(self); - - self.make_unique(&mut n.span); - } - - fn visit_mut_call_expr(&mut self, n: &mut CallExpr) { - n.visit_mut_children_with(self); - - if self.has_noinline(n.span) { - n.span = n.span.apply_mark(self.marks.noinline); - } - } - - fn visit_mut_function(&mut self, n: &mut Function) { - n.visit_mut_children_with(self); - - self.make_unique(&mut n.span); - } - - fn visit_mut_ident(&mut self, _: &mut Ident) {} - - fn visit_mut_lit(&mut self, _: &mut Lit) {} - - fn visit_mut_var_decl(&mut self, n: &mut VarDecl) { - n.visit_mut_children_with(self); - - if self.has_const_ann(n.span) { - n.span = n.span.apply_mark(self.marks.const_ann); - } - } -} diff --git a/ecmascript/minifier/src/lib.rs b/ecmascript/minifier/src/lib.rs index 1be08ef0b44..82588f3ae6b 100644 --- a/ecmascript/minifier/src/lib.rs +++ b/ecmascript/minifier/src/lib.rs @@ -13,8 +13,8 @@ //! `visit_mut_module_items`. use crate::compress::compressor; -use crate::hygiene::info_marker; use crate::marks::Marks; +use crate::metadata::info_marker; use crate::option::ExtraOptions; use crate::option::MinifyOptions; use crate::pass::compute_char_freq::compute_char_freq; @@ -32,6 +32,7 @@ use std::time::Instant; use swc_common::comments::Comments; use swc_common::sync::Lrc; use swc_common::SourceMap; +use swc_common::GLOBALS; use swc_ecma_ast::Module; use swc_ecma_visit::FoldWith; use swc_ecma_visit::VisitMutWith; @@ -40,19 +41,20 @@ use timing::Timings; mod analyzer; mod compress; mod debug; -mod hygiene; -mod marks; +pub mod marks; +mod metadata; pub mod option; mod pass; pub mod timing; mod util; const DISABLE_BUGGY_PASSES: bool = true; +const MAX_PAR_DEPTH: u8 = 3; #[inline] pub fn optimize( mut m: Module, - cm: Lrc, + _cm: Lrc, comments: Option<&dyn Comments>, mut timings: Option<&mut Timings>, options: &MinifyOptions, @@ -85,7 +87,7 @@ pub fn optimize( } } - m.visit_mut_with(&mut info_marker(comments, marks)); + m.visit_mut_with(&mut info_marker(comments, marks, extra.top_level_mark)); if options.wrap { // TODO: wrap_common_js @@ -116,7 +118,7 @@ pub fn optimize( } if let Some(options) = &options.compress { let start = now(); - m = m.fold_with(&mut compressor(cm.clone(), marks, &options)); + m = GLOBALS.with(|globals| m.fold_with(&mut compressor(globals, marks, &options))); if let Some(start) = start { log::info!("compressor took {:?}", Instant::now() - start); } @@ -148,7 +150,7 @@ pub fn optimize( } if let Some(property_mangle_options) = options.mangle.as_ref().and_then(|o| o.props.as_ref()) { - mangle_properties(&mut m, property_mangle_options.clone(), marks); + mangle_properties(&mut m, property_mangle_options.clone()); } if let Some(ref mut t) = timings { @@ -156,7 +158,7 @@ pub fn optimize( } { - let data = analyze(&m, marks); + let data = analyze(&m, None); m.visit_mut_with(&mut hygiene_optimizer(data, extra.top_level_mark)); } diff --git a/ecmascript/minifier/src/marks.rs b/ecmascript/minifier/src/marks.rs index 5fa01230163..5a5484820ec 100644 --- a/ecmascript/minifier/src/marks.rs +++ b/ecmascript/minifier/src/marks.rs @@ -1,22 +1,35 @@ use swc_common::Mark; #[derive(Debug, Clone, Copy)] -pub(crate) struct Marks { +pub struct Marks { /// [Mark] applied to non-top level varaibles which is injected while /// inlining. + /// + /// In other words, AST nodes marked with this mark will not be treated as a + /// top level item, even if it's in the top level scope. pub(crate) non_top_level: Mark, + /// Treat this function as a top level module. + /// + /// If this mark is applied, the function will be treated as a black box. It + /// will not be analyzed by usage analyzer. + /// + /// # Note + /// + /// Standalone functions should not depend on any other declarations in the + /// outer scope. + /// + /// This is only applied to [swc_ecma_ast::Function] and it should not be + /// nested. + pub(crate) standalone: Mark, + + //// Appied to [swc_ecma_ast::Module]. + pub(crate) bundle_of_standalones: Mark, + /// Optimization is finished. This mark is only applied if both of the /// stateful optimizer and the pure optimizer cannot optimize anymore. pub(crate) done: Mark, - /// Temporary mark, used to mark nodes which cannot be optimized by the pure - /// optimizer. - /// - /// The stateful optimizer removes this mark if it modified the node, so - /// that the pure optimizer can try again to optimize the node. - pub(crate) pure_done: Mark, - /// `/** @const */`. pub(crate) const_ann: Mark, @@ -28,15 +41,16 @@ pub(crate) struct Marks { } impl Marks { - pub fn new() -> Marks { + pub fn new() -> Self { fn m() -> Mark { Mark::fresh(Mark::root()) } Marks { non_top_level: m(), + standalone: m(), + bundle_of_standalones: m(), done: m(), - pure_done: m(), const_ann: m(), noinline: m(), pure: m(), diff --git a/ecmascript/minifier/src/metadata/mod.rs b/ecmascript/minifier/src/metadata/mod.rs new file mode 100644 index 00000000000..541b917e028 --- /dev/null +++ b/ecmascript/minifier/src/metadata/mod.rs @@ -0,0 +1,452 @@ +use crate::marks::Marks; +use swc_common::comments::Comment; +use swc_common::comments::CommentKind; +use swc_common::comments::Comments; +use swc_common::Mark; +use swc_common::Span; +use swc_common::SyntaxContext; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_utils::find_ids; +use swc_ecma_utils::ident::IdentLike; +use swc_ecma_utils::Id; +use swc_ecma_visit::noop_visit_mut_type; +use swc_ecma_visit::noop_visit_type; +use swc_ecma_visit::Node; +use swc_ecma_visit::Visit; +use swc_ecma_visit::VisitMut; +use swc_ecma_visit::VisitMutWith; +use swc_ecma_visit::VisitWith; + +#[cfg(test)] +mod tests; + +/// This pass analyzes the comment +/// +/// - Makes all nodes except identifiers unique in aspect of span hygiene. +/// - Convert annottatinos into [Mark]. +pub(crate) fn info_marker<'a>( + comments: Option<&'a dyn Comments>, + marks: Marks, + top_level_mark: Mark, +) -> impl 'a + VisitMut { + InfoMarker { + comments, + marks, + top_level_mark, + state: Default::default(), + top_level_bindings: Default::default(), + } +} + +#[derive(Default)] +struct State { + is_bundle: bool, + is_in_export: bool, +} + +struct InfoMarker<'a> { + comments: Option<&'a dyn Comments>, + marks: Marks, + top_level_mark: Mark, + state: State, + top_level_bindings: Vec, +} + +impl InfoMarker<'_> { + fn make_unique(&self, span: &mut Span) { + debug_assert_eq!( + span.ctxt, + SyntaxContext::empty(), + "Expected empty syntax context" + ); + + span.ctxt = span.ctxt.apply_mark(Mark::fresh(Mark::root())); + } + + /// Check for `/** @const */`. + pub(super) fn has_const_ann(&self, span: Span) -> bool { + self.find_comment(span, |c| { + if c.kind == CommentKind::Block { + if !c.text.starts_with('*') { + return false; + } + let t = c.text[1..].trim(); + // + if t.starts_with("@const") { + return true; + } + } + + false + }) + } + + /// Check for `/*#__NOINLINE__*/` + pub(super) fn has_noinline(&self, span: Span) -> bool { + self.has_flag(span, "NOINLINE") + } + + fn find_comment(&self, span: Span, mut op: F) -> bool + where + F: FnMut(&Comment) -> bool, + { + let mut found = false; + if let Some(comments) = self.comments { + let cs = comments.get_leading(span.lo); + if let Some(cs) = cs { + for c in &cs { + found |= op(&c); + if found { + break; + } + } + } + } + + found + } + + fn has_flag(&self, span: Span, text: &'static str) -> bool { + self.find_comment(span, |c| { + if c.kind == CommentKind::Block { + // + if c.text.len() == (text.len() + 5) + && c.text.starts_with("#__") + && c.text.ends_with("__") + && text == &c.text[3..c.text.len() - 2] + { + return true; + } + } + + false + }) + } +} + +impl VisitMut for InfoMarker<'_> { + noop_visit_mut_type!(); + + fn visit_mut_block_stmt(&mut self, n: &mut BlockStmt) { + n.visit_mut_children_with(self); + + self.make_unique(&mut n.span); + } + + fn visit_mut_call_expr(&mut self, n: &mut CallExpr) { + n.visit_mut_children_with(self); + + if self.has_noinline(n.span) { + n.span = n.span.apply_mark(self.marks.noinline); + } + } + + fn visit_mut_export_default_decl(&mut self, e: &mut ExportDefaultDecl) { + self.state.is_in_export = true; + e.visit_mut_children_with(self); + self.state.is_in_export = false; + } + + fn visit_mut_export_default_expr(&mut self, e: &mut ExportDefaultExpr) { + self.state.is_in_export = true; + e.visit_mut_children_with(self); + self.state.is_in_export = false; + } + + fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) { + n.visit_mut_children_with(self); + + if !self.state.is_in_export + && n.function + .params + .iter() + .any(|p| is_param_one_of(p, &["module"])) + && n.function.params.iter().any(|p| { + is_param_one_of( + p, + &["exports", "__webpack_require__", "__webpack_exports__"], + ) + }) + { + if is_standalone( + &mut n.function, + self.top_level_mark, + &self.top_level_bindings, + ) { + self.state.is_bundle = true; + + n.function.span = n.function.span.apply_mark(self.marks.standalone); + return; + } + } + } + + fn visit_mut_function(&mut self, n: &mut Function) { + n.visit_mut_children_with(self); + + self.make_unique(&mut n.span); + } + + fn visit_mut_ident(&mut self, _: &mut Ident) {} + + fn visit_mut_lit(&mut self, _: &mut Lit) {} + + fn visit_mut_module(&mut self, m: &mut Module) { + self.top_level_bindings = { + let mut v = TopLevelBindingCollector { + top_level_ctxt: SyntaxContext::empty().apply_mark(self.top_level_mark), + bindings: Default::default(), + }; + m.visit_with(&Invalid { span: DUMMY_SP }, &mut v); + v.bindings + }; + + m.visit_mut_children_with(self); + + if self.state.is_bundle { + log::info!("Running minifier in the bundle mode"); + m.span = m.span.apply_mark(self.marks.bundle_of_standalones); + } else { + log::info!("Running minifier in the normal mode"); + } + } + + fn visit_mut_var_decl(&mut self, n: &mut VarDecl) { + n.visit_mut_children_with(self); + + if self.has_const_ann(n.span) { + n.span = n.span.apply_mark(self.marks.const_ann); + } + } +} + +fn is_param_one_of(p: &Param, allowed: &[&str]) -> bool { + match &p.pat { + Pat::Ident(i) => allowed.contains(&&*i.id.sym), + _ => false, + } +} + +struct TopLevelBindingCollector { + top_level_ctxt: SyntaxContext, + bindings: Vec, +} + +impl TopLevelBindingCollector { + fn add(&mut self, id: Id) { + if id.1 != self.top_level_ctxt { + return; + } + + self.bindings.push(id); + } +} + +impl Visit for TopLevelBindingCollector { + noop_visit_type!(); + + fn visit_class_decl(&mut self, v: &ClassDecl, _: &dyn Node) { + self.add(v.ident.to_id()); + } + + fn visit_fn_decl(&mut self, v: &FnDecl, _: &dyn Node) { + self.add(v.ident.to_id()); + } + + fn visit_function(&mut self, _: &Function, _: &dyn Node) {} + + fn visit_var_decl(&mut self, v: &VarDecl, _: &dyn Node) { + v.visit_children_with(self); + let ids: Vec = find_ids(&v.decls); + + for id in ids { + self.add(id) + } + } +} + +fn is_standalone(n: &mut N, top_level_mark: Mark, top_level_bindings: &[Id]) -> bool +where + N: VisitMutWith, +{ + let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); + + let bindings = { + let mut v = IdentCollector { + ids: Default::default(), + for_binding: true, + is_pat_decl: false, + }; + n.visit_mut_with(&mut v); + v.ids + }; + + let used = { + let mut v = IdentCollector { + ids: Default::default(), + for_binding: false, + is_pat_decl: false, + }; + n.visit_mut_with(&mut v); + v.ids + }; + + for used_id in &used { + if used_id.0.starts_with("__WEBPACK_EXTERNAL_MODULE_") { + continue; + } + + match &*used_id.0 { + "__webpack_require__" | "exports" => continue, + _ => {} + } + + if used_id.1 == top_level_ctxt { + if top_level_bindings.contains(&used_id) { + if cfg!(feature = "debug") { + log::debug!( + "Due to {}{:?} (top-level), it's not a bundle", + used_id.0, + used_id.1 + ); + } + + return false; + } + + continue; + } + + if bindings.contains(used_id) { + continue; + } + + if cfg!(feature = "debug") { + log::debug!("Due to {}{:?}, it's not a bundle", used_id.0, used_id.1); + } + return false; + } + + true +} + +struct IdentCollector { + ids: Vec, + for_binding: bool, + + is_pat_decl: bool, +} + +impl IdentCollector { + fn add(&mut self, i: &Ident) { + let id = i.to_id(); + self.ids.push(id); + } +} + +impl VisitMut for IdentCollector { + noop_visit_mut_type!(); + + fn visit_mut_catch_clause(&mut self, c: &mut CatchClause) { + let old = self.is_pat_decl; + self.is_pat_decl = true; + c.param.visit_mut_children_with(self); + self.is_pat_decl = old; + + self.is_pat_decl = false; + c.body.visit_mut_with(self); + + self.is_pat_decl = old; + } + + fn visit_mut_class_decl(&mut self, e: &mut ClassDecl) { + if self.for_binding { + e.ident.visit_mut_with(self); + } + + e.class.visit_mut_with(self); + } + + fn visit_mut_class_expr(&mut self, e: &mut ClassExpr) { + e.class.visit_mut_with(self); + } + + fn visit_mut_expr(&mut self, e: &mut Expr) { + match e { + Expr::Ident(..) if self.for_binding => {} + _ => { + e.visit_mut_children_with(self); + } + } + } + + fn visit_mut_fn_decl(&mut self, e: &mut FnDecl) { + if self.for_binding { + e.ident.visit_mut_with(self); + } + + e.function.visit_mut_with(self); + } + + fn visit_mut_fn_expr(&mut self, e: &mut FnExpr) { + if self.for_binding { + e.ident.visit_mut_with(self); + } + + e.function.visit_mut_with(self); + } + + fn visit_mut_ident(&mut self, i: &mut Ident) { + self.add(&*i); + } + + fn visit_mut_labeled_stmt(&mut self, s: &mut LabeledStmt) { + s.body.visit_mut_with(self); + } + + fn visit_mut_member_expr(&mut self, e: &mut MemberExpr) { + e.obj.visit_mut_with(self); + if e.computed { + e.prop.visit_mut_with(self); + } + } + + fn visit_mut_param(&mut self, p: &mut Param) { + let old = self.is_pat_decl; + self.is_pat_decl = true; + p.visit_mut_children_with(self); + self.is_pat_decl = old; + } + + fn visit_mut_pat(&mut self, p: &mut Pat) { + match p { + Pat::Ident(..) if self.for_binding && !self.is_pat_decl => {} + + _ => { + p.visit_mut_children_with(self); + } + } + } + + fn visit_mut_prop_name(&mut self, p: &mut PropName) { + match p { + PropName::Computed(..) => { + p.visit_mut_children_with(self); + } + _ => {} + } + } + + fn visit_mut_var_declarator(&mut self, d: &mut VarDeclarator) { + let old = self.is_pat_decl; + + self.is_pat_decl = true; + d.name.visit_mut_with(self); + + self.is_pat_decl = false; + d.init.visit_mut_with(self); + + self.is_pat_decl = old; + } +} diff --git a/ecmascript/minifier/src/metadata/tests.rs b/ecmascript/minifier/src/metadata/tests.rs new file mode 100644 index 00000000000..69f482ac80f --- /dev/null +++ b/ecmascript/minifier/src/metadata/tests.rs @@ -0,0 +1,160 @@ +use swc_common::{input::SourceFileInput, FileName, Mark, Span, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_parser::{lexer::Lexer, Parser}; +use swc_ecma_transforms::resolver_with_mark; +use swc_ecma_visit::{Node, Visit, VisitMutWith, VisitWith}; + +use crate::marks::Marks; + +use super::info_marker; + +fn assert_standalone(src: &str, expected: usize) { + testing::run_test(false, |cm, _handler| { + let marks = Marks::new(); + let top_level_mark = Mark::fresh(Mark::root()); + let fm = cm.new_source_file(FileName::Anon, src.to_string()); + + let lexer = Lexer::new( + Default::default(), + EsVersion::latest(), + SourceFileInput::from(&*fm), + None, + ); + + let mut parser = Parser::new_from(lexer); + + let mut m = parser.parse_module().expect("failed to parse"); + m.visit_mut_with(&mut resolver_with_mark(top_level_mark)); + + m.visit_mut_with(&mut info_marker(None, marks, top_level_mark)); + + eprintln!("Expected: {} modules in bundle", expected); + let actual = { + let mut counter = MarkCounter { + mark: marks.standalone, + count: 0, + }; + m.visit_with(&Invalid { span: DUMMY_SP }, &mut counter); + counter.count + }; + eprintln!("Actual: {} modules in bundle", actual); + + assert_eq!(expected, actual); + + if expected != 0 { + assert!( + m.span.has_mark(marks.bundle_of_standalones), + "Expected module to be marked as a bundle" + ); + } else { + assert!( + !m.span.has_mark(marks.bundle_of_standalones), + "Expected module to be not marked as a bundle" + ); + } + + Ok(()) + }) + .unwrap(); +} + +struct MarkCounter { + mark: Mark, + count: usize, +} + +impl Visit for MarkCounter { + fn visit_span(&mut self, span: &Span, _: &dyn Node) { + if span.has_mark(self.mark) { + self.count += 1; + } + } +} + +#[test] +fn standalone_base() { + assert_standalone("function foo() {}", 0); +} + +#[test] +fn standalone_no_usage() { + assert_standalone( + "function foo() { + declare(function (module, exports) { + + }, function (module, exports) { + + }); + }", + 2, + ); +} + +#[test] +fn usage_of_var_1() { + assert_standalone( + "function foo() { + var bar = 2; + declare(function (module, exports) { + bar = 1; + }, function (module, exports) { + + }); + }", + 1, + ); +} + +#[test] +fn usage_of_class_1() { + assert_standalone( + "function foo() { + class Foo { + + } + declare(function (module, exports) { + const bar = new Foo(); + }, function (module, exports) { + + }); + }", + 1, + ); +} + +#[test] +fn usage_of_fn_1() { + assert_standalone( + "function foo() { + function bar() { + + } + declare(function (module, exports) { + const baz = new bar(); + }, function (module, exports) { + + }); + }", + 1, + ); +} + +#[test] +fn usage_of_var_2() { + assert_standalone( + "var C = 1; + var obj = { + bar: function (module, exports) { + return C + C; + }, + }; + console.log(obj.bar()); + ", + 0, + ); +} + +#[test] +fn export_default_fn_1() { + assert_standalone("export default function f(module, exports) {}", 0); +} diff --git a/ecmascript/minifier/src/pass/hygiene/mod.rs b/ecmascript/minifier/src/pass/hygiene/mod.rs index a96a12983ab..41c0bfc6ef2 100644 --- a/ecmascript/minifier/src/pass/hygiene/mod.rs +++ b/ecmascript/minifier/src/pass/hygiene/mod.rs @@ -1,6 +1,5 @@ use crate::analyzer::analyze; use crate::analyzer::ProgramData; -use crate::marks::Marks; use crate::pass::hygiene::analyzer::HygieneAnalyzer; use crate::pass::hygiene::analyzer::HygieneData; use crate::util::now; @@ -21,7 +20,7 @@ mod analyzer; /// /// Requires [swc_common::GLOBALS]. pub fn optimize_hygiene(m: &mut Module, top_level_mark: Mark) { - let data = analyze(&*m, Marks::new()); + let data = analyze(&*m, None); m.visit_mut_with(&mut hygiene_optimizer(data, top_level_mark)) } diff --git a/ecmascript/minifier/src/pass/mangle_names/mod.rs b/ecmascript/minifier/src/pass/mangle_names/mod.rs index 3fcdafe3c4d..d9dd681cec6 100644 --- a/ecmascript/minifier/src/pass/mangle_names/mod.rs +++ b/ecmascript/minifier/src/pass/mangle_names/mod.rs @@ -167,7 +167,7 @@ impl VisitMut for Mangler { } fn visit_mut_module(&mut self, n: &mut Module) { - let data = analyze(&*n, self.marks); + let data = analyze(&*n, None); self.data = Some(data); self.preserved = idents_to_preserve(self.options.clone(), n); self.preserved_symbols = self.preserved.iter().map(|v| v.0.clone()).collect(); @@ -200,7 +200,7 @@ impl VisitMut for Mangler { } fn visit_mut_script(&mut self, n: &mut Script) { - let data = analyze(&*n, self.marks); + let data = analyze(&*n, None); self.data = Some(data); self.preserved = idents_to_preserve(self.options.clone(), n); self.preserved_symbols = self.preserved.iter().map(|v| v.0.clone()).collect(); diff --git a/ecmascript/minifier/src/pass/mangle_props.rs b/ecmascript/minifier/src/pass/mangle_props.rs index 22b0c08c4ac..2437c5c439c 100644 --- a/ecmascript/minifier/src/pass/mangle_props.rs +++ b/ecmascript/minifier/src/pass/mangle_props.rs @@ -1,6 +1,5 @@ use crate::analyzer::analyze; use crate::analyzer::ProgramData; -use crate::marks::Marks; use crate::option::ManglePropertiesOptions; use crate::util::base54::incr_base54; use once_cell::sync::Lazy; @@ -106,17 +105,13 @@ impl ManglePropertiesState { } } -pub(crate) fn mangle_properties<'a>( - m: &mut Module, - options: ManglePropertiesOptions, - marks: Marks, -) { +pub(crate) fn mangle_properties<'a>(m: &mut Module, options: ManglePropertiesOptions) { let mut state = ManglePropertiesState { options, ..Default::default() }; - let data = analyze(&*m, marks); + let data = analyze(&*m, None); m.visit_mut_with(&mut PropertyCollector { state: &mut state, data, diff --git a/ecmascript/minifier/src/pass/precompress.rs b/ecmascript/minifier/src/pass/precompress.rs index ef0246ce4ec..ace95aec959 100644 --- a/ecmascript/minifier/src/pass/precompress.rs +++ b/ecmascript/minifier/src/pass/precompress.rs @@ -60,7 +60,7 @@ impl PrecompressOptimizer<'_> { }); if has_decl { - let data = Some(analyze(&*stmts, self.marks)); + let data = Some(analyze(&*stmts, Some(self.marks))); stmts.visit_mut_children_with(&mut PrecompressOptimizer { options: self.options, diff --git a/ecmascript/minifier/src/util/mod.rs b/ecmascript/minifier/src/util/mod.rs index a466df212b8..63f0df6ed71 100644 --- a/ecmascript/minifier/src/util/mod.rs +++ b/ecmascript/minifier/src/util/mod.rs @@ -21,6 +21,7 @@ use swc_ecma_visit::VisitWith; pub(crate) mod base54; pub(crate) mod sort; +pub(crate) mod unit; /// pub(crate) fn make_number(span: Span, value: f64) -> Expr { diff --git a/ecmascript/minifier/src/util/unit.rs b/ecmascript/minifier/src/util/unit.rs new file mode 100644 index 00000000000..2c52af9a3b9 --- /dev/null +++ b/ecmascript/minifier/src/util/unit.rs @@ -0,0 +1,63 @@ +use crate::debug::dump; +use swc_common::Mark; +use swc_ecma_ast::*; +use swc_ecma_transforms::fixer; +use swc_ecma_utils::DropSpan; +use swc_ecma_visit::{FoldWith, VisitMut, VisitMutWith}; + +/// Inidicates a unit of minifaction. +pub(crate) trait CompileUnit: + swc_ecma_codegen::Node + Clone + VisitMutWith + VisitMutWith +{ + fn is_module() -> bool; + + fn dump(&self) -> String; + + fn apply(&mut self, visitor: &mut V) + where + V: VisitMut; + + fn remove_mark(&mut self) -> Mark; +} + +impl CompileUnit for Module { + fn is_module() -> bool { + true + } + + fn dump(&self) -> String { + dump(&self.clone().fold_with(&mut fixer(None))) + } + + fn apply(&mut self, visitor: &mut V) + where + V: VisitMut, + { + self.visit_mut_with(&mut *visitor) + } + + fn remove_mark(&mut self) -> Mark { + Mark::root() + } +} + +impl CompileUnit for FnExpr { + fn is_module() -> bool { + false + } + + fn dump(&self) -> String { + dump(&self.clone().fold_with(&mut fixer(None))) + } + + fn apply(&mut self, visitor: &mut V) + where + V: VisitMut, + { + self.visit_mut_with(&mut *visitor) + } + + fn remove_mark(&mut self) -> Mark { + self.function.span.remove_mark() + } +} diff --git a/ecmascript/minifier/tests/compress.rs b/ecmascript/minifier/tests/compress.rs index 2b24e089f00..98789ab1652 100644 --- a/ecmascript/minifier/tests/compress.rs +++ b/ecmascript/minifier/tests/compress.rs @@ -116,6 +116,10 @@ fn run( config: &str, mangle: Option, ) -> Option { + let _ = rayon::ThreadPoolBuilder::new() + .thread_name(|i| format!("rayon-{}", i + 1)) + .build_global(); + let (_module, config) = parse_compressor_config(cm.clone(), &config); let fm = cm.load_file(&input).expect("failed to load input.js"); diff --git a/ecmascript/minifier/tests/compress/fixture/projects/backbone/20/output.js b/ecmascript/minifier/tests/compress/fixture/projects/backbone/20/output.js index 57dc7cf1757..fb258f083e4 100644 --- a/ecmascript/minifier/tests/compress/fixture/projects/backbone/20/output.js +++ b/ecmascript/minifier/tests/compress/fixture/projects/backbone/20/output.js @@ -2,11 +2,11 @@ export const E = { set: function(models, options) { (options = _.defaults({ }, options, setOptions)).parse && (models = this.parse(models, options)); - var singular = !_.isArray(models); + var i, l, id, model, attrs, existing, sort, singular = !_.isArray(models); models = singular ? models ? [ models ] : [] : _.clone(models); - var i, l, id, model, attrs, existing, sort, at = options.at, targetModel = this.model, sortable = this.comparator && null == at && !1 !== options.sort, sortAttr = _.isString(this.comparator) ? this.comparator : null, toAdd = [], toRemove = [], modelMap = { + var at = options.at, targetModel = this.model, sortable = this.comparator && null == at && !1 !== options.sort, sortAttr = _.isString(this.comparator) ? this.comparator : null, toAdd = [], toRemove = [], modelMap = { }, add = options.add, merge = options.merge, remove = options.remove, order = !sortable && !!add && !!remove && []; for(i = 0, l = models.length; i < l; i++){ if (id = (attrs = models[i]) instanceof Model ? model = attrs : attrs[targetModel.prototype.idAttribute], existing = this.get(id)) remove && (modelMap[existing.cid] = !0), merge && (attrs = attrs === model ? model.attributes : attrs, options.parse && (attrs = existing.parse(attrs, options)), existing.set(attrs, options), sortable && !sort && existing.hasChanged(sortAttr) && (sort = !0)), models[i] = existing; diff --git a/ecmascript/minifier/tests/compress/fixture/projects/react/8/output.js b/ecmascript/minifier/tests/compress/fixture/projects/react/8/output.js index ceae3892a2f..503131199e3 100644 --- a/ecmascript/minifier/tests/compress/fixture/projects/react/8/output.js +++ b/ecmascript/minifier/tests/compress/fixture/projects/react/8/output.js @@ -25,14 +25,14 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) { } else null != mappedChild && (isValidElement(mappedChild) && (mappedChild = cloneAndReplaceKey(mappedChild, escapedPrefix + (mappedChild.key && (!_child || _child.key !== mappedChild.key) ? escapeUserProvidedKey("" + mappedChild.key) + "/" : "") + childKey)), array.push(mappedChild)); return 1; } - var child, subtreeCount = 0, nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + SUBSEPARATOR; + var subtreeCount = 0, nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + SUBSEPARATOR; if (Array.isArray(children)) for(var i = 0; i < children.length; i++)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = children[i], i), callback); else { var iteratorFn = getIteratorFn(children); if ("function" == typeof iteratorFn) { - var iterableChildren = children; + var child, step, iterableChildren = children; iteratorFn === iterableChildren.entries && (didWarnAboutMaps || warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."), didWarnAboutMaps = !0); - for(var step, iterator = iteratorFn.call(iterableChildren), ii = 0; !(step = iterator.next()).done;)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = step.value, ii++), callback); + for(var iterator = iteratorFn.call(iterableChildren), ii = 0; !(step = iterator.next()).done;)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = step.value, ii++), callback); } else if ("object" === type) { var childrenString = "" + children; throw Error("Objects are not valid as a React child (found: " + ("[object Object]" === childrenString ? "object with keys {" + Object.keys(children).join(", ") + "}" : childrenString) + "). If you meant to render a collection of children, use an array instead."); diff --git a/ecmascript/minifier/tests/compress/fixture/terser/issue-2435-1/4/output.js b/ecmascript/minifier/tests/compress/fixture/terser/issue-2435-1/4/output.js index 57dcf1d4c54..b3c0fa61494 100644 --- a/ecmascript/minifier/tests/compress/fixture/terser/issue-2435-1/4/output.js +++ b/ecmascript/minifier/tests/compress/fixture/terser/issue-2435-1/4/output.js @@ -1,2 +1,2 @@ -(x() || 0) && y(); +x() && y(); x(); diff --git a/ecmascript/minifier/tests/projects/output/angular-1.2.5.js b/ecmascript/minifier/tests/projects/output/angular-1.2.5.js index ba0f6ab233d..6fd166d6ed6 100644 --- a/ecmascript/minifier/tests/projects/output/angular-1.2.5.js +++ b/ecmascript/minifier/tests/projects/output/angular-1.2.5.js @@ -10,7 +10,7 @@ return new Error(message); }; } - var msie, jqLite, jQuery, angularModule, nodeName_, lowercase = function(string) { + var promiseWarning, msie, jqLite, jQuery, angularModule, nodeName_, lowercase = function(string) { return isString(string) ? string.toLowerCase() : string; }, uppercase = function(string) { return isString(string) ? string.toUpperCase() : string; @@ -1092,7 +1092,7 @@ }, remove: function(key) { var lruEntry = lruHash[key]; - lruEntry && (lruEntry == freshEnd && (freshEnd = lruEntry.p), lruEntry == staleEnd && (staleEnd = lruEntry.n), link(lruEntry.n, lruEntry.p), delete lruHash[key], delete data[key], size--); + !!lruEntry && (lruEntry == freshEnd && (freshEnd = lruEntry.p), lruEntry == staleEnd && (staleEnd = lruEntry.n), link(lruEntry.n, lruEntry.p), delete lruHash[key], delete data[key], size--); }, removeAll: function() { data = { @@ -2109,7 +2109,7 @@ return this.$$replace = !0, this; } }; - var promiseWarning, $parseMinErr = minErr("$parse"), promiseWarningCache = { + var $parseMinErr = minErr("$parse"), promiseWarningCache = { }; function ensureSafeMemberName(name, fullExpression) { if ("constructor" === name) throw $parseMinErr("isecfld", "Referencing \"constructor\" field in Angular expressions is disallowed! Expression: {0}", fullExpression); @@ -2604,9 +2604,9 @@ promise.$$v = val; })), pathVal = pathVal.$$v), key3 && null !== pathVal && pathVal !== undefined && ((pathVal = pathVal[key3]) && pathVal.then && (promiseWarning(fullExp), "$$v" in pathVal || ((promise = pathVal).$$v = undefined, promise.then(function(val) { promise.$$v = val; - })), pathVal = pathVal.$$v), key4 && null !== pathVal && pathVal !== undefined))) && (pathVal = pathVal[key4]) && pathVal.then && (promiseWarning(fullExp), "$$v" in pathVal || ((promise = pathVal).$$v = undefined, promise.then(function(val) { + })), pathVal = pathVal.$$v), key4 && null !== pathVal && pathVal !== undefined && (pathVal = pathVal[key4]) && pathVal.then && (promiseWarning(fullExp), "$$v" in pathVal || ((promise = pathVal).$$v = undefined, promise.then(function(val) { promise.$$v = val; - })), pathVal = pathVal.$$v)), pathVal; + })), pathVal = pathVal.$$v))))), pathVal; } : function(scope, locals) { var pathVal = locals && locals.hasOwnProperty(key0) ? locals : scope; return null === pathVal || pathVal === undefined ? pathVal : (pathVal = pathVal[key0], key1 && null !== pathVal && pathVal !== undefined && (pathVal = pathVal[key1], key2 && null !== pathVal && pathVal !== undefined && (pathVal = pathVal[key2], key3 && null !== pathVal && pathVal !== undefined && (pathVal = pathVal[key3], key4 && null !== pathVal && pathVal !== undefined)))) ? pathVal = pathVal[key4] : pathVal; @@ -3560,7 +3560,7 @@ priority: 99, link: function(scope, element, attr) { attr.$observe(normalized, function(value) { - value && (attr.$set(attrName, value), msie && element.prop(attrName, attr[attrName])); + !!value && (attr.$set(attrName, value), msie && element.prop(attrName, attr[attrName])); }); } }; @@ -3725,7 +3725,7 @@ }; if ($sniffer.hasEvent("input")) element.on("input", listener); else { - var timeout, deferListener = function() { + var deferListener = function() { timeout || (timeout = $browser.defer(function() { listener(), timeout = null; })); @@ -3738,7 +3738,7 @@ element.on("change", listener), ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? "" : ctrl.$viewValue); }; - var patternValidator, match, pattern = attr.ngPattern, validate = function(regexp, value) { + var timeout, patternValidator, match, pattern = attr.ngPattern, validate = function(regexp, value) { return ctrl.$isEmpty(value) || regexp.test(value) ? (ctrl.$setValidity("pattern", !0), value) : void ctrl.$setValidity("pattern", !1); }; if (pattern && (patternValidator = (match = pattern.match(/^\/(.*)\/([gim]*)$/)) ? function(value) { diff --git a/ecmascript/minifier/tests/projects/output/backbone-1.1.0.js b/ecmascript/minifier/tests/projects/output/backbone-1.1.0.js index ebc3f224444..dce90f356b3 100644 --- a/ecmascript/minifier/tests/projects/output/backbone-1.1.0.js +++ b/ecmascript/minifier/tests/projects/output/backbone-1.1.0.js @@ -305,11 +305,11 @@ set: function(models, options) { (options = _.defaults({ }, options, setOptions)).parse && (models = this.parse(models, options)); - var singular = !_.isArray(models); + var i, l, id, model, attrs, existing, sort, singular = !_.isArray(models); models = singular ? models ? [ models ] : [] : _.clone(models); - var i, l, id, model, attrs, existing, sort, at = options.at, targetModel = this.model, sortable = this.comparator && null == at && !1 !== options.sort, sortAttr = _.isString(this.comparator) ? this.comparator : null, toAdd = [], toRemove = [], modelMap = { + var at = options.at, targetModel = this.model, sortable = this.comparator && null == at && !1 !== options.sort, sortAttr = _.isString(this.comparator) ? this.comparator : null, toAdd = [], toRemove = [], modelMap = { }, add = options.add, merge = options.merge, remove = options.remove, order = !sortable && !!add && !!remove && []; for(i = 0, l = models.length; i < l; i++){ if (id = (attrs = models[i]) instanceof Model ? model = attrs : attrs[targetModel.prototype.idAttribute], existing = this.get(id)) remove && (modelMap[existing.cid] = !0), merge && (attrs = attrs === model ? model.attributes : attrs, options.parse && (attrs = existing.parse(attrs, options)), existing.set(attrs, options), sortable && !sort && existing.hasChanged(sortAttr) && (sort = !0)), models[i] = existing; diff --git a/ecmascript/minifier/tests/projects/output/jquery-1.9.1.js b/ecmascript/minifier/tests/projects/output/jquery-1.9.1.js index b82545a179e..a3c2c19ffdb 100644 --- a/ecmascript/minifier/tests/projects/output/jquery-1.9.1.js +++ b/ecmascript/minifier/tests/projects/output/jquery-1.9.1.js @@ -2043,8 +2043,8 @@ ])[1].toLowerCase()]) { value = value.replace(rxhtmlTag, "<$1>"); try { - for(; i < l; i++)1 === (elem = this[i] || { - }).nodeType && (jQuery.cleanData(getAll(elem, !1)), elem.innerHTML = value); + for(; i < l; i++)elem = this[i] || { + }, 1 === elem.nodeType && (jQuery.cleanData(getAll(elem, !1)), elem.innerHTML = value); elem = 0; } catch (e) { } @@ -2155,8 +2155,8 @@ return elem = el || elem, "none" === jQuery.css(elem, "display") || !jQuery.contains(elem.ownerDocument, elem); } function showHide(elements, show) { - for(var display, elem, hidden, values = [], index = 0, length = elements.length; index < length; index++)(elem = elements[index]).style && (values[index] = jQuery._data(elem, "olddisplay"), display = elem.style.display, show ? (values[index] || "none" !== display || (elem.style.display = ""), "" === elem.style.display && isHidden(elem) && (values[index] = jQuery._data(elem, "olddisplay", css_defaultDisplay(elem.nodeName)))) : values[index] || (hidden = isHidden(elem), (display && "none" !== display || !hidden) && jQuery._data(elem, "olddisplay", hidden ? display : jQuery.css(elem, "display")))); - for(index = 0; index < length; index++)(elem = elements[index]).style && (show && "none" !== elem.style.display && "" !== elem.style.display || (elem.style.display = show ? values[index] || "" : "none")); + for(var display, elem, hidden, values = [], index = 0, length = elements.length; index < length; index++)!!(elem = elements[index]).style && (values[index] = jQuery._data(elem, "olddisplay"), display = elem.style.display, show ? (values[index] || "none" !== display || (elem.style.display = ""), "" === elem.style.display && isHidden(elem) && (values[index] = jQuery._data(elem, "olddisplay", css_defaultDisplay(elem.nodeName)))) : values[index] || (hidden = isHidden(elem), (display && "none" !== display || !hidden) && jQuery._data(elem, "olddisplay", hidden ? display : jQuery.css(elem, "display")))); + for(index = 0; index < length; index++)!!(elem = elements[index]).style && (show && "none" !== elem.style.display && "" !== elem.style.display || (elem.style.display = show ? values[index] || "" : "none")); return elements; } function setPositiveNumber(elem, value, subtract) { diff --git a/ecmascript/minifier/tests/projects/output/jquery.mobile-1.4.2.js b/ecmascript/minifier/tests/projects/output/jquery.mobile-1.4.2.js index 1c45fcc49db..d0317780894 100644 --- a/ecmascript/minifier/tests/projects/output/jquery.mobile-1.4.2.js +++ b/ecmascript/minifier/tests/projects/output/jquery.mobile-1.4.2.js @@ -624,7 +624,7 @@ var v, uc_prop = prop.charAt(0).toUpperCase() + prop.substr(1), props = (prop + " " + vendors.join(uc_prop + " ") + uc_prop).split(" "); for(v in props)if (undefined !== fbCSS[props[v]]) return !0; } - var w, ua, platform, wkmatch, wkversion, ffmatch, ffversion, operammobilematch, omversion, fauxBase, base, supports, element, documentElement, getComputedStyle, ua1, nokiaLTE7_3, fakeBody = $17("").prependTo("html"), fbCSS = fakeBody[0].style, vendors = [ + var nokiaLTE7_3, w, ua, platform, wkmatch, wkversion, ffmatch, ffversion, operammobilematch, omversion, fauxBase, base, supports, element, documentElement, getComputedStyle, ua1, fakeBody = $17("").prependTo("html"), fbCSS = fakeBody[0].style, vendors = [ "Webkit", "Moz", "O" @@ -1207,7 +1207,7 @@ $17.event.special.swipe.eventInProgress = !0; var stop, start = $17.event.special.swipe.start(event), origTarget = event.target, emitted = !1; context.move = function(event) { - start && (stop = $17.event.special.swipe.stop(event), !emitted && (emitted = $17.event.special.swipe.handleSwipe(start, stop, thisObject, origTarget)) && ($17.event.special.swipe.eventInProgress = !1), Math.abs(start.coords[0] - stop.coords[0]) > $17.event.special.swipe.scrollSupressionThreshold && event.preventDefault()); + !!start && (stop = $17.event.special.swipe.stop(event), !emitted && (emitted = $17.event.special.swipe.handleSwipe(start, stop, thisObject, origTarget)) && ($17.event.special.swipe.eventInProgress = !1), Math.abs(start.coords[0] - stop.coords[0]) > $17.event.special.swipe.scrollSupressionThreshold && event.preventDefault()); }, context.stop = function() { emitted = !0, $17.event.special.swipe.eventInProgress = !1, $document.off(touchMoveEvent, context.move), context.move = null; }, $document.on(touchMoveEvent, context.move).one(touchStopEvent, context.stop); @@ -1282,7 +1282,7 @@ }).prependTo($7("head")), linkSelector: "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]", set: function(href) { - $7.mobile.dynamicBaseEnabled && $7.support.dynamicBaseTag && base.element.attr("href", $7.mobile.path.makeUrlAbsolute(href, $7.mobile.path.documentBase)); + !!$7.mobile.dynamicBaseEnabled && $7.support.dynamicBaseTag && base.element.attr("href", $7.mobile.path.makeUrlAbsolute(href, $7.mobile.path.documentBase)); }, rewrite: function(href, page) { var newPath = $7.mobile.path.get(href); @@ -1728,7 +1728,7 @@ }), $17.mobile.document.bind("vclick", function(event) { var $btn, btnEls, target = event.target, needClosest = !1; if (!(event.which > 1) && $17.mobile.linkBindingEnabled) { - if ($lastVClicked = $17(target), $17.data(target, "mobile-button")) getAjaxFormData($17(target).closest("form"), !0) && target.parentNode && (target = target.parentNode); + if ($lastVClicked = $17(target), $17.data(target, "mobile-button")) !!getAjaxFormData($17(target).closest("form"), !0) && target.parentNode && (target = target.parentNode); else { if (!((target = findClosestLink(target)) && "#" !== $17.mobile.path.parseUrl(target.getAttribute("href") || "#").hash)) return; if (!$17(target).jqmHijackable().length) return; @@ -3522,7 +3522,7 @@ }); }, close: function() { - !this.options.disabled && this.isOpen && ("page" === this.menuType ? (this.menuPage.dialog("close"), this.list.appendTo(this.listbox)) : this.listbox.popup("close"), this._focusButton(), this.isOpen = !1); + this.options.disabled || !this.isOpen || ("page" === this.menuType ? (this.menuPage.dialog("close"), this.list.appendTo(this.listbox)) : this.listbox.popup("close"), this._focusButton(), this.isOpen = !1); }, open: function() { this.button.click(); diff --git a/ecmascript/minifier/tests/projects/output/mootools-1.4.5.js b/ecmascript/minifier/tests/projects/output/mootools-1.4.5.js index 23236c27709..f9a3c477db4 100644 --- a/ecmascript/minifier/tests/projects/output/mootools-1.4.5.js +++ b/ecmascript/minifier/tests/projects/output/mootools-1.4.5.js @@ -1898,7 +1898,7 @@ Elements.prototype = { return document.id(Slick.find(this, expression)); } }); - var set, translations, contains = { + var set, translations, types, search, find, match, pseudos, addSlickPseudos, contains = { contains: function(element) { return Slick.contains(this, element); } @@ -2081,7 +2081,7 @@ Elements.prototype = { var value = node.value; node.type = type, node.value = value; }), input = null; - var types, search, find, match, pseudos, addSlickPseudos, div, pollutesGetAttribute = ((div = document.createElement("div")).random = "attribute", "attribute" == div.getAttribute("random")); + var div, pollutesGetAttribute = ((div = document.createElement("div")).random = "attribute", "attribute" == div.getAttribute("random")); Element1.implement({ setProperty: function(name, value) { var setter = propertySetters[name.toLowerCase()]; diff --git a/ecmascript/minifier/tests/projects/output/react-17.0.2.js b/ecmascript/minifier/tests/projects/output/react-17.0.2.js index ef6b91a1444..71b58ed889d 100644 --- a/ecmascript/minifier/tests/projects/output/react-17.0.2.js +++ b/ecmascript/minifier/tests/projects/output/react-17.0.2.js @@ -5,7 +5,7 @@ }); }(this, function(exports) { "use strict"; - var REACT_ELEMENT_TYPE = 60103, REACT_PORTAL_TYPE = 60106; + var specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs, prevLog, prevInfo, prevWarn, prevError, prevGroup, prevGroupCollapsed, prevGroupEnd, prefix, componentFrameCache, propTypesMisspellWarningShown, requestHostCallback, requestHostTimeout, cancelHostTimeout, shouldYieldToHost, requestPaint, getCurrentTime, forceFrameRate, REACT_ELEMENT_TYPE = 60103, REACT_PORTAL_TYPE = 60106; exports.Fragment = 60107, exports.StrictMode = 60108, exports.Profiler = 60114; var REACT_PROVIDER_TYPE = 60109, REACT_CONTEXT_TYPE = 60110, REACT_FORWARD_REF_TYPE = 60112; exports.Suspense = 60113; @@ -178,7 +178,7 @@ return null; } pureComponentPrototype.constructor = PureComponent, assign(pureComponentPrototype, Component.prototype), pureComponentPrototype.isPureReactComponent = !0; - var specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs, hasOwnProperty$1 = Object.prototype.hasOwnProperty, RESERVED_PROPS = { + var hasOwnProperty$1 = Object.prototype.hasOwnProperty, RESERVED_PROPS = { key: !0, ref: !0, __self: !0, @@ -276,7 +276,7 @@ } function cloneElement(element, config, children) { if (!(null != element)) throw Error("React.cloneElement(...): The argument must be a React element, but you passed " + element + "."); - var defaultProps, propName, props = assign({ + var propName, defaultProps, props = assign({ }, element.props), key = element.key, ref = element.ref, self = element._self, source = element._source, owner = element._owner; if (null != config) for(propName in hasValidRef(config) && (ref = config.ref, owner = ReactCurrentOwner.current), hasValidKey(config) && (key = "" + config.key), element.type && element.type.defaultProps && (defaultProps = element.type.defaultProps), config)hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) && (void 0 === config[propName] && void 0 !== defaultProps ? props[propName] = defaultProps[propName] : props[propName] = config[propName]); var childrenLength = arguments.length - 2; @@ -333,14 +333,14 @@ } else null != mappedChild && (isValidElement(mappedChild) && (mappedChild = cloneAndReplaceKey(mappedChild, escapedPrefix + (mappedChild.key && (!_child || _child.key !== mappedChild.key) ? escapeUserProvidedKey("" + mappedChild.key) + "/" : "") + childKey)), array.push(mappedChild)); return 1; } - var child, subtreeCount = 0, nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + ":"; + var subtreeCount = 0, nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + ":"; if (Array.isArray(children)) for(var i = 0; i < children.length; i++)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = children[i], i), callback); else { var iteratorFn = getIteratorFn(children); if ("function" == typeof iteratorFn) { - var iterableChildren = children; + var child, step, iterableChildren = children; iteratorFn === iterableChildren.entries && (didWarnAboutMaps || warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."), didWarnAboutMaps = !0); - for(var step, iterator = iteratorFn.call(iterableChildren), ii = 0; !(step = iterator.next()).done;)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = step.value, ii++), callback); + for(var iterator = iteratorFn.call(iterableChildren), ii = 0; !(step = iterator.next()).done;)subtreeCount += mapIntoArray(child, array, escapedPrefix, nextNamePrefix + getElementKey(child = step.value, ii++), callback); } else if ("object" === type) { var childrenString = "" + children; throw Error("Objects are not valid as a React child (found: " + ("[object Object]" === childrenString ? "object with keys {" + Object.keys(children).join(", ") + "}" : childrenString) + "). If you meant to render a collection of children, use an array instead."); @@ -384,7 +384,7 @@ if (!(null !== dispatcher)) throw Error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem."); return dispatcher; } - var prevLog, prevInfo, prevWarn, prevError, prevGroup, prevGroupCollapsed, prevGroupEnd, disabledDepth = 0; + var disabledDepth = 0; function disabledLog() { } function disableLogs() { @@ -449,7 +449,7 @@ disabledDepth < 0 && error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); } disabledLog.__reactDisabledLog = !0; - var prefix, ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher; + var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher; function describeBuiltInComponentFrame(name, source, ownerFn) { if (void 0 === prefix) try { throw Error(); @@ -459,7 +459,7 @@ } return "\n" + prefix + name; } - var componentFrameCache, reentry = !1; + var reentry = !1; function describeNativeComponentFrame(fn, construct) { if (!fn || reentry) return ""; var control, frame = componentFrameCache.get(fn); @@ -667,7 +667,7 @@ if (validType) for(var i = 2; i < arguments.length; i++)validateChildKeys(arguments[i], type); return type === exports.Fragment ? validateFragmentProps(element) : validatePropTypes(element), element; } - var propTypesMisspellWarningShown, requestHostCallback, requestHostTimeout, cancelHostTimeout, shouldYieldToHost, requestPaint, getCurrentTime, forceFrameRate, didWarnAboutDeprecatedCreateFactory = !1, enableSchedulerDebugging = !1, enableProfiling = !1; + var didWarnAboutDeprecatedCreateFactory = !1, enableSchedulerDebugging = !1, enableProfiling = !1; if ("object" == typeof performance && "function" == typeof performance.now) { var localPerformance = performance; getCurrentTime = function() { @@ -869,7 +869,7 @@ } }, unstable_scheduleCallback: function(priorityLevel, callback, options) { - var timeout, startTime, currentTime = getCurrentTime(); + var startTime, timeout, currentTime = getCurrentTime(); if ("object" == typeof options && null !== options) { var delay = options.delay; startTime = "number" == typeof delay && delay > 0 ? currentTime + delay : currentTime; @@ -1038,14 +1038,14 @@ return ++threadIDCounter; }, unstable_trace: function(name, timestamp, callback) { - var threadID = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 0, interaction = { + var returnValue, threadID = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 0, interaction = { __count: 1, id: interactionIDCounter++, name: name, timestamp: timestamp }, prevInteractions = interactionsRef.current, interactions = new Set(prevInteractions); interactions.add(interaction), interactionsRef.current = interactions; - var returnValue, subscriber = subscriberRef.current; + var subscriber = subscriberRef.current; try { null !== subscriber && subscriber.onInteractionTraced(interaction); } finally{ diff --git a/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js b/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js index a7c93e2f611..3036eac7ff2 100644 --- a/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js +++ b/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js @@ -6,7 +6,7 @@ }, global.React); }(this, function(exports, React) { "use strict"; - var devToolsConfig, ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + var devToolsConfig, prevLog, prevInfo, prevWarn, prevError, prevGroup, prevGroupCollapsed, prevGroupEnd, prefix, componentFrameCache, didWarnValueDefaultValue$1, reusableSVGContainer, attemptUserBlockingHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, attemptHydrationAtPriority, lastMovementX, lastMovementY, lastMouseEvent, warnedUnknownTags, suppressHydrationWarning, validatePropertiesInDevelopment, warnForTextDifference, warnForPropDifference, warnForExtraAttributes, warnForInvalidEventListener, canDiffStyleForHydrationWarning, normalizeMarkupForTextOrAttribute, normalizeHTML, SUPPRESS_HYDRATION_WARNING$1, fiberStack, warnedAboutMissingGetChildContext, rendererSigil, didWarnUpdateInsideUpdate, currentlyProcessingQueue, didWarnAboutStateAssignmentForComponent, didWarnAboutUninitializedState, didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate, didWarnAboutLegacyLifecyclesAndDerivedState, didWarnAboutUndefinedDerivedState, warnOnUndefinedDerivedState, warnOnInvalidCallback, didWarnAboutDirectlyAssigningPropsToState, didWarnAboutContextTypeAndContextTypes, didWarnAboutInvalidateContextType, didWarnAboutMaps, didWarnAboutGenerators, didWarnAboutStringRefs, ownerHasKeyUseWarning, ownerHasFunctionTypeWarning, rendererSigil$1, didWarnAboutMismatchedHooksForComponent, didWarnAboutUseOpaqueIdentifier, didWarnAboutBadClass, didWarnAboutModulePatternComponent, didWarnAboutContextTypeOnFunctionComponent, didWarnAboutGetDerivedStateOnFunctionComponent, didWarnAboutFunctionRefs, didWarnAboutReassigningProps, didWarnAboutRevealOrder, didWarnAboutTailOptions, appendAllChildren, updateHostContainer, updateHostComponent$1, updateHostText$1, beginWork$1, didWarnAboutUpdateInRenderForAnotherComponent, hasBadMapPolyfill, didWarnAboutNestedUpdates, didWarnAboutFindNodeInStrictMode, topLevelUpdateWarnings, ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; function warn(format) { for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++)args[_key - 1] = arguments[_key]; printWarning("warn", format, args); @@ -36,7 +36,7 @@ registrationNameDependencies[registrationName] && error("EventRegistry: More than one plugin attempted to publish the same registration name, `%s`.", registrationName), registrationNameDependencies[registrationName] = dependencies, possibleRegistrationNames[registrationName.toLowerCase()] = registrationName, "onDoubleClick" === registrationName && (possibleRegistrationNames.ondblclick = registrationName); for(var i = 0; i < dependencies.length; i++)allNativeEvents.add(dependencies[i]); } - var canUseDOM = !!("undefined" != typeof window && void 0 !== window.document && void 0 !== window.document.createElement), VALID_ATTRIBUTE_NAME_REGEX = /^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, hasOwnProperty = Object.prototype.hasOwnProperty, illegalAttributeNameCache = { + var canUseDOM = !!("undefined" != typeof window && void 0 !== window.document && void 0 !== window.document.createElement), ATTRIBUTE_NAME_CHAR = ":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040", VALID_ATTRIBUTE_NAME_REGEX = new RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][" + ATTRIBUTE_NAME_CHAR + "]*$"), hasOwnProperty = Object.prototype.hasOwnProperty, illegalAttributeNameCache = { }, validatedAttributeNameCache = { }; function isAttributeNameSafe(attributeName) { @@ -351,7 +351,7 @@ var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"]; return "function" == typeof maybeIterator ? maybeIterator : null; } - var prevLog, prevInfo, prevWarn, prevError, prevGroup, prevGroupCollapsed, prevGroupEnd, disabledDepth = 0; + var disabledDepth = 0; function disabledLog() { } function disableLogs() { @@ -416,7 +416,7 @@ disabledDepth < 0 && error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); } disabledLog.__reactDisabledLog = !0; - var prefix, ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; + var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function describeBuiltInComponentFrame(name, source, ownerFn) { if (void 0 === prefix) try { throw Error(); @@ -426,7 +426,7 @@ } return "\n" + prefix + name; } - var componentFrameCache, reentry = !1; + var reentry = !1; function describeNativeComponentFrame(fn, construct) { if (!fn || reentry) return ""; var control, frame = componentFrameCache.get(fn); @@ -919,7 +919,7 @@ function getChildNamespace(parentNamespace, type) { return null == parentNamespace || "http://www.w3.org/1999/xhtml" === parentNamespace ? getIntrinsicNamespace(type) : "http://www.w3.org/2000/svg" === parentNamespace && "foreignObject" === type ? "http://www.w3.org/1999/xhtml" : parentNamespace; } - var didWarnValueDefaultValue$1, reusableSVGContainer, setInnerHTML = function(func) { + var setInnerHTML = function(func) { return "undefined" != typeof MSApp && MSApp.execUnsafeLocalFunction ? function(arg0, arg1, arg2, arg3) { MSApp.execUnsafeLocalFunction(function() { return func(arg0, arg1, arg2, arg3); @@ -1926,7 +1926,7 @@ "aria-rowspan": 0, "aria-setsize": 0 }, warnedProperties = { - }, rARIA = /^(aria)-[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, rARIACamel = /^(aria)[A-Z][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, hasOwnProperty$1 = Object.prototype.hasOwnProperty; + }, rARIA = new RegExp("^(aria)-[" + ATTRIBUTE_NAME_CHAR + "]*$"), rARIACamel = new RegExp("^(aria)[A-Z][" + ATTRIBUTE_NAME_CHAR + "]*$"), hasOwnProperty$1 = Object.prototype.hasOwnProperty; function validateProperty(tagName, name) { if (hasOwnProperty$1.call(warnedProperties, name) && warnedProperties[name]) return !0; if (rARIACamel.test(name)) { @@ -1956,8 +1956,8 @@ } var validateProperty$1 = function() { }, warnedProperties$1 = { - }, _hasOwnProperty = Object.prototype.hasOwnProperty, EVENT_NAME_REGEX = /^on./, INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/, rARIA$1 = /^(aria)-[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, rARIACamel$1 = /^(aria)[A-Z][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, warnUnknownProperties = function(type, props, eventRegistry) { - var unknownProps = []; + }, _hasOwnProperty = Object.prototype.hasOwnProperty, EVENT_NAME_REGEX = /^on./, INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/, rARIA$1 = new RegExp("^(aria)-[" + ATTRIBUTE_NAME_CHAR + "]*$"), rARIACamel$1 = new RegExp("^(aria)[A-Z][" + ATTRIBUTE_NAME_CHAR + "]*$"), warnUnknownProperties = function(type, props, eventRegistry) { + var isValid, unknownProps = []; for(var key in props)(validateProperty$1 = function(tagName, name, value, eventRegistry) { if (_hasOwnProperty.call(warnedProperties$1, name) && warnedProperties$1[name]) return !0; var lowerCasedName = name.toLowerCase(); @@ -2114,7 +2114,7 @@ var fakeNode = document.createElement("react"); invokeGuardedCallbackImpl = function(name, func, context, a, b, c, d, e, f) { if (!("undefined" != typeof document)) throw Error("The `document` global was defined when React was initialized, but is not defined anymore. This can happen in a test environment if a component schedules an update from an asynchronous callback, but the test has already finished running. To solve this, you can either unmount the component at the end of your test (and ensure that any asynchronous operations get canceled in `componentWillUnmount`), or you can change the test itself to be asynchronous."); - var evt = document.createEvent("Event"), didCall = !1, didError = !0, windowEvent = window.event, windowEventDescriptor = Object.getOwnPropertyDescriptor(window, "event"); + var error, evt = document.createEvent("Event"), didCall = !1, didError = !0, windowEvent = window.event, windowEventDescriptor = Object.getOwnPropertyDescriptor(window, "event"); function restoreAfterDispatch() { fakeNode.removeEventListener(evtType, callCallback, !1), void 0 !== window.event && window.hasOwnProperty("event") && (window.event = windowEvent); } @@ -2122,7 +2122,7 @@ function callCallback() { didCall = !0, restoreAfterDispatch(), func.apply(context, funcArgs), didError = !1; } - var error, didSetError = !1, isCrossOriginError = !1; + var didSetError = !1, isCrossOriginError = !1; function handleWindowError(event) { if (didSetError = !0, null === (error = event.error) && 0 === event.colno && 0 === event.lineno && (isCrossOriginError = !0), event.defaultPrevented && null != error && "object" == typeof error) try { error._suppressLogging = !0; @@ -2529,7 +2529,7 @@ var priority = eventPriorities.get(domEventName); return void 0 === priority ? 2 : priority; } - var attemptUserBlockingHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, attemptHydrationAtPriority, _ReactInternals$Sched$1 = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing, __interactionsRef = _ReactInternals$Sched$1.__interactionsRef, __subscriberRef = _ReactInternals$Sched$1.__subscriberRef, unstable_getThreadID = _ReactInternals$Sched$1.unstable_getThreadID, unstable_wrap = _ReactInternals$Sched$1.unstable_wrap; + var _ReactInternals$Sched$1 = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing, __interactionsRef = _ReactInternals$Sched$1.__interactionsRef, __subscriberRef = _ReactInternals$Sched$1.__subscriberRef, unstable_getThreadID = _ReactInternals$Sched$1.unstable_getThreadID, unstable_wrap = _ReactInternals$Sched$1.unstable_wrap; if (_ReactInternals$Sched$1.unstable_clear, _ReactInternals$Sched$1.unstable_getCurrent, _ReactInternals$Sched$1.unstable_subscribe, _ReactInternals$Sched$1.unstable_trace, _ReactInternals$Sched$1.unstable_unsubscribe, !(null != __interactionsRef && null != __interactionsRef.current)) throw Error("It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at https://reactjs.org/link/profiling"); unstable_now(); var NoLanes = 0, NoLane = 0, SyncLane = 1; @@ -2781,7 +2781,7 @@ entanglements[index] |= entangledLanes, lanes &= ~lane; } } - var clz32 = Math.clz32 ? Math.clz32 : function(lanes) { + var clz32 = Math.clz32 ? Math.clz32 : function clz32Fallback(lanes) { return 0 === lanes ? 32 : 31 - (log(lanes) / LN2 | 0) | 0; }, log = Math.log, LN2 = Math.LN2, UserBlockingPriority$1 = unstable_UserBlockingPriority, runWithPriority = unstable_runWithPriority, _enabled = !0; function setEnabled(enabled) { @@ -2912,7 +2912,7 @@ isPersistent: functionThatReturnsTrue }), SyntheticBaseEvent; } - var lastMovementX, lastMovementY, lastMouseEvent, EventInterface = { + var EventInterface = { eventPhase: 0, bubbles: 0, cancelable: 0, @@ -3039,7 +3039,7 @@ } var SyntheticKeyboardEvent = createSyntheticEvent(_assign({ }, UIEventInterface, { - key: function(nativeEvent) { + key: function getEventKey(nativeEvent) { if (nativeEvent.key) { var key = normalizeKey[nativeEvent.key] || nativeEvent.key; if ("Unidentified" !== key) return key; @@ -3152,10 +3152,9 @@ } var isComposing = !1; function extractCompositionEvent(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget) { - var eventType, fallbackData; if (canUseCompositionEvent ? eventType = getCompositionEventType(domEventName) : isComposing ? isFallbackCompositionEnd(domEventName, nativeEvent) && (eventType = "onCompositionEnd") : isFallbackCompositionStart(domEventName, nativeEvent) && (eventType = "onCompositionStart"), !eventType) return null; useFallbackCompositionData && !isUsingKoreanIME(nativeEvent) && (isComposing || "onCompositionStart" !== eventType ? "onCompositionEnd" === eventType && isComposing && (fallbackData = getData()) : isComposing = initialize(nativeEventTarget)); - var listeners = accumulateTwoPhaseListeners(targetInst, eventType); + var eventType, fallbackData, listeners = accumulateTwoPhaseListeners(targetInst, eventType); if (listeners.length > 0) { var event = new SyntheticCompositionEvent(eventType, domEventName, null, nativeEvent, nativeEventTarget); if (dispatchQueue.push({ @@ -3310,7 +3309,7 @@ handleEventFunc && handleEventFunc(domEventName, targetNode, targetInst), "focusout" === domEventName && handleControlledInputBlur(targetNode); } function extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) { - var isOverEvent = "mouseover" === domEventName || "pointerover" === domEventName, isOutEvent = "mouseout" === domEventName || "pointerout" === domEventName; + var win, from, to, isOverEvent = "mouseover" === domEventName || "pointerover" === domEventName, isOutEvent = "mouseout" === domEventName || "pointerout" === domEventName; if (isOverEvent && (16 & eventSystemFlags) == 0) { var related = nativeEvent.relatedTarget || nativeEvent.fromElement; if (related && (getClosestInstanceFromNode(related) || isContainerMarkedAsRoot(related))) return; @@ -3322,7 +3321,7 @@ win = doc ? doc.defaultView || doc.parentWindow : window; } if (isOutEvent) { - var win, from, to, _related = nativeEvent.relatedTarget || nativeEvent.toElement; + var _related = nativeEvent.relatedTarget || nativeEvent.toElement; from = targetInst, null !== (to = _related ? getClosestInstanceFromNode(_related) : null) && (to !== getNearestMountedFiber(to) || 5 !== to.tag && 6 !== to.tag) && (to = null); } else from = null, to = targetInst; if (from !== to) { @@ -3348,7 +3347,7 @@ } return isSupported; })("input") && (!document.documentMode || document.documentMode > 9)); - var objectIs = "function" == typeof Object.is ? Object.is : function(x, y) { + var objectIs = "function" == typeof Object.is ? Object.is : function is(x, y) { return x === y && (0 !== x || 1 / x == 1 / y) || x != x && y != y; }, hasOwnProperty$2 = Object.prototype.hasOwnProperty; function shallowEqual(objA, objB) { @@ -4175,7 +4174,7 @@ } function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) { validatePropertiesInDevelopment(tag, nextRawProps); - var lastProps, nextProps, updatePayload = null; + var lastProps, nextProps, propKey, styleName, updatePayload = null; switch(tag){ case "input": lastProps = getHostProps(domElement, lastRawProps), nextProps = getHostProps(domElement, nextRawProps), updatePayload = []; @@ -4194,7 +4193,7 @@ break; } assertValidProps(tag, nextProps); - var propKey, styleName, styleUpdates = null; + var styleUpdates = null; for(propKey in lastProps)if (!nextProps.hasOwnProperty(propKey) && lastProps.hasOwnProperty(propKey) && null != lastProps[propKey]) if ("style" === propKey) { var lastStyle = lastProps[propKey]; for(styleName in lastStyle)lastStyle.hasOwnProperty(styleName) && (styleUpdates || (styleUpdates = { @@ -4235,7 +4234,6 @@ return possibleStandardNames.hasOwnProperty(lowerCasedName) ? possibleStandardNames[lowerCasedName] || null : null; } function diffHydratedProperties(domElement, tag, rawProps, parentNamespace, rootContainerElement) { - var isCustomComponentTag, extraAttributeNames; switch(suppressHydrationWarning = !0 === rawProps.suppressHydrationWarning, isCustomComponentTag = isCustomComponent(tag, rawProps), validatePropertiesInDevelopment(tag, rawProps), tag){ case "dialog": listenToNonDelegatedEvent("cancel", domElement), listenToNonDelegatedEvent("close", domElement); @@ -4247,7 +4245,7 @@ break; case "video": case "audio": - for(var i = 0; i < mediaEventTypes.length; i++)listenToNonDelegatedEvent(mediaEventTypes[i], domElement); + for(var isCustomComponentTag, extraAttributeNames, i = 0; i < mediaEventTypes.length; i++)listenToNonDelegatedEvent(mediaEventTypes[i], domElement); break; case "source": listenToNonDelegatedEvent("error", domElement); @@ -4693,7 +4691,7 @@ } function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) { var hostContextDev = hostContext; - validateDOMNesting(type, null, hostContextDev.ancestorInfo), ("string" == typeof props.children || "number" == typeof props.children) && validateDOMNesting(null, "" + props.children, updatedAncestorInfo(hostContextDev.ancestorInfo, type)), hostContextDev.namespace; + validateDOMNesting(type, null, hostContextDev.ancestorInfo), ("string" == typeof props.children || "number" == typeof props.children) && validateDOMNesting(null, "" + props.children, updatedAncestorInfo(hostContextDev.ancestorInfo, type)); var domElement = createElement(type, props, rootContainerInstance, hostContextDev.namespace); return precacheFiberNode(internalInstanceHandle, domElement), updateFiberProps(domElement, props), domElement; } @@ -4955,7 +4953,7 @@ !error$1 || error$1 instanceof Error || (setCurrentlyValidatingElement(element), error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1), setCurrentlyValidatingElement(null)), error$1 instanceof Error && !(error$1.message in loggedTypeFailures) && (loggedTypeFailures[error$1.message] = !0, setCurrentlyValidatingElement(element), error("Failed %s type: %s", location, error$1.message), setCurrentlyValidatingElement(null)); } } - var warnedUnknownTags, suppressHydrationWarning, validatePropertiesInDevelopment, warnForTextDifference, warnForPropDifference, warnForExtraAttributes, warnForInvalidEventListener, canDiffStyleForHydrationWarning, normalizeMarkupForTextOrAttribute, normalizeHTML, SUPPRESS_HYDRATION_WARNING$1, fiberStack, valueStack = []; + var valueStack = []; fiberStack = []; var index = -1; function createCursor(defaultValue) { @@ -5238,7 +5236,7 @@ }, ReactStrictModeWarnings.discardPendingWarnings = function() { pendingComponentWillMountWarnings = [], pendingUNSAFE_ComponentWillMountWarnings = [], pendingComponentWillReceivePropsWarnings = [], pendingUNSAFE_ComponentWillReceivePropsWarnings = [], pendingComponentWillUpdateWarnings = [], pendingUNSAFE_ComponentWillUpdateWarnings = [], pendingLegacyContextWarning = new Map(); }; - var warnedAboutMissingGetChildContext, rendererSigil, valueCursor = createCursor(null); + var valueCursor = createCursor(null); rendererSigil = { }; var currentlyRenderingFiber = null, lastContextDependency = null, lastContextWithAllBitsObserved = null, isDisallowedContextReadInDEV = !1; @@ -5336,7 +5334,7 @@ } return context._currentValue; } - var didWarnUpdateInsideUpdate, currentlyProcessingQueue, ForceUpdate = 2, hasForceUpdate = !1; + var ForceUpdate = 2, hasForceUpdate = !1; function initializeUpdateQueue(fiber) { var queue = { baseState: fiber.memoizedState, @@ -5530,7 +5528,7 @@ } } didWarnUpdateInsideUpdate = !1, currentlyProcessingQueue = null; - var didWarnAboutStateAssignmentForComponent, didWarnAboutUninitializedState, didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate, didWarnAboutLegacyLifecyclesAndDerivedState, didWarnAboutUndefinedDerivedState, warnOnUndefinedDerivedState, warnOnInvalidCallback, didWarnAboutDirectlyAssigningPropsToState, didWarnAboutContextTypeAndContextTypes, didWarnAboutInvalidateContextType, fakeInternalInstance = { + var fakeInternalInstance = { }, isArray = Array.isArray, emptyRefsObject = new React.Component().refs; didWarnAboutStateAssignmentForComponent = new Set(), didWarnAboutUninitializedState = new Set(), didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(), didWarnAboutLegacyLifecyclesAndDerivedState = new Set(), didWarnAboutDirectlyAssigningPropsToState = new Set(), didWarnAboutUndefinedDerivedState = new Set(), didWarnAboutContextTypeAndContextTypes = new Set(), didWarnAboutInvalidateContextType = new Set(); var didWarnOnInvalidCallback = new Set(); @@ -5710,7 +5708,7 @@ var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext); return shouldUpdate ? (hasNewLifecycles || "function" != typeof instance.UNSAFE_componentWillUpdate && "function" != typeof instance.componentWillUpdate || ("function" == typeof instance.componentWillUpdate && instance.componentWillUpdate(newProps, newState, nextContext), "function" == typeof instance.UNSAFE_componentWillUpdate && instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext)), "function" == typeof instance.componentDidUpdate && (workInProgress.flags |= Update), "function" == typeof instance.getSnapshotBeforeUpdate && (workInProgress.flags |= 256)) : ("function" == typeof instance.componentDidUpdate && (unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState) && (workInProgress.flags |= Update), "function" == typeof instance.getSnapshotBeforeUpdate && (unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState) && (workInProgress.flags |= 256), workInProgress.memoizedProps = newProps, workInProgress.memoizedState = newState), instance.props = newProps, instance.state = newState, instance.context = nextContext, shouldUpdate; } - var didWarnAboutMaps, didWarnAboutGenerators, didWarnAboutStringRefs, ownerHasKeyUseWarning, ownerHasFunctionTypeWarning, warnForMissingKey = function(child, returnFiber) { + var warnForMissingKey = function(child, returnFiber) { }; didWarnAboutMaps = !1, didWarnAboutGenerators = !1, didWarnAboutStringRefs = { }, ownerHasKeyUseWarning = { @@ -6253,7 +6251,7 @@ function getIsHydrating() { return isHydrating; } - var rendererSigil$1, workInProgressSources = []; + var workInProgressSources = []; function markSourceAsDirty(mutableSource) { workInProgressSources.push(mutableSource); } @@ -6272,7 +6270,7 @@ } rendererSigil$1 = { }; - var didWarnAboutMismatchedHooksForComponent, didWarnAboutUseOpaqueIdentifier, ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher, ReactCurrentBatchConfig$1 = ReactSharedInternals.ReactCurrentBatchConfig; + var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher, ReactCurrentBatchConfig$1 = ReactSharedInternals.ReactCurrentBatchConfig; didWarnAboutUseOpaqueIdentifier = { }, didWarnAboutMismatchedHooksForComponent = new Set(); var renderLanes = NoLanes, currentlyRenderingFiber$1 = null, currentHook = null, workInProgressHook = null, didScheduleRenderPhaseUpdate = !1, didScheduleRenderPhaseUpdateDuringThisPass = !1, currentHookNameInDev = null, hookTypesDev = null, hookTypesUpdateIndexDev = -1, ignorePreviousDependencies = !1; @@ -7317,7 +7315,7 @@ function transferActualDuration(fiber) { for(var child = fiber.child; child;)fiber.actualDuration += child.actualDuration, child = child.sibling; } - var didWarnAboutBadClass, didWarnAboutModulePatternComponent, didWarnAboutContextTypeOnFunctionComponent, didWarnAboutGetDerivedStateOnFunctionComponent, didWarnAboutFunctionRefs, didWarnAboutReassigningProps, didWarnAboutRevealOrder, didWarnAboutTailOptions, ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner, didReceiveUpdate = !1; + var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner, didReceiveUpdate = !1; function reconcileChildren(current, workInProgress, nextChildren, renderLanes) { null === current ? workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes) : workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes); } @@ -7326,10 +7324,10 @@ } function updateForwardRef(current, workInProgress, Component, nextProps, renderLanes) { if (workInProgress.type !== workInProgress.elementType) { - var innerPropTypes = Component.propTypes; + var nextChildren, innerPropTypes = Component.propTypes; innerPropTypes && checkPropTypes(innerPropTypes, nextProps, "prop", getComponentName(Component)); } - var nextChildren, render = Component.render, ref = workInProgress.ref; + var render = Component.render, ref = workInProgress.ref; if (prepareToReadContext(workInProgress, renderLanes), ReactCurrentOwner$1.current = workInProgress, setIsRendering(!0), nextChildren = renderWithHooks(current, workInProgress, render, nextProps, ref, renderLanes), 1 & workInProgress.mode) { disableLogs(); try { @@ -7386,7 +7384,7 @@ function updateOffscreenComponent(current, workInProgress, renderLanes) { var _subtreeRenderLanes, nextProps = workInProgress.pendingProps, nextChildren = nextProps.children, prevState = null !== current ? current.memoizedState : null; if ("hidden" === nextProps.mode || "unstable-defer-without-hiding" === nextProps.mode) if ((4 & workInProgress.mode) == 0) { - var nextState = { + var nextBaseLanes, nextState = { baseLanes: NoLanes }; workInProgress.memoizedState = nextState, pushRenderLanes(workInProgress, renderLanes); @@ -7397,7 +7395,7 @@ workInProgress.memoizedState = _nextState2, pushRenderLanes(workInProgress, null !== prevState ? prevState.baseLanes : renderLanes); } else { nextBaseLanes = null !== prevState ? mergeLanes(prevState.baseLanes, renderLanes) : renderLanes, markSpawnedWork(1073741824), workInProgress.lanes = workInProgress.childLanes = laneToLanes(1073741824); - var nextBaseLanes, _nextState = { + var _nextState = { baseLanes: nextBaseLanes }; return workInProgress.memoizedState = _nextState, pushRenderLanes(workInProgress, nextBaseLanes), null; @@ -7530,7 +7528,7 @@ } function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) { null !== _current && (_current.alternate = null, workInProgress.alternate = null, workInProgress.flags |= Placement); - var value, context, props = workInProgress.pendingProps; + var context, value, props = workInProgress.pendingProps; if (context = getMaskedContext(workInProgress, getUnmaskedContext(workInProgress, Component, !1)), prepareToReadContext(workInProgress, renderLanes), Component.prototype && "function" == typeof Component.prototype.render) { var componentName = getComponentName(Component) || "Unknown"; didWarnAboutBadClass[componentName] || (error("The <%s /> component appears to have a render method, but doesn't extend React.Component. This is likely to cause errors. Change %s to extend React.Component instead.", componentName, componentName), didWarnAboutBadClass[componentName] = !0); @@ -7648,7 +7646,7 @@ return (2 & workInProgress.mode) == 0 && (primaryChildFragment.lanes = renderLanes), primaryChildFragment.return = workInProgress, primaryChildFragment.sibling = null, null !== currentFallbackChildFragment && (currentFallbackChildFragment.nextEffect = null, currentFallbackChildFragment.flags = Deletion, workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment), workInProgress.child = primaryChildFragment, primaryChildFragment; } function updateSuspenseFallbackChildren(current, workInProgress, primaryChildren, fallbackChildren, renderLanes) { - var fallbackChildFragment, primaryChildFragment, mode = workInProgress.mode, currentPrimaryChildFragment = current.child, currentFallbackChildFragment = currentPrimaryChildFragment.sibling, primaryChildProps = { + var primaryChildFragment, fallbackChildFragment, mode = workInProgress.mode, currentPrimaryChildFragment = current.child, currentFallbackChildFragment = currentPrimaryChildFragment.sibling, primaryChildProps = { mode: "hidden", children: primaryChildren }; @@ -7794,11 +7792,11 @@ } var hasWarnedAboutUsingContextAsConsumer = !1; function updateContextConsumer(current, workInProgress, renderLanes) { - var context = workInProgress.type; + var newChildren, context = workInProgress.type; void 0 === context._context ? context === context.Consumer || hasWarnedAboutUsingContextAsConsumer || (hasWarnedAboutUsingContextAsConsumer = !0, error("Rendering directly is not supported and will be removed in a future major release. Did you mean to render instead?")) : context = context._context; var newProps = workInProgress.pendingProps, render = newProps.children; "function" != typeof render && error("A context consumer was rendered with multiple children, or a child that isn't a function. A context consumer expects a single child that is a function. If you did pass a function, make sure there is no trailing or leading whitespace around it."), prepareToReadContext(workInProgress, renderLanes); - var newChildren, newValue = readContext(context, newProps.unstable_observedBits); + var newValue = readContext(context, newProps.unstable_observedBits); return ReactCurrentOwner$1.current = workInProgress, setIsRendering(!0), newChildren = render(newValue), setIsRendering(!1), workInProgress.flags |= 1, reconcileChildren(current, workInProgress, newChildren, renderLanes), workInProgress.child; } function markWorkInProgressReceivedUpdate() { @@ -8145,16 +8143,17 @@ } function logCapturedError(boundary, errorInfo) { try { - if (!1 === showErrorDialog(boundary, errorInfo)) return; + var logError = showErrorDialog(boundary, errorInfo); + if (!1 === logError) return; var error = errorInfo.value; if (0) console.error(error); else { - var source = errorInfo.source, stack = errorInfo.stack, componentStack = null !== stack ? stack : ""; + var errorBoundaryMessage, source = errorInfo.source, stack = errorInfo.stack, componentStack = null !== stack ? stack : ""; if (null != error && error._suppressLogging) { if (1 === boundary.tag) return; console.error(error); } - var errorBoundaryMessage, componentName = source ? getComponentName(source.type) : null, componentNameMessage = componentName ? "The above error occurred in the <" + componentName + "> component:" : "The above error occurred in one of your React components:", errorBoundaryName = getComponentName(boundary.type); + var componentName = source ? getComponentName(source.type) : null, componentNameMessage = componentName ? "The above error occurred in the <" + componentName + "> component:" : "The above error occurred in one of your React components:", errorBoundaryName = getComponentName(boundary.type); errorBoundaryMessage = errorBoundaryName ? "React will try to recreate this component tree from scratch " + ("using the error boundary you provided, " + errorBoundaryName + ".") : "Consider adding an error boundary to your tree to customize error handling behavior.\nVisit https://reactjs.org/link/error-boundaries to learn more about error boundaries."; var combinedMessage = componentNameMessage + "\n" + componentStack + "\n\n" + ("" + errorBoundaryMessage); console.error(combinedMessage); @@ -8832,9 +8831,8 @@ } function performSyncWorkOnRoot(root) { if ((48 & executionContext) != 0) throw Error("Should not already be working."); - var lanes, exitStatus; if (flushPassiveEffects(), root === workInProgressRoot && includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes) ? (exitStatus = renderRootSync(root, lanes = workInProgressRootRenderLanes), includesSomeLane(workInProgressRootIncludedLanes, workInProgressRootUpdatedLanes) && (exitStatus = renderRootSync(root, lanes = getNextLanes(root, lanes)))) : exitStatus = renderRootSync(root, lanes = getNextLanes(root, NoLanes)), 0 !== root.tag && 2 === exitStatus && (executionContext |= 64, root.hydrate && (root.hydrate = !1, clearContainer(root.containerInfo)), (lanes = getLanesToRetrySynchronouslyOnError(root)) !== NoLanes && (exitStatus = renderRootSync(root, lanes))), 1 === exitStatus) { - var fatalError = workInProgressRootFatalError; + var lanes, exitStatus, fatalError = workInProgressRootFatalError; throw prepareFreshStack(root, NoLanes), markRootSuspended$1(root, lanes), ensureRootIsScheduled(root, now()), fatalError; } var finishedWork = root.current.alternate; @@ -9023,13 +9021,13 @@ do flushPassiveEffects(); while (null !== rootWithPendingPassiveEffects) if (flushRenderPhaseStrictModeWarningsInDEV(), (48 & executionContext) != 0) throw Error("Should not already be working."); - var finishedWork = root.finishedWork, lanes = root.finishedLanes; + var firstEffect, finishedWork = root.finishedWork, lanes = root.finishedLanes; if (null === finishedWork) return null; if (root.finishedWork = null, root.finishedLanes = NoLanes, !(finishedWork !== root.current)) throw Error("Cannot commit the same tree as before. This error is likely caused by a bug in React. Please file an issue."); root.callbackNode = null; var remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); if (markRootFinished(root, remainingLanes), null !== rootsWithPendingDiscreteUpdates && !hasDiscreteLanes(remainingLanes) && rootsWithPendingDiscreteUpdates.has(root) && rootsWithPendingDiscreteUpdates.delete(root), root === workInProgressRoot && (workInProgressRoot = null, workInProgress = null, workInProgressRootRenderLanes = NoLanes), finishedWork.flags > 1 ? null !== finishedWork.lastEffect ? (finishedWork.lastEffect.nextEffect = finishedWork, firstEffect = finishedWork.firstEffect) : firstEffect = finishedWork : firstEffect = finishedWork.firstEffect, null !== firstEffect) { - var firstEffect, prevExecutionContext = executionContext; + var prevExecutionContext = executionContext; executionContext |= 32; var prevInteractions = pushInteractions(root); ReactCurrentOwner$2.current = null, focusedInstanceHandle = prepareForCommit(root.containerInfo), shouldFireAfterActiveInstanceBlur = !1, nextEffect = firstEffect; @@ -9300,7 +9298,7 @@ throw originalError; } }; - var appendAllChildren, updateHostContainer, updateHostComponent$1, updateHostText$1, beginWork$1, didWarnAboutUpdateInRenderForAnotherComponent, didWarnAboutUpdateInRender = !1; + var didWarnAboutUpdateInRender = !1; function warnAboutRenderPhaseUpdatesInDEV(fiber) { if (isRendering && (16 & executionContext) !== 0 && !getIsUpdatingOpaqueValueInRenderPhaseInDEV()) switch(fiber.tag){ case 0: @@ -9393,7 +9391,7 @@ function finishPendingInteractions(root, committedLanes) { var subscriber, remainingLanesAfterCommit = root.pendingLanes; try { - if (null !== (subscriber = __subscriberRef.current) && root.memoizedInteractions.size > 0) { + if (subscriber = __subscriberRef.current, null !== subscriber && root.memoizedInteractions.size > 0) { var threadID = computeThreadID(root, committedLanes); subscriber.onWorkStopped(root.memoizedInteractions, threadID); } @@ -9924,7 +9922,7 @@ unmarkContainerAsRoot(container); }); }; - var hasBadMapPolyfill, didWarnAboutNestedUpdates, didWarnAboutFindNodeInStrictMode, topLevelUpdateWarnings, ReactCurrentOwner$3 = ReactSharedInternals.ReactCurrentOwner, warnedAboutHydrateAPI = !1; + var ReactCurrentOwner$3 = ReactSharedInternals.ReactCurrentOwner, warnedAboutHydrateAPI = !1; function getReactRootElementInContainer(container) { return container ? 9 === container.nodeType ? container.documentElement : container.firstChild : null; } @@ -10075,12 +10073,7 @@ var hostFiber = findCurrentHostFiber(fiber); return null === hostFiber ? null : hostFiber.stateNode; }, - findFiberByHostInstance: (devToolsConfig = { - findFiberByHostInstance: getClosestInstanceFromNode, - bundleType: 1, - version: "17.0.2", - rendererPackageName: "react-dom" - }).findFiberByHostInstance || function(instance) { + findFiberByHostInstance: devToolsConfig.findFiberByHostInstance || function(instance) { return null; }, findHostInstancesForRefresh: function(root, families) { diff --git a/ecmascript/minifier/tests/projects/output/yui-3.12.0.js b/ecmascript/minifier/tests/projects/output/yui-3.12.0.js index f9e33e271f9..ed63e7458a4 100644 --- a/ecmascript/minifier/tests/projects/output/yui-3.12.0.js +++ b/ecmascript/minifier/tests/projects/output/yui-3.12.0.js @@ -1296,6 +1296,7 @@ var YUI = function() { "yui-base" ] }), YUI.add("loader-base", function(Y, NAME) { + var VERSION, CDN_BASE, COMBO_BASE, META, groups, yui2Update, galleryUpdate; VERSION = Y.version, COMBO_BASE = (CDN_BASE = Y.Env.base) + "combo?", groups = (META = { version: VERSION, root: VERSION + "/", @@ -1356,7 +1357,7 @@ var YUI = function() { "groups", "skin" ], 0, !0), YUI.Env[VERSION] = META; - var VERSION, CDN_BASE, COMBO_BASE, META, groups, yui2Update, galleryUpdate, modulekey, NOT_FOUND = { + var modulekey, NOT_FOUND = { }, NO_REQUIREMENTS = [], GLOBAL_ENV = YUI.Env, GLOBAL_LOADED = GLOBAL_ENV._loaded, VERSION1 = Y.version, YObject = Y.Object, oeach = YObject.each, yArray = Y.Array, _queue = GLOBAL_ENV._loaderQueue, META1 = GLOBAL_ENV[VERSION1], L = Y.Lang, ON_PAGE = GLOBAL_ENV.mods, _path = function(dir, file, type, nomin) { var path = dir + "/" + file; return nomin || (path += "-min"), path += "." + (type || "css"); diff --git a/ecmascript/minifier/tests/terser/compress/block-scope/do_not_hoist_let/output.js b/ecmascript/minifier/tests/terser/compress/block-scope/do_not_hoist_let/output.js index 0e97dec2b43..514480ab695 100644 --- a/ecmascript/minifier/tests/terser/compress/block-scope/do_not_hoist_let/output.js +++ b/ecmascript/minifier/tests/terser/compress/block-scope/do_not_hoist_let/output.js @@ -1,3 +1,5 @@ function x() { - if (FOO) var var1, var2; + if (FOO) { + let let1, let2; + } } diff --git a/ecmascript/minifier/tests/terser/compress/issue-281/issue_1595_3/output.js b/ecmascript/minifier/tests/terser/compress/issue-281/issue_1595_3/output.js index 0d117bc53d0..4c14ab0a4ab 100644 --- a/ecmascript/minifier/tests/terser/compress/issue-281/issue_1595_3/output.js +++ b/ecmascript/minifier/tests/terser/compress/issue-281/issue_1595_3/output.js @@ -1,2 +1 @@ -var a; g(3);