feat(es/minifier): Implement more rules (#1766)

swc_ecma_minifier:
 - Implement more rules.

swc_ecma_transforms_base:
 - `resolver`: Fix syntax context of catch block.

swc_ecma_transforms_optimization:
 - `expr_simplifier`: Fix a bug related to `this` of call expressions.
 - `dead_branch_remover`: Don't reduce switch cases if test is now known.
 - `dead_branch_remover`: Don't break `&&`.
This commit is contained in:
강동윤 2021-06-29 16:11:22 +09:00 committed by GitHub
parent f8a3df8cc3
commit 33a43f85b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 2617 additions and 410 deletions

View File

@ -6,7 +6,7 @@ edition = "2018"
license = "Apache-2.0/MIT"
name = "swc_ecmascript"
repository = "https://github.com/swc-project/swc.git"
version = "0.43.0"
version = "0.44.0"
[package.metadata.docs.rs]
all-features = true
@ -31,7 +31,7 @@ typescript = ["swc_ecma_transforms/typescript"]
swc_ecma_ast = {version = "0.47.0", path = "./ast"}
swc_ecma_codegen = {version = "0.60.0", path = "./codegen", optional = true}
swc_ecma_dep_graph = {version = "0.29.0", path = "./dep-graph", optional = true}
swc_ecma_minifier = {version = "0.9.0", path = "./minifier", optional = true}
swc_ecma_minifier = {version = "0.10.0", path = "./minifier", optional = true}
swc_ecma_parser = {version = "0.61.0", path = "./parser", optional = true}
swc_ecma_transforms = {version = "0.57.0", path = "./transforms", optional = true}
swc_ecma_utils = {version = "0.38.0", path = "./utils", optional = true}

View File

@ -3,12 +3,13 @@ use super::*;
use crate::config::Config;
use crate::text_writer::omit_trailing_semi;
use std::{
fmt::{self, Debug, Display, Formatter},
fmt::Debug,
io::Write,
sync::{Arc, RwLock},
};
use swc_common::{comments::SingleThreadedComments, FileName, SourceMap};
use swc_ecma_parser;
use testing::DebugUsingDisplay;
struct Builder {
cfg: Config,
@ -649,12 +650,3 @@ impl Write for Buf {
self.0.write().unwrap().flush()
}
}
#[derive(PartialEq, Eq)]
struct DebugUsingDisplay<'a>(&'a str);
impl<'a> Debug for DebugUsingDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}

View File

@ -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.9.0"
version = "0.10.0"
[features]
debug = []

View File

@ -4,7 +4,10 @@
# Note that this is append-only.
set -eu
export SWC_RUN=0
cargo test --test compress --all-features \
| grep 'terser__compress' \
| grep 'js .\.\. ok$' \
| sed -e 's!test fixture_terser__compress__!!' \
| sed -e 's! ... ok!!' \

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu
export RUST_LOG=swc_ecma_minifier=trace
# To prevent regression, we run base test before real tests.
touch tests/compress.rs
cargo test --test compress ${1-base_exec} --all-features

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -eu
export SWC_RUN=0
cargo test --test compress --all-features ${1-''} \
| grep 'terser__compress' \
| grep 'js .\.\. FAILED$' \

View File

@ -10,6 +10,7 @@ find tests/compress/exec -name output.js | xargs -L 1 rm
find tests/compress/exec -name expected.stdout | xargs -L 1 rm
find tests/compress/exec -name output.terser.js | xargs -L 1 rm
find tests/compress/exec -name mangle.json | xargs -L 1 rm
find tests/compress/exec -empty -type d -delete
cargo test --test compress --all-features ${1-''} \
| grep 'terser__compress' \
@ -18,6 +19,10 @@ cargo test --test compress --all-features ${1-''} \
| sed -e 's! ... FAILED!!' \
| sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \
>> tests/postponed.txt
>> tests/postponed_candidates.txt
comm -23 tests/postponed_candidates.txt tests/golden.txt >> tests/postponed.txt
rm tests/postponed_candidates.txt
./scripts/sort.sh

View File

@ -15,7 +15,8 @@ export RUST_LOG=swc_ecma_minifier=trace
# To prevent regression, we run base test before real tests.
touch tests/compress.rs
cargo test --test compress base_exec --all-features
UPDATE=1 ./scripts/base.sh base_fixture
./scripts/base.sh base_exec
if [ -z "$@" ]; then
./scripts/sort.sh

View File

@ -5,6 +5,7 @@ find tests/compress/exec -name output.js | xargs -L 1 rm
find tests/compress/exec -name expected.stdout | xargs -L 1 rm
find tests/compress/exec -name output.terser.js | xargs -L 1 rm
find tests/compress/exec -name mangle.json | xargs -L 1 rm
find tests/compress/exec -empty -type d -delete
./scripts/run.sh \
| grep 'base_exec_compress__exec__terser__' \

View File

@ -1,8 +1,7 @@
use swc_ecma_ast::VarDeclKind;
use super::UsageAnalyzer;
use std::ops::Deref;
use std::ops::DerefMut;
use swc_ecma_ast::VarDeclKind;
impl UsageAnalyzer {
pub(super) fn with_ctx(&mut self, ctx: Ctx) -> WithCtx {
@ -22,6 +21,15 @@ pub(super) struct Ctx {
pub in_pat_of_var_decl_with_init: bool,
pub in_pat_of_param: bool,
pub in_catch_param: bool,
/// `true` for `foo.bar` and `false` for `foo` in `foo.bar++`
pub is_exact_reassignment: bool,
/// `true` for arugments of [swc_ecma_ast::Expr::Call] or
/// [swc_ecma_ast::Expr::New]
pub in_call_arg: bool,
/// `false` for `array` in `array.length.
pub is_exact_arg: bool,
pub in_left_of_for_loop: bool,

View File

@ -1,4 +1,6 @@
use self::ctx::Ctx;
use crate::util::can_end_conditionally;
use crate::util::idents_used_by;
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use std::collections::hash_map::Entry;
@ -6,6 +8,7 @@ use swc_atoms::JsWord;
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_type;
@ -28,14 +31,14 @@ where
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
let top_scope = v.scope;
v.data.top.merge(top_scope);
v.data.top.merge(top_scope, false);
v.data
}
#[derive(Debug, Default)]
pub(crate) struct VarUsageInfo {
/// # of reference to this identifier.
/// The number of reference to this identifier.
pub ref_count: usize,
/// `true` if a varaible is conditionally initialized.
@ -43,14 +46,19 @@ pub(crate) struct VarUsageInfo {
/// `false` if it's only used.
pub declared: bool,
pub declared_count: usize,
/// `true` if the enclosing function defines this variable as a parameter.
pub declared_as_fn_param: bool,
pub assign_count: usize,
pub mutation_by_call_count: usize,
pub usage_count: usize,
/// The variable itself is modified.
pub reassigned: bool,
/// The variable itself or a property of it is modified.
pub mutated: bool,
pub has_property_access: bool,
pub accessed_props: FxHashSet<JsWord>,
@ -66,6 +74,7 @@ pub(crate) struct VarUsageInfo {
pub used_in_loop: bool,
pub var_kind: Option<VarDeclKind>,
pub var_initialized: bool,
pub declared_as_catch_param: bool,
@ -78,6 +87,12 @@ pub(crate) struct VarUsageInfo {
infects: Vec<Id>,
}
impl VarUsageInfo {
pub fn is_mutated_only_by_one_call(&self) -> bool {
self.assign_count == 0 && self.mutation_by_call_count == 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScopeKind {
Fn,
@ -88,12 +103,21 @@ enum ScopeKind {
pub(crate) struct ScopeData {
pub has_with_stmt: bool,
pub has_eval_call: bool,
/// Variables declared in the scope.
pub declared_symbols: FxHashMap<JsWord, FxHashSet<SyntaxContext>>,
}
impl ScopeData {
fn merge(&mut self, other: ScopeData) {
fn merge(&mut self, other: ScopeData, is_child: bool) {
self.has_with_stmt |= other.has_with_stmt;
self.has_eval_call |= other.has_eval_call;
if !is_child {
for (k, v) in other.declared_symbols {
self.declared_symbols.entry(k).or_default().extend(v);
}
}
}
}
@ -105,15 +129,18 @@ pub(crate) struct ProgramData {
pub top: ScopeData,
pub scopes: FxHashMap<SyntaxContext, ScopeData>,
/// { dependant: [dependendcies] }
var_deps: FxHashMap<Id, FxHashSet<Id>>,
}
impl ProgramData {
fn merge(&mut self, kind: ScopeKind, child: ProgramData) {
for (ctxt, scope) in child.scopes {
let to = self.scopes.entry(ctxt).or_default();
self.top.merge(scope.clone());
self.top.merge(scope.clone(), true);
to.merge(scope);
to.merge(scope, false);
}
for (id, var_info) in child.vars {
@ -121,11 +148,15 @@ impl ProgramData {
Entry::Occupied(mut e) => {
e.get_mut().ref_count += var_info.ref_count;
e.get_mut().cond_init |= var_info.cond_init;
e.get_mut().reassigned |= var_info.reassigned;
e.get_mut().mutated |= var_info.mutated;
e.get_mut().has_property_access |= var_info.has_property_access;
e.get_mut().exported |= var_info.exported;
e.get_mut().declared |= var_info.declared;
e.get_mut().declared_count += var_info.declared_count;
e.get_mut().declared_as_fn_param |= var_info.declared_as_fn_param;
// If a var is registered at a parent scope, it means that it's delcared before
@ -134,6 +165,7 @@ impl ProgramData {
// e.get_mut().used_above_decl |= var_info.used_above_decl;
e.get_mut().used_in_loop |= var_info.used_in_loop;
e.get_mut().assign_count += var_info.assign_count;
e.get_mut().mutation_by_call_count += var_info.mutation_by_call_count;
e.get_mut().usage_count += var_info.usage_count;
e.get_mut().declared_as_catch_param |= var_info.declared_as_catch_param;
@ -153,6 +185,12 @@ impl ProgramData {
}
}
}
/// TODO: Make this recursive
#[allow(unused)]
pub fn deps(&self, id: &Id) -> FxHashSet<Id> {
self.var_deps.get(id).cloned().unwrap_or_default()
}
}
/// This assumes there are no two variable with same name and same span hygiene.
@ -175,42 +213,52 @@ impl UsageAnalyzer {
};
let ret = op(&mut child);
{
let child_scope = child.data.scopes.entry(child_ctxt).or_default();
child
.data
.scopes
.entry(child_ctxt)
.or_default()
.merge(child.scope);
child_scope.merge(child.scope, false);
}
self.data.merge(kind, child.data);
ret
}
fn report(&mut self, i: Id, is_assign: bool) {
fn report(&mut self, i: Id, is_modify: bool, dejavu: &mut FxHashSet<Id>) {
let is_first = dejavu.is_empty();
if !dejavu.insert(i.clone()) {
return;
}
let e = self.data.vars.entry(i).or_insert_with(|| VarUsageInfo {
used_above_decl: true,
..Default::default()
});
e.ref_count += 1;
e.reassigned |= is_assign;
e.reassigned |= is_first && is_modify && self.ctx.is_exact_reassignment;
// Passing object as a argument is possibly modification.
e.mutated |= is_modify || (self.ctx.in_call_arg && self.ctx.is_exact_arg);
e.used_in_loop |= self.ctx.in_loop;
if is_assign {
if is_modify && self.ctx.is_exact_reassignment {
e.assign_count += 1;
for other in e.infects.clone() {
self.report(other, true)
self.report(other, true, dejavu)
}
} else {
if self.ctx.in_call_arg && self.ctx.is_exact_arg {
e.mutation_by_call_count += 1;
}
e.usage_count += 1;
}
}
fn report_usage(&mut self, i: &Ident, is_assign: bool) {
self.report(i.to_id(), is_assign)
self.report(i.to_id(), is_assign, &mut Default::default())
}
fn declare_decl(
@ -223,12 +271,26 @@ impl UsageAnalyzer {
.data
.vars
.entry(i.to_id())
.and_modify(|v| {
if has_init {
v.mutated = true;
v.reassigned = true;
v.assign_count += 1;
}
})
.or_insert_with(|| VarUsageInfo {
is_fn_local: true,
var_kind: kind,
var_initialized: has_init,
..Default::default()
});
self.scope
.declared_symbols
.entry(i.sym.clone())
.or_default()
.insert(i.span.ctxt);
v.declared_count += 1;
v.declared = true;
if self.ctx.in_cond && has_init {
v.cond_init = true;
@ -268,6 +330,7 @@ impl Visit for UsageAnalyzer {
fn visit_assign_expr(&mut self, n: &AssignExpr, _: &dyn Node) {
let ctx = Ctx {
in_assign_lhs: true,
is_exact_reassignment: true,
..self.ctx
};
n.left.visit_with(n, &mut *self.with_ctx(ctx));
@ -282,7 +345,15 @@ impl Visit for UsageAnalyzer {
}
fn visit_call_expr(&mut self, n: &CallExpr, _: &dyn Node) {
n.visit_children_with(self);
{
n.callee.visit_with(n, self);
let ctx = Ctx {
in_call_arg: true,
is_exact_arg: true,
..self.ctx
};
n.args.visit_with(n, &mut *self.with_ctx(ctx));
}
match &n.callee {
ExprOrSuper::Expr(callee) => match &**callee {
@ -323,6 +394,7 @@ impl Visit for UsageAnalyzer {
fn visit_do_while_stmt(&mut self, n: &DoWhileStmt, _: &dyn Node) {
let ctx = Ctx {
in_loop: true,
in_cond: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
@ -355,12 +427,14 @@ impl Visit for UsageAnalyzer {
self.with_child(n.span.ctxt, ScopeKind::Block, |child| {
let ctx = Ctx {
in_left_of_for_loop: true,
is_exact_reassignment: true,
..child.ctx
};
n.left.visit_with(n, &mut *child.with_ctx(ctx));
let ctx = Ctx {
in_loop: true,
in_cond: true,
..child.ctx
};
n.body.visit_with(n, &mut *child.with_ctx(ctx))
@ -373,12 +447,14 @@ impl Visit for UsageAnalyzer {
self.with_child(n.span.ctxt, ScopeKind::Block, |child| {
let ctx = Ctx {
in_left_of_for_loop: true,
is_exact_reassignment: true,
..child.ctx
};
n.left.visit_with(n, &mut *child.with_ctx(ctx));
let ctx = Ctx {
in_loop: true,
in_cond: true,
..child.ctx
};
n.body.visit_with(n, &mut *child.with_ctx(ctx))
@ -390,6 +466,7 @@ impl Visit for UsageAnalyzer {
let ctx = Ctx {
in_loop: true,
in_cond: true,
..self.ctx
};
@ -427,10 +504,24 @@ impl Visit for UsageAnalyzer {
}
fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) {
e.obj.visit_with(&Invalid { span: DUMMY_SP }, self);
{
let ctx = Ctx {
is_exact_arg: false,
is_exact_reassignment: false,
..self.ctx
};
e.obj
.visit_with(&Invalid { span: DUMMY_SP }, &mut *self.with_ctx(ctx));
}
if e.computed {
e.prop.visit_with(&Invalid { span: DUMMY_SP }, self);
let ctx = Ctx {
is_exact_arg: false,
is_exact_reassignment: false,
..self.ctx
};
e.prop
.visit_with(&Invalid { span: DUMMY_SP }, &mut *self.with_ctx(ctx));
}
match &e.obj {
@ -460,6 +551,18 @@ impl Visit for UsageAnalyzer {
n.visit_children_with(self);
}
fn visit_new_expr(&mut self, n: &NewExpr, _: &dyn Node) {
{
n.callee.visit_with(n, self);
let ctx = Ctx {
in_call_arg: true,
is_exact_arg: true,
..self.ctx
};
n.args.visit_with(n, &mut *self.with_ctx(ctx));
}
}
fn visit_param(&mut self, n: &Param, _: &dyn Node) {
let ctx = Ctx {
in_pat_of_param: false,
@ -469,6 +572,7 @@ impl Visit for UsageAnalyzer {
let ctx = Ctx {
in_pat_of_param: true,
var_decl_kind_of_pat: None,
..self.ctx
};
n.pat.visit_with(n, &mut *self.with_ctx(ctx));
@ -501,6 +605,7 @@ impl Visit for UsageAnalyzer {
if in_left_of_for_loop {
v.reassigned = true;
v.mutated = true;
}
} else {
self.report_usage(&i.id, true);
@ -526,16 +631,18 @@ impl Visit for UsageAnalyzer {
}
fn visit_setter_prop(&mut self, n: &SetterProp, _: &dyn Node) {
n.key.visit_with(n, self);
{
let ctx = Ctx {
in_pat_of_param: true,
..self.ctx
};
n.param.visit_with(n, &mut *self.with_ctx(ctx));
}
self.with_child(n.span.ctxt, ScopeKind::Fn, |a| {
n.key.visit_with(n, a);
{
let ctx = Ctx {
in_pat_of_param: true,
..a.ctx
};
n.param.visit_with(n, &mut *a.with_ctx(ctx));
}
n.body.visit_with(n, self);
n.body.visit_with(n, a);
});
}
fn visit_stmt(&mut self, n: &Stmt, _: &dyn Node) {
@ -546,6 +653,33 @@ impl Visit for UsageAnalyzer {
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_stmts(&mut self, stmts: &[Stmt], _: &dyn Node) {
let mut had_cond = false;
for n in stmts {
let ctx = Ctx {
in_cond: self.ctx.in_cond || had_cond,
..self.ctx
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut *self.with_ctx(ctx));
had_cond |= can_end_conditionally(n);
}
}
fn visit_switch_case(&mut self, n: &SwitchCase, _: &dyn Node) {
n.test.visit_with(n, self);
{
let ctx = Ctx {
in_cond: true,
..self.ctx
};
n.cons.visit_with(n, &mut *self.with_ctx(ctx));
}
}
fn visit_try_stmt(&mut self, n: &TryStmt, _: &dyn Node) {
let ctx = Ctx {
in_cond: true,
@ -558,6 +692,7 @@ impl Visit for UsageAnalyzer {
fn visit_update_expr(&mut self, n: &UpdateExpr, _: &dyn Node) {
let ctx = Ctx {
in_update_arg: true,
is_exact_reassignment: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
@ -572,13 +707,24 @@ impl Visit for UsageAnalyzer {
for decl in &n.decls {
match (&decl.name, decl.init.as_deref()) {
(Pat::Ident(var), Some(Expr::Ident(init))) => {
self.data
.vars
.entry(init.to_id())
.or_default()
.infects
.push(var.to_id());
(Pat::Ident(var), Some(init)) => {
let used_idents = idents_used_by(init);
for id in used_idents {
self.data
.vars
.entry(id.clone())
.or_default()
.infects
.push(var.to_id());
self.data
.vars
.entry(var.to_id())
.or_default()
.infects
.push(id);
}
}
_ => {}
}
@ -593,12 +739,24 @@ impl Visit for UsageAnalyzer {
};
e.name.visit_with(e, &mut *self.with_ctx(ctx));
let declared_names: Vec<Id> = find_ids(&e.name);
let used_names = idents_used_by(&e.init);
for name in declared_names {
self.data
.var_deps
.entry(name)
.or_default()
.extend(used_names.clone());
}
e.init.visit_with(e, self);
}
fn visit_while_stmt(&mut self, n: &WhileStmt, _: &dyn Node) {
let ctx = Ctx {
in_loop: true,
in_cond: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));

View File

@ -136,7 +136,7 @@ impl VisitMut for Compressor<'_> {
panic!("Infinite loop detected")
}
let start = if cfg!(debug_assertions) {
let start = if cfg!(feature = "debug") {
let start = dump(&*n);
log::trace!("===== Start =====\n{}", start);
start
@ -150,10 +150,12 @@ impl VisitMut for Compressor<'_> {
self.changed |= visitor.changed();
if visitor.changed() {
log::trace!("compressor: Simplified expressions");
log::trace!("===== Simplified =====\n{}", dump(&*n));
if cfg!(feature = "debug") {
log::trace!("===== Simplified =====\n{}", dump(&*n));
}
}
if cfg!(debug_assertions) && !visitor.changed() {
if cfg!(feature = "debug") && !visitor.changed() {
let simplified = dump(&*n);
if start != simplified {
assert_eq!(
@ -176,8 +178,27 @@ impl VisitMut for Compressor<'_> {
}
if self.options.conditionals || self.options.dead_code {
let start = if cfg!(feature = "debug") {
dump(&*n)
} else {
"".into()
};
let mut v = dead_branch_remover();
n.map_with_mut(|n| n.fold_with(&mut v));
if cfg!(feature = "debug") {
let simplified = dump(&*n);
if start != simplified {
log::trace!(
"===== Removed dead branches =====\n{}\n==== ===== ===== ===== ======\n{}",
start,
simplified
);
}
}
self.changed |= v.changed();
}
@ -223,7 +244,7 @@ impl VisitMut for Compressor<'_> {
}
#[derive(PartialEq, Eq)]
struct DebugUsingDisplay<'a>(&'a str);
struct DebugUsingDisplay<'a>(pub &'a str);
impl<'a> Debug for DebugUsingDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {

View File

@ -1,11 +1,11 @@
use std::iter::repeat_with;
use crate::analyzer::analyze;
use super::Optimizer;
use crate::analyzer::analyze;
use crate::compress::optimize::is_left_access_to_arguments;
use std::iter::repeat_with;
use swc_atoms::js_word;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::private_ident;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
@ -53,7 +53,7 @@ impl Optimizer<'_> {
return;
}
if f.params.iter().any(|param| match param.pat {
if f.params.iter().any(|param| match &param.pat {
Pat::Ident(BindingIdent {
id:
Ident {
@ -62,7 +62,12 @@ impl Optimizer<'_> {
},
..
}) => true,
Pat::Ident(..) => false,
Pat::Ident(i) => self
.data
.as_ref()
.and_then(|v| v.vars.get(&i.id.to_id()))
.map(|v| v.declared_count >= 2)
.unwrap_or(false),
_ => true,
}) {
return;
@ -82,6 +87,7 @@ impl Optimizer<'_> {
params: &mut f.params,
changed: false,
keep_fargs: self.options.keep_fargs,
prevent: false,
};
// We visit body two time, to use simpler logic in `inject_params_if_required`
@ -96,6 +102,7 @@ struct ArgReplacer<'a> {
params: &'a mut Vec<Param>,
changed: bool,
keep_fargs: bool,
prevent: bool,
}
impl ArgReplacer<'_> {
@ -126,7 +133,19 @@ impl ArgReplacer<'_> {
impl VisitMut for ArgReplacer<'_> {
noop_visit_mut_type!();
fn visit_mut_assign_expr(&mut self, n: &mut AssignExpr) {
n.visit_mut_children_with(self);
if is_left_access_to_arguments(&n.left) {
self.prevent = true;
}
}
fn visit_mut_expr(&mut self, n: &mut Expr) {
if self.prevent {
return;
}
n.visit_mut_children_with(self);
match n {
@ -197,6 +216,10 @@ impl VisitMut for ArgReplacer<'_> {
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
if self.prevent {
return;
}
n.obj.visit_mut_with(self);
if n.computed {

View File

@ -339,6 +339,22 @@ impl Optimizer<'_> {
}
}
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::trace!("bools: `expr || false` => `expr` (in bool context)");
self.changed = true;
*n = *left.take();
return;
}
}
_ => {
let span = n.span();
let v = n.as_pure_bool();

View File

@ -157,7 +157,7 @@ impl Optimizer<'_> {
.and_then(|data| data.vars.get(&name.to_id()))
{
if var.usage_count != 1
|| var.reassigned
|| var.mutated
|| !SimpleUsageFinder::find(&name.id, r)
{
return None;

View File

@ -311,7 +311,7 @@ impl Optimizer<'_> {
op: op!("&&"),
right: Box::new(alt.take()),
}));
self.compress_logical_exprs_as_bang_bang(&mut expr);
self.compress_logical_exprs_as_bang_bang(&mut expr, true);
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr,
@ -326,7 +326,7 @@ impl Optimizer<'_> {
op: op!("||"),
right: Box::new(alt.take()),
}));
self.compress_logical_exprs_as_bang_bang(&mut expr);
self.compress_logical_exprs_as_bang_bang(&mut expr, false);
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr,

View File

@ -39,7 +39,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}
@ -104,7 +104,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}
@ -320,7 +320,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}
@ -434,7 +434,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}
@ -755,7 +755,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}
@ -886,7 +886,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.in_lhs_of_assign {
return;
}

View File

@ -18,7 +18,7 @@ impl Optimizer<'_> {
if !self
.data
.as_ref()
.and_then(|data| data.vars.get(&name.to_id()).map(|v| !v.reassigned))
.and_then(|data| data.vars.get(&name.to_id()).map(|v| !v.mutated))
.unwrap_or(false)
{
return;

View File

@ -7,6 +7,7 @@ use fxhash::FxHashMap;
use std::collections::HashMap;
use std::mem::replace;
use std::mem::swap;
use swc_atoms::js_word;
use swc_common::pass::Either;
use swc_common::Spanned;
use swc_common::DUMMY_SP;
@ -159,12 +160,33 @@ impl Optimizer<'_> {
for (idx, param) in params.iter().enumerate() {
let arg = e.args.get(idx).map(|v| &v.expr);
if let Pat::Ident(param) = &param {
if let Some(usage) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&param.to_id()))
{
if usage.reassigned {
continue;
}
}
if let Some(arg) = arg {
let should_be_inlined = self.can_be_inlined_for_iife(arg);
if should_be_inlined {
log::trace!(
"iife: Trying to inline {}{:?}",
param.id.sym,
param.id.span.ctxt
);
vars.insert(param.to_id(), arg.clone());
}
} else {
log::trace!(
"iife: Trying to inline {}{:?} (undefined)",
param.id.sym,
param.id.span.ctxt
);
vars.insert(param.to_id(), undefined(param.span()));
}
}
@ -172,9 +194,11 @@ impl Optimizer<'_> {
match find_body(callee) {
Some(Either::Left(body)) => {
log::debug!("inline: Inlining arguments");
self.inline_vars_in_node(body, vars);
}
Some(Either::Right(body)) => {
log::debug!("inline: Inlining arguments");
self.inline_vars_in_node(body, vars);
}
_ => {}
@ -328,6 +352,12 @@ impl Optimizer<'_> {
return;
}
if let Some(i) = &f.ident {
if idents_used_by(&f.function.body).contains(&i.to_id()) {
return;
}
}
if is_param_used_by_body(&f.function.params, &f.function.body) {
return;
}
@ -354,8 +384,12 @@ impl Optimizer<'_> {
fn inline_fn_like(&mut self, body: &mut BlockStmt) -> Option<Expr> {
if !body.stmts.iter().all(|stmt| match stmt {
Stmt::Expr(e) if e.expr.is_await_expr() => false,
Stmt::Expr(..) => true,
Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
Some(Expr::Await(..)) => false,
Some(Expr::Lit(Lit::Num(..))) => {
if self.ctx.in_obj_of_non_computed_member {
false
@ -497,5 +531,11 @@ where
}
}
for (sym, _) in used {
if sym == js_word!("arguments") {
return true;
}
}
false
}

View File

@ -1,3 +1,6 @@
use crate::compress::optimize::util::class_has_side_effect;
use crate::compress::optimize::util::is_valid_for_lhs;
use super::Optimizer;
use swc_atoms::js_word;
use swc_common::Spanned;
@ -21,6 +24,15 @@ impl Optimizer<'_> {
let should_preserve = (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level();
if self
.data
.as_ref()
.map(|v| v.top.has_eval_call)
.unwrap_or(false)
{
return;
}
// TODO: Check for side effect between original decl position and inlined
// position
@ -52,7 +64,7 @@ impl Optimizer<'_> {
return;
}
if self.options.reduce_vars && self.options.typeofs && !usage.reassigned {
if !usage.reassigned {
match &**init {
Expr::Fn(..) | Expr::Arrow(..) => {
self.typeofs.insert(i.to_id(), js_word!("function"));
@ -78,8 +90,9 @@ impl Optimizer<'_> {
|| self.options.collapse_vars
|| self.options.inline != 0;
// Mutation of properties are ok
if is_inline_enabled
&& !usage.reassigned
&& (!usage.mutated || (usage.assign_count == 0 && !usage.reassigned))
&& match &**init {
Expr::Lit(lit) => match lit {
Lit::Str(_)
@ -123,7 +136,7 @@ impl Optimizer<'_> {
// Single use => inlined
if is_inline_enabled
&& !should_preserve
&& !usage.reassigned
&& (!usage.mutated || usage.is_mutated_only_by_one_call())
&& usage.ref_count == 1
{
match &**init {
@ -146,11 +159,25 @@ impl Optimizer<'_> {
}
match &**init {
Expr::Lit(Lit::Regex(..)) => return,
Expr::This(..) => {
// Don't inline this if it passes function boundaries.
if !usage.is_fn_local {
return;
}
}
_ => {}
}
if init.may_have_side_effects() {
// TODO: Inline partially
return;
if !self
.vars_accessible_without_side_effect
.contains(&i.to_id())
{
// TODO: Inline partially
return;
}
}
log::trace!(
@ -192,10 +219,6 @@ impl Optimizer<'_> {
/// Stores `typeof` of [ClassDecl] and [FnDecl].
pub(super) fn store_typeofs(&mut self, decl: &mut Decl) {
if !self.options.reduce_vars || !self.options.typeofs {
return;
}
let i = match &*decl {
Decl::Class(v) => v.ident.clone(),
Decl::Fn(f) => f.ident.clone(),
@ -259,6 +282,10 @@ impl Optimizer<'_> {
return;
}
if self.ctx.is_exported {
return;
}
if let Some(usage) = self
.data
.as_ref()
@ -268,6 +295,10 @@ impl Optimizer<'_> {
return;
}
if usage.reassigned {
return;
}
// Inline very simple functions.
match decl {
Decl::Fn(f) if self.options.inline >= 2 && f.ident.sym != *"arguments" => {
@ -309,15 +340,8 @@ impl Optimizer<'_> {
&& !usage.used_in_loop
{
match decl {
Decl::Class(ClassDecl {
class:
Class {
super_class: Some(super_class),
..
},
..
}) => {
if super_class.may_have_side_effects() {
Decl::Class(ClassDecl { class, .. }) => {
if class_has_side_effect(&class) {
return;
}
}
@ -378,7 +402,7 @@ impl Optimizer<'_> {
if let Some(value) = self.lits.get(&i.to_id()).cloned() {
match &*value {
Expr::Lit(Lit::Num(..)) => {
if self.ctx.is_lhs_of_assign {
if self.ctx.in_lhs_of_assign {
return;
}
}
@ -389,7 +413,11 @@ impl Optimizer<'_> {
log::trace!("inline: Replacing a variable with cheap expression");
*e = *value;
} else if let Some(value) = self.vars_for_inlining.remove(&i.to_id()) {
} else if let Some(value) = self.vars_for_inlining.get(&i.to_id()) {
if self.ctx.is_exact_lhs_of_assign && !is_valid_for_lhs(&value) {
return;
}
self.changed = true;
log::trace!(
"inline: Replacing '{}{:?}' with an expression",
@ -397,7 +425,7 @@ impl Optimizer<'_> {
i.span.ctxt
);
*e = *value;
*e = *value.clone();
}
}
_ => {}

View File

@ -60,8 +60,22 @@ impl Optimizer<'_> {
};
// We only care about instant breaks.
if !f.body.is_break_stmt() {
return;
match &*f.body {
Stmt::Break(BreakStmt { label: None, .. }) => {}
Stmt::Break(BreakStmt {
label: Some(label), ..
}) => {
if let Some(closest_label) = self.label.clone() {
if closest_label.0 != label.sym {
return;
}
} else {
return;
}
}
_ => {
return;
}
}
self.changed = true;

View File

@ -4,6 +4,7 @@ use crate::analyzer::UsageAnalyzer;
use crate::option::CompressOptions;
use crate::util::contains_leaping_yield;
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use retain_mut::RetainMut;
use std::fmt::Write;
use std::mem::take;
@ -86,6 +87,8 @@ pub(super) fn optimizer<'a>(
ctx: Default::default(),
done,
done_ctxt,
vars_accessible_without_side_effect: Default::default(),
label: Default::default(),
}
}
@ -114,7 +117,9 @@ struct Ctx {
is_delete_arg: bool,
/// `true` if we are in `arg` of `++arg` or `--arg`.
is_update_arg: bool,
is_lhs_of_assign: bool,
in_lhs_of_assign: bool,
/// `false` for `d` in `d[0] = foo`.
is_exact_lhs_of_assign: bool,
/// `true` for loop bodies and conditions of loops.
executed_multiple_time: bool,
@ -134,6 +139,8 @@ struct Ctx {
/// `true` while we are in a function or something simillar.
in_fn_like: bool,
in_block: bool,
in_obj_of_non_computed_member: bool,
in_tpl_expr: bool,
@ -141,11 +148,24 @@ struct Ctx {
/// True while handling callee, except an arrow expression in callee.
is_this_aware_callee: bool,
can_inline_arguments: bool,
/// Current scope.
scope: SyntaxContext,
}
impl Ctx {
pub fn is_top_level_for_block_level_vars(self) -> bool {
if self.top_level {
return true;
}
if self.in_fn_like || self.in_block {
return false;
}
true
}
pub fn in_top_level(self) -> bool {
self.top_level || !self.in_fn_like
}
@ -181,6 +201,11 @@ struct Optimizer<'a> {
/// In future: This will be used to `mark` node as done.
done: Mark,
done_ctxt: SyntaxContext,
vars_accessible_without_side_effect: FxHashSet<Id>,
/// Closest label.
label: Option<Id>,
}
impl Repeated for Optimizer<'_> {
@ -570,18 +595,41 @@ impl Optimizer<'_> {
}
}
fn remove_invalid(&mut self, e: &mut Expr) {
match e {
Expr::Bin(BinExpr { left, right, .. }) => {
self.remove_invalid(left);
self.remove_invalid(right);
if left.is_invalid() {
*e = *right.take();
self.remove_invalid(e);
return;
} else if right.is_invalid() {
*e = *left.take();
self.remove_invalid(e);
return;
}
}
_ => {}
}
}
/// Returns [None] if expression is side-effect-free.
/// If an expression has a side effect, only side effects are returned.
fn ignore_return_value(&mut self, e: &mut Expr) -> Option<Expr> {
match e {
Expr::Ident(..) | Expr::This(_) | Expr::Invalid(_) | Expr::Lit(..) => {
log::trace!("Dropping unused expr");
log::trace!("ignore_return_value: Dropping unused expr");
self.changed = true;
return None;
}
// Function expression cannot have a side effect.
Expr::Fn(_) => {
log::trace!("Dropping unused fn expr as it does not have any side effect");
log::trace!(
"ignore_return_value: Dropping unused fn expr as it does not have any side \
effect"
);
self.changed = true;
return None;
}
@ -606,10 +654,20 @@ impl Optimizer<'_> {
},
ClassMember::ClassProp(ClassProp {
key,
computed: true,
computed,
is_static,
value,
..
}) => {
exprs.extend(self.ignore_return_value(key).map(Box::new));
if *computed {
exprs.extend(self.ignore_return_value(key).map(Box::new));
}
if *is_static {
if let Some(v) = value {
exprs.extend(self.ignore_return_value(v).map(Box::new));
}
}
}
_ => {}
@ -628,6 +686,24 @@ impl Optimizer<'_> {
Expr::Paren(e) => return self.ignore_return_value(&mut e.expr),
Expr::Bin(BinExpr {
op: op!("&&") | op!("||"),
left,
right,
..
}) => {
let new_r = self.ignore_return_value(right);
match new_r {
Some(r) => {
*right = Box::new(r);
}
None => return self.ignore_return_value(left),
}
return Some(e.take());
}
Expr::Unary(UnaryExpr {
op: op!("delete"), ..
}) => return Some(e.take()),
@ -641,8 +717,8 @@ impl Optimizer<'_> {
//
// Div is exlcuded because of zero.
// TOOD: Handle
Expr::Bin(BinExpr { op, .. })
if match op {
Expr::Bin(BinExpr {
op:
BinaryOp::EqEq
| BinaryOp::NotEq
| BinaryOp::EqEqEq
@ -664,12 +740,9 @@ impl Optimizer<'_> {
| BinaryOp::BitAnd
| BinaryOp::In
| BinaryOp::InstanceOf
| BinaryOp::Exp => false,
_ => true,
} =>
{
return Some(e.take())
}
| BinaryOp::Exp,
..
}) => return Some(e.take()),
// Pure calls can be removed
Expr::Call(CallExpr {
@ -1246,18 +1319,34 @@ impl Optimizer<'_> {
impl VisitMut for Optimizer<'_> {
noop_visit_mut_type!();
fn visit_mut_arrow_expr(&mut self, n: &mut ArrowExpr) {
let ctx = Ctx {
can_inline_arguments: true,
..self.ctx
};
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
}
fn visit_mut_assign_expr(&mut self, e: &mut AssignExpr) {
{
let ctx = Ctx {
is_lhs_of_assign: true,
in_lhs_of_assign: true,
is_exact_lhs_of_assign: true,
..self.ctx
};
e.left.visit_mut_with(&mut *self.with_ctx(ctx));
if is_left_access_to_arguments(&e.left) {
// self.ctx.can_inline_arguments = false;
}
}
e.right.visit_mut_with(self);
self.compress_bin_assignment_to_left(e);
self.compress_bin_assignment_to_right(e);
self.vars_accessible_without_side_effect.clear();
}
fn visit_mut_assign_pat_prop(&mut self, n: &mut AssignPatProp) {
@ -1300,8 +1389,9 @@ impl VisitMut for Optimizer<'_> {
fn visit_mut_block_stmt(&mut self, n: &mut BlockStmt) {
let ctx = Ctx {
stmt_lablled: false,
scope: n.span.ctxt,
top_level: false,
in_block: true,
scope: n.span.ctxt,
..self.ctx
};
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
@ -1325,6 +1415,15 @@ impl VisitMut for Optimizer<'_> {
e.callee.visit_mut_with(&mut *self.with_ctx(ctx));
}
match &e.callee {
ExprOrSuper::Super(_) => {}
ExprOrSuper::Expr(e) => {
if e.may_have_side_effects() {
self.vars_accessible_without_side_effect.clear();
}
}
}
{
let ctx = Ctx {
is_this_aware_callee: false,
@ -1337,6 +1436,8 @@ impl VisitMut for Optimizer<'_> {
self.optimize_symbol_call_unsafely(e);
self.inline_args_of_iife(e);
self.vars_accessible_without_side_effect.clear();
}
fn visit_mut_class(&mut self, n: &mut Class) {
@ -1386,9 +1487,7 @@ impl VisitMut for Optimizer<'_> {
DefaultDecl::Class(_) => {}
DefaultDecl::Fn(f) => {
if !self.options.keep_fargs && self.options.evaluate && self.options.unused {
f.function.params.iter_mut().for_each(|param| {
self.take_pat_if_unused(&mut param.pat, None);
})
self.drop_unused_params(&mut f.function.params);
}
}
DefaultDecl::TsInterfaceDecl(_) => {}
@ -1403,9 +1502,7 @@ impl VisitMut for Optimizer<'_> {
// I don't know why, but terser removes parameters from an exported function if
// `unused` is true, regardless of keep_fargs or others.
if self.options.unused {
f.function.params.iter_mut().for_each(|param| {
self.take_pat_if_unused(&mut param.pat, None);
})
self.drop_unused_params(&mut f.function.params);
}
}
_ => {}
@ -1433,16 +1530,7 @@ impl VisitMut for Optimizer<'_> {
};
e.visit_mut_children_with(&mut *self.with_ctx(ctx));
match e {
Expr::Bin(BinExpr { left, right, .. }) => {
if left.is_invalid() {
*e = *right.take();
} else if right.is_invalid() {
*e = *left.take();
}
}
_ => {}
}
self.remove_invalid(e);
self.concat_str(e);
@ -1479,7 +1567,7 @@ impl VisitMut for Optimizer<'_> {
self.optimize_nullish_coalescing(e);
self.compress_logical_exprs_as_bang_bang(e);
self.compress_logical_exprs_as_bang_bang(e, false);
self.compress_useless_cond_expr(e);
@ -1573,9 +1661,7 @@ impl VisitMut for Optimizer<'_> {
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
if !self.options.keep_fargs && self.options.evaluate && self.options.unused {
f.function.params.iter_mut().for_each(|param| {
self.take_pat_if_unused(&mut param.pat, None);
})
self.drop_unused_params(&mut f.function.params);
}
f.visit_mut_children_with(self);
@ -1626,6 +1712,7 @@ impl VisitMut for Optimizer<'_> {
}
fn visit_mut_function(&mut self, n: &mut Function) {
let old_vars = take(&mut self.vars_accessible_without_side_effect);
{
let ctx = Ctx {
stmt_lablled: false,
@ -1639,6 +1726,7 @@ impl VisitMut for Optimizer<'_> {
stmt_lablled: false,
in_fn_like: true,
scope: n.span.ctxt,
can_inline_arguments: true,
..self.ctx
};
let optimizer = &mut *self.with_ctx(ctx);
@ -1657,11 +1745,19 @@ impl VisitMut for Optimizer<'_> {
}
}
self.optimize_usage_of_arguments(n);
{
let ctx = Ctx {
can_inline_arguments: true,
..self.ctx
};
self.with_ctx(ctx).optimize_usage_of_arguments(n);
}
if let Some(body) = &mut n.body {
self.merge_if_returns(&mut body.stmts);
}
self.vars_accessible_without_side_effect = old_vars;
}
fn visit_mut_if_stmt(&mut self, n: &mut IfStmt) {
@ -1684,19 +1780,27 @@ impl VisitMut for Optimizer<'_> {
stmt_lablled: true,
..self.ctx
};
let old_label = self.label.take();
self.label = Some(n.label.to_id());
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
self.label = old_label;
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
{
let ctx = Ctx {
in_obj_of_non_computed_member: !n.computed,
is_exact_lhs_of_assign: false,
..self.ctx
};
n.obj.visit_mut_with(&mut *self.with_ctx(ctx));
}
if n.computed {
n.prop.visit_mut_with(self);
let ctx = Ctx {
is_exact_lhs_of_assign: false,
..self.ctx
};
n.prop.visit_mut_with(&mut *self.with_ctx(ctx));
}
self.handle_known_computed_member_expr(n);
@ -1715,6 +1819,20 @@ impl VisitMut for Optimizer<'_> {
});
}
fn visit_mut_new_expr(&mut self, n: &mut NewExpr) {
n.callee.visit_mut_with(self);
if n.callee.may_have_side_effects() {
self.vars_accessible_without_side_effect.clear();
}
{
n.args.visit_mut_with(self);
}
self.vars_accessible_without_side_effect.clear();
}
fn visit_mut_param(&mut self, n: &mut Param) {
n.visit_mut_children_with(self);
@ -1789,7 +1907,7 @@ impl VisitMut for Optimizer<'_> {
is_delete_arg: false,
is_exported: false,
is_update_arg: false,
is_lhs_of_assign: false,
in_lhs_of_assign: false,
in_obj_of_non_computed_member: false,
..self.ctx
};
@ -1992,6 +2110,8 @@ impl VisitMut for Optimizer<'_> {
};
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
self.vars_accessible_without_side_effect.clear();
}
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
@ -2039,9 +2159,13 @@ impl VisitMut for Optimizer<'_> {
..self.ctx
};
var.visit_mut_children_with(&mut *self.with_ctx(ctx));
var.name.visit_mut_with(&mut *self.with_ctx(ctx));
var.init.visit_mut_with(&mut *self.with_ctx(ctx));
}
self.remove_duplicate_names(var);
self.store_var_for_inining(var);
self.store_var_for_prop_hoisting(var);
@ -2144,3 +2268,29 @@ fn is_callee_this_aware(callee: &Expr) -> bool {
true
}
fn is_expr_access_to_arguments(l: &Expr) -> bool {
match l {
Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj),
..
}) => match &**obj {
Expr::Ident(Ident {
sym: js_word!("arguments"),
..
}) => true,
_ => false,
},
_ => false,
}
}
fn is_left_access_to_arguments(l: &PatOrExpr) -> bool {
match l {
PatOrExpr::Expr(e) => is_expr_access_to_arguments(&e),
PatOrExpr::Pat(pat) => match &**pat {
Pat::Expr(e) => is_expr_access_to_arguments(&e),
_ => false,
},
}
}

View File

@ -1,4 +1,5 @@
use super::Optimizer;
use crate::util::make_bool;
use crate::util::ValueExt;
use std::mem::swap;
use swc_atoms::js_word;
@ -140,18 +141,39 @@ impl Optimizer<'_> {
return;
}
if e.op == op!("===") || e.op == op!("==") || e.op == op!("!=") || e.op == op!("!==") {
if e.left.is_ident() && e.left.eq_ignore_span(&e.right) {
match e.op {
op!("===") | op!("==") | op!("!==") | op!("!=") => {
if e.left.is_ident() && e.left.eq_ignore_span(&e.right) {
let id: Ident = e.left.clone().ident().unwrap();
if let Some(t) = self.typeofs.get(&id.to_id()) {
match *t {
js_word!("object") | js_word!("function") => {
e.left = Box::new(make_bool(
e.span,
e.op == op!("===") || e.op == op!("=="),
));
e.right.take();
return;
}
_ => {}
}
}
}
}
_ => {}
}
if e.op == op!("===") || e.op == op!("!==") {
if (e.left.is_ident() || e.left.is_member()) && e.left.eq_ignore_span(&e.right) {
self.changed = true;
log::trace!("Reducing comparison of same variable ({})", e.op);
// TODO(kdy1): Create another method and assign to `Expr` instead of using a
// hack based on take.
e.left = Box::new(Expr::Lit(Lit::Bool(Bool {
span: e.span,
value: e.op == op!("===") || e.op == op!("==="),
})));
e.right.take();
e.op = if e.op == op!("===") {
op!("==")
} else {
op!("!=")
};
return;
}
}
@ -225,8 +247,12 @@ impl Optimizer<'_> {
| Expr::Bin(BinExpr { op: op!("<"), .. })
| Expr::Bin(BinExpr { op: op!(">="), .. })
| Expr::Bin(BinExpr { op: op!(">"), .. }) => {
log::trace!("Optimizing: `!!expr` => `expr`");
*e = *arg.take();
if let Known(Type::Bool) = arg.get_type() {
self.changed = true;
log::trace!("Optimizing: `!!expr` => `expr`");
*e = *arg.take();
}
return;
}
@ -408,8 +434,32 @@ impl Optimizer<'_> {
e.right = left.take();
}
/// Swap lhs and rhs in certain conditions.
pub(super) fn swap_bin_operands(&mut self, expr: &mut Expr) {
fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool {
match (l, r) {
(Expr::Ident(l), Expr::Ident(r)) => {
self.options.comparisons && (is_for_rel || l.sym > r.sym)
}
(Expr::Ident(..), Expr::Lit(..))
| (
Expr::Ident(..),
Expr::Unary(UnaryExpr {
op: op!("void"), ..
}),
)
| (
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!("===")
@ -424,40 +474,40 @@ impl Optimizer<'_> {
}
}
fn optimize(op: BinaryOp, left: &mut Expr, right: &mut Expr) -> bool {
if !is_supported(op) {
return false;
}
match (&*left, &*right) {
(Expr::Ident(..), Expr::Lit(..))
| (
Expr::Ident(..),
Expr::Unary(UnaryExpr {
op: op!("void"), ..
}),
)
| (
Expr::This(..),
Expr::Unary(UnaryExpr {
op: op!("void"), ..
}),
)
| (Expr::Unary(..), Expr::Lit(..))
| (Expr::Tpl(..), Expr::Lit(..)) => {
log::trace!("Swapping operands of binary exprssion");
swap(left, right);
return true;
}
_ => {}
}
false
if !is_supported(op) {
return false;
}
if self.can_swap_bin_operands(&left, &right, false) {
log::trace!("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::trace!("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 optimize(bin.op, &mut bin.left, &mut bin.right) {
if self.try_swap_bin(bin.op, &mut bin.left, &mut bin.right) {
self.changed = true;
}
}
@ -467,11 +517,14 @@ impl Optimizer<'_> {
/// Remove meaningless literals in a binary expressions.
///
/// # Parameters
///
/// - `in_bool_ctx`: True for expressions casted to bool.
///
/// # Examples
///
/// - `x() && true` => `!!x()`
pub(super) fn compress_logical_exprs_as_bang_bang(&mut self, e: &mut Expr) {
pub(super) fn compress_logical_exprs_as_bang_bang(&mut self, e: &mut Expr, in_bool_ctx: bool) {
if !self.options.conditionals && !self.options.reduce_vars {
return;
}
@ -482,27 +535,65 @@ impl Optimizer<'_> {
};
match bin.op {
op!("&&") => {
let rt = bin.right.get_type();
match rt {
Known(Type::Bool) => {}
_ => return,
}
op!("&&") | op!("||") => {
self.compress_logical_exprs_as_bang_bang(&mut bin.left, true);
self.compress_logical_exprs_as_bang_bang(&mut bin.right, true);
}
_ => {}
}
let lt = bin.left.get_type();
if !in_bool_ctx {
match lt {
// Don't change type
Known(Type::Bool) => {}
_ => return,
}
}
let rt = bin.right.get_type();
match rt {
Known(Type::Bool) => {}
_ => return,
}
match bin.op {
op!("&&") => {
let rb = bin.right.as_pure_bool();
let rb = match rb {
Value::Known(v) => v,
_ => return,
};
log::trace!("Optimizing: e && true => !!e");
if rb {
self.changed = true;
log::trace!("Optimizing: e && true => !!e");
self.negate_twice(&mut bin.left);
*e = *bin.left.take();
} else {
self.changed = true;
log::trace!("Optimizing: e && false => e");
*e = *bin.left.take();
}
}
op!("||") => {
let rb = bin.right.as_pure_bool();
let rb = match rb {
Value::Known(v) => v,
_ => return,
};
if !rb {
self.changed = true;
log::trace!("Optimizing: e || false => !!e");
self.negate_twice(&mut bin.left);
*e = *bin.left.take();
}
}
op!("||") => {}
_ => {}
}
}

View File

@ -1,3 +1,4 @@
use crate::compress::optimize::util::class_has_side_effect;
use crate::option::PureGetterOption;
use super::Optimizer;
@ -5,6 +6,7 @@ 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::contains_ident_ref;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
@ -94,10 +96,21 @@ impl Optimizer<'_> {
return;
}
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
return;
// Top-level
match self.ctx.var_kind {
Some(VarDeclKind::Var) => {
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
return;
}
}
Some(VarDeclKind::Let) | Some(VarDeclKind::Const) => {
if !self.options.top_level() && self.ctx.is_top_level_for_block_level_vars() {
return;
}
}
None => {}
}
if let Some(scope) = self
@ -117,6 +130,16 @@ impl Optimizer<'_> {
self.take_pat_if_unused(name, init);
}
pub(super) fn drop_unused_params(&mut self, params: &mut Vec<Param>) {
for param in params.iter_mut().rev() {
self.take_pat_if_unused(&mut param.pat, None);
if !param.pat.is_invalid() {
return;
}
}
}
pub(super) fn take_pat_if_unused(&mut self, name: &mut Pat, mut init: Option<&mut Expr>) {
let had_value = init.is_some();
let can_drop_children = had_value;
@ -237,7 +260,7 @@ impl Optimizer<'_> {
return;
}
if !self.options.top_level() && (self.ctx.top_level || !self.ctx.in_fn_like) {
if !self.options.top_level() && self.ctx.is_top_level_for_block_level_vars() {
return;
}
@ -255,6 +278,15 @@ impl Optimizer<'_> {
}
}
match decl {
Decl::Class(c) => {
if class_has_side_effect(&c.class) {
return;
}
}
_ => {}
}
match decl {
Decl::Class(ClassDecl { ident, .. }) | Decl::Fn(FnDecl { ident, .. }) => {
// We should skip if the name of decl is arguments.
@ -379,6 +411,34 @@ impl Optimizer<'_> {
}
}
}
/// `var Parser = function Parser() {};` => `var Parser = function () {}`
pub(super) fn remove_duplicate_names(&mut self, v: &mut VarDeclarator) {
if !self.options.unused {
return;
}
match v.init.as_deref_mut() {
Some(Expr::Fn(f)) => {
if f.ident.is_none() {
return;
}
if contains_ident_ref(&f.function.body, f.ident.as_ref().unwrap()) {
return;
}
self.changed = true;
log::trace!(
"unused: Removing the name of a function expression because it's not used by \
it'"
);
f.ident = None;
}
_ => {}
}
}
}
#[derive(Debug, Default)]

View File

@ -9,6 +9,7 @@ use swc_common::Mark;
use swc_common::Span;
use swc_ecma_ast::*;
use swc_ecma_utils::prop_name_eq;
use swc_ecma_utils::ExprExt;
impl<'b> Optimizer<'b> {
pub(super) fn access_property<'e>(
@ -179,3 +180,56 @@ impl Drop for WithCtx<'_, '_> {
self.reducer.ctx = self.orig_ctx;
}
}
pub(crate) fn class_has_side_effect(c: &Class) -> bool {
if let Some(e) = &c.super_class {
if e.may_have_side_effects() {
return true;
}
}
for m in &c.body {
match m {
ClassMember::Method(p) => {
if let PropName::Computed(key) = &p.key {
if key.expr.may_have_side_effects() {
return true;
}
}
}
ClassMember::ClassProp(p) => {
if p.computed {
if p.key.may_have_side_effects() {
return true;
}
}
if let Some(v) = &p.value {
if v.may_have_side_effects() {
return true;
}
}
}
ClassMember::PrivateProp(p) => {
if let Some(v) = &p.value {
if v.may_have_side_effects() {
return true;
}
}
}
_ => {}
}
}
false
}
pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool {
match e {
Expr::Lit(..) => return false,
Expr::Unary(..) => return false,
_ => true,
}
}

View File

@ -20,9 +20,10 @@ use crate::pass::compute_char_freq::compute_char_freq;
use crate::pass::expand_names::name_expander;
use crate::pass::global_defs;
use crate::pass::hygiene::hygiene_optimizer;
pub use crate::pass::hygiene::optimize_hygiene;
use crate::pass::mangle_names::name_mangler;
use crate::pass::mangle_props::mangle_properties;
use crate::pass::remove_parens::remove_parens;
use crate::pass::single::single_pass_optimizer;
use analyzer::analyze;
use swc_common::comments::Comments;
use swc_ecma_ast::Module;
@ -47,8 +48,6 @@ pub fn optimize(
options: &MinifyOptions,
extra: &ExtraOptions,
) -> Module {
m.visit_mut_with(&mut remove_parens());
if let Some(defs) = options.compress.as_ref().map(|c| &c.global_defs) {
// Apply global defs.
//
@ -62,6 +61,10 @@ pub fn optimize(
}
}
m.visit_mut_with(&mut single_pass_optimizer(
options.compress.clone().unwrap_or_default(),
));
m.visit_mut_with(&mut unique_marker());
if options.wrap {
@ -124,7 +127,7 @@ pub fn optimize(
{
let data = analyze(&m);
m.visit_mut_with(&mut hygiene_optimizer(data));
m.visit_mut_with(&mut hygiene_optimizer(data, extra.top_level_mark));
}
if let Some(ref mut t) = timings {

View File

@ -0,0 +1,121 @@
use crate::{
analyzer::{ProgramData, ScopeData},
util::has_mark,
};
use fxhash::FxHashSet;
use swc_common::{Mark, SyntaxContext};
use swc_ecma_ast::*;
use swc_ecma_utils::{ident::IdentLike, Id};
use swc_ecma_visit::{noop_visit_type, Node, Visit, VisitWith};
pub(super) struct HygieneAnalyzer<'a> {
pub data: &'a ProgramData,
pub hygiene: HygieneData,
pub top_level_mark: Mark,
pub cur_scope: Option<SyntaxContext>,
}
#[derive(Debug, Default)]
pub(super) struct HygieneData {
/// Has higher precedence over `modified`.
pub preserved: FxHashSet<Id>,
pub modified: FxHashSet<Id>,
}
impl HygieneAnalyzer<'_> {
fn scope(&self) -> &ScopeData {
match self.cur_scope {
Some(v) => self.data.scopes.get(&v).expect("failed to get scope"),
None => &self.data.top,
}
}
fn with_scope<F, Ret>(&mut self, scope_ctxt: SyntaxContext, op: F) -> Ret
where
F: FnOnce(&mut HygieneAnalyzer) -> Ret,
{
let old = self.cur_scope;
self.cur_scope = Some(scope_ctxt);
let ret = op(self);
self.cur_scope = old;
ret
}
/// Registers a binding ident. This is treated as [PatMode::OtherDecl].
///
/// If it conflicts
#[allow(unused)]
fn register_binding_ident(&mut self, i: &mut Ident) {}
}
impl Visit for HygieneAnalyzer<'_> {
noop_visit_type!();
fn visit_function(&mut self, n: &Function, _: &dyn Node) {
self.with_scope(n.span.ctxt, |v| {
n.visit_children_with(v);
});
}
fn visit_ident(&mut self, i: &Ident, _: &dyn Node) {
log::trace!("hygiene: Handling ({}{:?})", i.sym, i.span.ctxt);
if i.span.ctxt == SyntaxContext::empty() {
return;
}
if has_mark(i.span, self.top_level_mark) {
return;
}
let info = self.data.vars.get(&i.to_id());
// Ignore labels.
let info = match info {
Some(v) => v,
None => {
log::trace!("hygiene: No such var: {}{:?}", i.sym, i.span.ctxt);
return;
}
};
// If multiple variables with same name is declared, skip it.
if let Some(decls) = self.scope().declared_symbols.get(&i.sym) {
if decls.len() >= 2 {
self.hygiene.preserved.insert(i.to_id());
log::trace!(
"hygiene: Preserving hygiene of {}{:?} because it's declared multiple times",
i.sym,
i.span.ctxt
);
return;
}
}
if info.is_fn_local {
log::trace!(
"hygiene: Optimization candidate: {}{:?}",
i.sym,
i.span.ctxt
);
self.hygiene.modified.insert(i.to_id());
} else {
log::trace!(
"hygiene: Preserving {}{:?} as it is not fn-local",
i.sym,
i.span.ctxt
);
}
}
fn visit_member_expr(&mut self, n: &MemberExpr, _: &dyn Node) {
n.obj.visit_with(n, self);
if n.computed {
n.prop.visit_with(n, self);
}
}
}

View File

@ -1,53 +1,63 @@
use std::time::Instant;
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use swc_common::Span;
use crate::pass::hygiene::analyzer::HygieneAnalyzer;
use crate::pass::hygiene::analyzer::HygieneData;
use swc_common::Mark;
use swc_common::SyntaxContext;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::VisitWith;
mod analyzer;
pub fn optimize_hygiene(m: &mut Module, top_level_mark: Mark) {
let data = analyze(&*m);
m.visit_mut_with(&mut hygiene_optimizer(data, top_level_mark))
}
/// Create a hygiene optimizer.
///
/// Hygiene optimizer removes span hygiene without renaming if it's ok to do so.
pub(crate) fn hygiene_optimizer(data: ProgramData) -> impl 'static + VisitMut {
Optimizer { data }
pub(crate) fn hygiene_optimizer(
data: ProgramData,
top_level_mark: Mark,
) -> impl 'static + VisitMut {
Optimizer {
data,
hygiene: Default::default(),
top_level_mark,
}
}
struct Optimizer {
data: ProgramData,
hygiene: HygieneData,
top_level_mark: Mark,
}
impl Optimizer {
/// Registers a binding ident. This is treated as [PatMode::OtherDecl].
///
/// If it conflicts
#[allow(unused)]
fn register_binding_ident(&mut self, i: &mut Ident) {}
}
impl Optimizer {}
impl VisitMut for Optimizer {
noop_visit_mut_type!();
fn visit_mut_span(&mut self, span: &mut Span) {
span.ctxt = SyntaxContext::empty();
}
fn visit_mut_ident(&mut self, i: &mut Ident) {
if i.span.ctxt == SyntaxContext::empty() {
return;
}
let info = self.data.vars.get(&i.to_id());
// Ignore labels.
let info = match info {
Some(v) => v,
None => return,
};
if info.is_fn_local {
i.span.ctxt = SyntaxContext::empty();
if self.hygiene.preserved.contains(&i.to_id())
|| !self.hygiene.modified.contains(&i.to_id())
{
return;
}
i.span.ctxt = SyntaxContext::empty().apply_mark(self.top_level_mark);
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
@ -57,4 +67,27 @@ impl VisitMut for Optimizer {
n.prop.visit_mut_with(self);
}
}
fn visit_mut_module(&mut self, n: &mut Module) {
log::debug!("hygiene: Analyzing span hygiene");
let start = Instant::now();
let mut analyzer = HygieneAnalyzer {
data: &self.data,
hygiene: Default::default(),
top_level_mark: self.top_level_mark,
cur_scope: None,
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut analyzer);
self.hygiene = analyzer.hygiene;
let end = Instant::now();
log::debug!("hygiene: Span hygiene analysis took {:?}", end - start);
let start = end;
log::debug!("hygiene: Optimizing span hygiene");
n.visit_mut_children_with(self);
let end = Instant::now();
log::debug!("hygiene: Span hygiene optimiation took {:?}", end - start);
}
}

View File

@ -4,4 +4,4 @@ pub mod global_defs;
pub mod hygiene;
pub mod mangle_names;
pub mod mangle_props;
pub mod remove_parens;
pub mod single;

View File

@ -1,26 +0,0 @@
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
pub fn remove_parens() -> impl 'static + VisitMut {
RemoveParens
}
struct RemoveParens;
impl VisitMut for RemoveParens {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Paren(p) => {
*e = *p.expr.take();
}
_ => {}
}
}
}

View File

@ -0,0 +1,153 @@
use crate::{
analyzer::{analyze, ProgramData},
option::CompressOptions,
};
use fxhash::FxHashMap;
use swc_atoms::js_word;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::{ident::IdentLike, Id};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
///
/// - Remove parens.
pub fn single_pass_optimizer(options: CompressOptions) -> impl VisitMut {
SinglePassOptimizer {
options,
data: Default::default(),
fn_decl_count: Default::default(),
ctx: Default::default(),
}
}
#[derive(Debug, Default)]
struct SinglePassOptimizer {
options: CompressOptions,
data: ProgramData,
fn_decl_count: FxHashMap<Id, usize>,
ctx: Ctx,
}
#[derive(Debug, Default, Clone, Copy)]
struct Ctx {
in_var_pat: bool,
}
impl VisitMut for SinglePassOptimizer {
noop_visit_mut_type!();
fn visit_mut_decl(&mut self, n: &mut Decl) {
n.visit_mut_children_with(self);
match n {
Decl::Fn(FnDecl { ident, .. }) => {
if ident.sym == js_word!("") {
n.take();
}
}
_ => {}
}
}
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Paren(p) => {
*e = *p.expr.take();
}
_ => {}
}
}
fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) {
n.visit_mut_children_with(self);
if self.options.dead_code || self.options.unused {
if let Some(usage) = self.data.vars.get(&n.ident.to_id()) {
// Remove if variable with same name exists.
if usage.var_kind.is_some() && usage.var_initialized {
n.ident.take();
return;
}
if usage.assign_count > 1 {
let v = self.fn_decl_count.entry(n.ident.to_id()).or_default();
*v += 1;
if *v == usage.assign_count {
n.ident.take();
}
}
}
}
}
fn visit_mut_module(&mut self, n: &mut Module) {
let data = analyze(&*n);
self.data = data;
n.visit_mut_children_with(self);
}
fn visit_mut_module_item(&mut self, n: &mut ModuleItem) {
n.visit_mut_children_with(self);
match n {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Var(var),
..
})) if var.decls.is_empty() => {
n.take();
}
_ => {}
}
}
fn visit_mut_module_items(&mut self, n: &mut Vec<ModuleItem>) {
n.visit_mut_children_with(self);
n.retain(|s| match s {
ModuleItem::Stmt(Stmt::Empty(..)) => false,
_ => true,
});
}
fn visit_mut_script(&mut self, n: &mut Script) {
let data = analyze(&*n);
self.data = data;
n.visit_mut_children_with(self);
}
fn visit_mut_stmt(&mut self, n: &mut Stmt) {
n.visit_mut_children_with(self);
match n {
Stmt::Decl(Decl::Var(var)) if var.decls.is_empty() => {
n.take();
}
_ => {}
}
}
fn visit_mut_stmts(&mut self, n: &mut Vec<Stmt>) {
n.visit_mut_children_with(self);
n.retain(|s| match s {
Stmt::Empty(..) => false,
_ => true,
});
}
fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
let old = self.ctx;
self.ctx.in_var_pat = true;
n.name.visit_mut_with(self);
self.ctx.in_var_pat = false;
n.init.visit_mut_with(self);
self.ctx = old;
}
}

View File

@ -3,6 +3,7 @@ use swc_common::pass::CompilerPass;
use swc_common::pass::Repeated;
use swc_common::Mark;
use swc_common::Span;
use swc_common::SyntaxContext;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
@ -304,3 +305,62 @@ where
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.ids
}
pub(crate) fn ctxt_has_mark(mut ctxt: SyntaxContext, mark: Mark) -> bool {
debug_assert_ne!(mark, Mark::root());
loop {
if ctxt == SyntaxContext::empty() {
return false;
}
let m = ctxt.remove_mark();
if m == mark {
return true;
}
if m == Mark::root() {
return false;
}
}
}
pub(crate) fn has_mark(span: Span, mark: Mark) -> bool {
ctxt_has_mark(span.ctxt, mark)
}
pub(crate) fn can_end_conditionally(s: &Stmt) -> bool {
///
///`ignore_always`: If true, [Stmt::Return] will be ignored.
fn can_end(s: &Stmt, ignore_always: bool) -> bool {
match s {
Stmt::If(s) => {
can_end(&s.cons, false)
|| s.alt
.as_deref()
.map(|s| can_end(s, false))
.unwrap_or_default()
}
Stmt::Switch(s) => s
.cases
.iter()
.any(|case| case.cons.iter().any(|s| can_end(&s, false))),
Stmt::DoWhile(s) => can_end(&s.body, false),
Stmt::While(s) => can_end(&s.body, false),
Stmt::For(s) => can_end(&s.body, false),
Stmt::ForOf(s) => can_end(&s.body, false),
Stmt::ForIn(s) => can_end(&s.body, false),
Stmt::Return(..) | Stmt::Break(..) | Stmt::Continue(..) => !ignore_always,
_ => false,
}
}
can_end(s, true)
}

View File

@ -5,10 +5,7 @@ use anyhow::Error;
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::env;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fs::read_to_string;
use std::panic::catch_unwind;
use std::path::Path;
@ -17,6 +14,7 @@ use std::process::Command;
use swc_common::comments::SingleThreadedComments;
use swc_common::errors::Handler;
use swc_common::sync::Lrc;
use swc_common::EqIgnoreSpan;
use swc_common::FileName;
use swc_common::Mark;
use swc_common::SourceMap;
@ -35,8 +33,10 @@ use swc_ecma_parser::Parser;
use swc_ecma_transforms::fixer;
use swc_ecma_transforms::hygiene;
use swc_ecma_transforms::resolver_with_mark;
use swc_ecma_utils::drop_span;
use swc_ecma_visit::FoldWith;
use testing::assert_eq;
use testing::DebugUsingDisplay;
use testing::NormalizedOutput;
fn load_txt(filename: &str) -> Vec<String> {
@ -110,6 +110,26 @@ fn run(
eprintln!("---- {} -----\n{}", Color::Green.paint("Input"), fm.src);
if env::var("SWC_RUN").unwrap_or_default() == "1" {
let stdout = stdout_of(&fm.src);
match stdout {
Ok(stdout) => {
eprintln!(
"---- {} -----\n{}",
Color::Green.paint("Stdout (expected)"),
stdout
);
}
Err(err) => {
eprintln!(
"---- {} -----\n{:?}",
Color::Green.paint("Error (of orignal source code)"),
err
);
}
}
}
let top_level_mark = Mark::fresh(Mark::root());
let lexer = Lexer::new(
@ -179,6 +199,35 @@ fn stdout_of(code: &str) -> Result<String, Error> {
Ok(String::from_utf8_lossy(&actual_output.stdout).to_string())
}
#[testing::fixture("compress/fixture/**/input.js")]
fn base_fixture(input: PathBuf) {
let dir = input.parent().unwrap();
let config = dir.join("config.json");
let config = read_to_string(&config).expect("failed to read config.json");
eprintln!("---- {} -----\n{}", Color::Green.paint("Config"), config);
testing::run_test2(false, |cm, handler| {
let output = run(cm.clone(), &handler, &input, &config, None);
let output_module = match output {
Some(v) => v,
None => return Ok(()),
};
let output = print(cm.clone(), &[output_module.clone()]);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
println!("{}", input.display());
NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js"))
.unwrap();
Ok(())
})
.unwrap()
}
/// Tests used to prevent regressions.
#[testing::fixture("compress/exec/**/input.js")]
fn base_exec(input: PathBuf) {
@ -206,7 +255,10 @@ fn base_exec(input: PathBuf) {
let actual_output = stdout_of(&output).expect("failed to execute the optimized code");
assert_ne!(actual_output, "");
assert_eq!(actual_output, expected_output);
assert_eq!(
DebugUsingDisplay(&actual_output),
DebugUsingDisplay(&*expected_output)
);
Ok(())
})
@ -240,11 +292,12 @@ fn fixture(input: PathBuf) {
mangle.map(|s| serde_json::from_str(&s).expect("failed to deserialize mangle.json"));
let output = run(cm.clone(), &handler, &input, &config, mangle);
let output = match output {
let output_module = match output {
Some(v) => v,
None => return Ok(()),
};
let output = print(cm.clone(), &[output]);
let output = print(cm.clone(), &[output_module.clone()]);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
@ -262,6 +315,14 @@ fn fixture(input: PathBuf) {
err.into_diagnostic(&handler).emit();
})?;
let mut expected = expected.fold_with(&mut fixer(None));
expected = drop_span(expected);
if output_module.eq_ignore_span(&expected)
|| drop_span(output_module.clone()) == expected
{
return Ok(());
}
expected.body.retain(|s| match s {
ModuleItem::Stmt(Stmt::Empty(..)) => false,
_ => true,
@ -307,7 +368,9 @@ fn fixture(input: PathBuf) {
});
}
assert_eq!(DebugUsingDisplay(&output), DebugUsingDisplay(&expected));
let output_str = print(cm.clone(), &[drop_span(output_module.clone())]);
assert_eq!(DebugUsingDisplay(&output_str), DebugUsingDisplay(&expected));
Ok(())
})
@ -332,12 +395,3 @@ fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N]) -> String {
String::from_utf8(buf).unwrap()
}
#[derive(PartialEq, Eq)]
struct DebugUsingDisplay<'a>(&'a str);
impl<'a> Debug for DebugUsingDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log(x() && true && y());

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log(y() && true && x());

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log(x() || false || y());

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log(y() || false || x());

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log((x() || false) && y());

View File

@ -0,0 +1,6 @@
{
"booleans": true,
"conditionals": true,
"evaluate": true,
"side_effects": true
}

View File

@ -0,0 +1,6 @@
function x() {
}
function y() {
return "foo";
}
console.log((y() || false) && x());

View File

@ -0,0 +1,8 @@
{
"evaluate": true,
"inline": true,
"passes": 3,
"reduce_vars": true,
"side_effects": true,
"unused": true
}

View File

@ -0,0 +1,4 @@
console.log(function c() {
c = 6;
return c;
}())

View File

@ -0,0 +1,4 @@
{
"arguments": true,
"defaults": true
}

View File

@ -0,0 +1,3 @@
(function (a, a) {
console.log((a = "foo"), arguments[0]);
})("baz", "Bar");

View File

@ -0,0 +1,4 @@
{
"defaults": true,
"toplevel": true
}

View File

@ -0,0 +1,4 @@
function shouldBePure() {
return arguments.length;
}
console.log(shouldBePure())

View File

@ -0,0 +1,3 @@
{
"arguments": true
}

View File

@ -0,0 +1,9 @@
(function (a, b) {
var c = arguments[0];
var d = arguments[1];
var a = "foo";
b++;
arguments[0] = "moo";
arguments[1] *= 2;
console.log(a, b, c, d, arguments[0], arguments[1]);
})("bar", 42);

View File

@ -0,0 +1,4 @@
{
"arguments": true,
"reduce_vars": true
}

View File

@ -0,0 +1,10 @@
"use strict";
(function (a, b) {
var c = arguments[0];
var d = arguments[1];
var a = "foo";
b++;
arguments[0] = "moo";
arguments[1] *= 2;
console.log(a, b, c, d, arguments[0], arguments[1]);
})("bar", 42);

View File

@ -0,0 +1,12 @@
{
"collapse_vars": true,
"conditionals": true,
"evaluate": true,
"inline": true,
"negate_iife": true,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"toplevel": true,
"unused": true
}

View File

@ -0,0 +1,23 @@
(async function () {
return await 3;
})();
(async function (x) {
await console.log(x);
})(4);
function invoke(x, y) {
return x(y);
}
invoke(async function () {
return await 1;
});
invoke(async function (x) {
await console.log(x);
}, 2);
function top() {
console.log("top");
}
top();
async function async_top() {
console.log("async_top");
}
async_top();

View File

@ -0,0 +1,5 @@
{
"toplevel": true,
"reduce_vars": true,
"unused": true
}

View File

@ -0,0 +1,12 @@
let x = 0;
class NoProps {}
class WithProps {
prop = (x = x === 1 ? "PASS" : "FAIL");
}
class WithStaticProps {
static prop = (x = x === 0 ? 1 : "FAIL");
}
new NoProps();
new WithProps();
new WithStaticProps();
console.log(x);

View File

@ -0,0 +1,4 @@
{
"dead_code": true,
"loops": true
}

View File

@ -0,0 +1,4 @@
const a = 9,
b = 0;
for (const a = 1; a < 3; ++b) break;
console.log(a, b);

View File

@ -0,0 +1,4 @@
{
"dead_code": true,
"loops": true
}

View File

@ -0,0 +1,4 @@
let a = 9,
b = 0;
for (const a = 1; a < 3; ++b) break;
console.log(a, b);

View File

@ -0,0 +1,4 @@
{
"dead_code": true,
"loops": true
}

View File

@ -0,0 +1,4 @@
var a = 9,
b = 0;
for (const a = 1; a < 3; ++b) break;
console.log(a, b);

View File

@ -0,0 +1,5 @@
{
"reduce_funcs": true,
"reduce_vars": true,
"unused": true
}

View File

@ -0,0 +1,6 @@
(function () {
var x = function f() {
return f;
};
console.log(x() === eval("x"));
})();

View File

@ -0,0 +1,7 @@
{
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true,
"unsafe": true,
"unused": true
}

View File

@ -0,0 +1,65 @@
console.log(
(function () {
var o = { p: 1 };
o.p++;
console.log(o.p);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 2 };
--o.p;
console.log(o.p);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 3 };
o.p += "";
console.log(o.p);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 4 };
o = {};
console.log(o.p);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 5 };
o.p = -9;
console.log(o.p);
return o.p;
})()
);
function inc() {
this.p++;
}
console.log(
(function () {
var o = { p: 6 };
inc.call(o);
console.log(o.p);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 7 };
console.log([o][0].p++);
return o.p;
})()
);
console.log(
(function () {
var o = { p: 8 };
console.log({ q: o }.q.p++);
return o.p;
})()
);

View File

@ -0,0 +1,9 @@
{
"evaluate": true,
"inline": true,
"passes": 3,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"unused": true
}

View File

@ -0,0 +1,8 @@
function f() {
return g(2);
function g(b) {
return b;
}
}
console.log(f())

View File

@ -0,0 +1,6 @@
{
"inline": true,
"reduce_funcs": true,
"reduce_vars": true,
"unused": true
}

View File

@ -0,0 +1,14 @@
function f() {
function g() {
return 1;
}
function h() {
return 2;
}
g = function () {
return 3;
};
return g() + h();
}
console.log(f())

View File

@ -0,0 +1,10 @@
{
"evaluate": true,
"inline": true,
"passes": 2,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"toplevel": true,
"unused": true
}

View File

@ -0,0 +1,6 @@
var a = 42;
!(function (a) {
console.log(a());
})(function () {
return a;
});

View File

@ -0,0 +1,10 @@
{
"evaluate": true,
"inline": true,
"passes": 2,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"toplevel": true,
"unused": true
}

View File

@ -0,0 +1,6 @@
var a = 42;
!(function (a) {
console.log(a());
})(function (a) {
return a;
});

View File

@ -0,0 +1,6 @@
{
"inline": true,
"reduce_funcs": true,
"reduce_vars": true,
"unused": true
}

View File

@ -0,0 +1,18 @@
function f(a) {
function a() {
return 1;
}
function b() {
return 2;
}
function c() {
return 3;
}
b.inject = [];
c = function () {
return 4;
};
return a() + b() + c();
}
console.log(f(1423796))

View File

@ -0,0 +1,6 @@
{
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true,
"unused": true
}

View File

@ -0,0 +1,5 @@
!(function () {
var a = 1;
for (var b = 1; --b; ) var a = 2;
console.log(a);
})();

View File

@ -0,0 +1,5 @@
{
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true
}

View File

@ -0,0 +1,10 @@
function f(a) {
l: {
if (a) break l;
var t = 1;
}
console.log(t);
}
f(123123)
f(0)

View File

@ -0,0 +1,8 @@
{
"evaluate": true,
"inline": true,
"passes": 3,
"reduce_vars": true,
"side_effects": true,
"unused": true
}

View File

@ -0,0 +1,39 @@
console.log(
(function () {
var a = 1;
return a;
})(),
(function () {
var b;
b = 2;
return b;
})(),
(function () {
c = 3;
return c;
var c;
})(),
(function (c) {
c = 4;
return c;
})(),
(function (c) {
c = 5;
return c;
var c;
})(),
(function c() {
c = 6;
return typeof c;
})(),
(function c() {
c = 7;
return c;
var c;
})(),
(function () {
c = 8;
return c;
var c = "foo";
})()
);

View File

@ -0,0 +1,12 @@
{
"comparisons": true,
"conditionals": true,
"dead_code": true,
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"typeofs": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function f() {
switch (1) {
case 0:
var a = true;
break;
default:
if (typeof a === "undefined") console.log("PASS");
else console.log("FAIL");
}
})();

View File

@ -0,0 +1,11 @@
{
"conditionals": true,
"dead_code": true,
"evaluate": true,
"passes": 2,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function f() {
switch (1) {
case 0:
var a = true;
break;
default:
if (typeof a === "undefined") console.log("PASS");
else console.log("FAIL");
}
})();

View File

@ -0,0 +1,12 @@
{
"comparisons": true,
"conditionals": true,
"dead_code": true,
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"typeofs": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function f() {
switch (1) {
case 0:
var a = true;
break;
case 1:
if (typeof a === "undefined") console.log("PASS");
else console.log("FAIL");
}
})();

View File

@ -0,0 +1,11 @@
{
"conditionals": true,
"dead_code": true,
"evaluate": true,
"passes": 2,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function f() {
switch (1) {
case 0:
var a = true;
break;
case 1:
if (typeof a === "undefined") console.log("PASS");
else console.log("FAIL");
}
})();

View File

@ -0,0 +1,10 @@
{
"dead_code": true,
"evaluate": true,
"keep_fargs": false,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function (a) {
switch (1) {
case a:
console.log(a);
break;
default:
console.log(2);
break;
}
})(1);

View File

@ -0,0 +1,10 @@
{
"dead_code": true,
"evaluate": true,
"keep_fargs": false,
"reduce_funcs": true,
"reduce_vars": true,
"side_effects": true,
"switches": true,
"unused": true
}

View File

@ -0,0 +1,10 @@
(function (a) {
switch (1) {
case (a = 1):
console.log(a);
break;
default:
console.log(2);
break;
}
})(1);

View File

@ -0,0 +1,6 @@
{
"evaluate": true,
"reduce_funcs": true,
"reduce_vars": true,
"unsafe": true
}

View File

@ -0,0 +1,10 @@
function f(some) {
some.thing = false;
}
console.log(
(function () {
var some = { thing: true };
f(some);
return some.thing;
})()
);

View File

@ -0,0 +1,5 @@
{
"reduce_vars": true,
"toplevel": true,
"unused": true
}

Some files were not shown because too many files have changed in this diff Show More