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

swc_ecma_codegen:
 - Fix codegen of `U+0028` and `U+0029`. 

swc_ecma_minifier:
 - Implement MVP.

swc_ecma_transforms_base:
 - `fixer`: Handle seq/await in rhs of for-of.
This commit is contained in:
강동윤 2021-07-30 10:11:27 +09:00 committed by GitHub
parent d64aa6f80d
commit b02e189d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
366 changed files with 238188 additions and 1364 deletions

View File

@ -160,7 +160,7 @@ jobs:
# Source map format
- uses: actions/setup-node@v2
with:
node-version: "14"
node-version: "16"
# We explicitly do this to cache properly.
- name: Install Rust
@ -170,7 +170,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- uses: denoland/setup-deno@v1
with:

15
Cargo.lock generated
View File

@ -2430,7 +2430,7 @@ dependencies = [
[[package]]
name = "swc_ecma_codegen"
version = "0.64.0"
version = "0.64.2"
dependencies = [
"bitflags",
"num-bigint",
@ -2500,11 +2500,12 @@ dependencies = [
[[package]]
name = "swc_ecma_minifier"
version = "0.14.0"
version = "0.15.0"
dependencies = [
"ansi_term 0.12.1",
"anyhow",
"fxhash",
"indexmap",
"log",
"once_cell",
"pretty_assertions 0.6.1",
@ -2522,7 +2523,9 @@ dependencies = [
"swc_ecma_transforms_base",
"swc_ecma_utils",
"swc_ecma_visit",
"swc_node_base",
"testing",
"unicode-xid",
"walkdir",
]
@ -2605,7 +2608,7 @@ dependencies = [
[[package]]
name = "swc_ecma_transforms_base"
version = "0.24.0"
version = "0.24.1"
dependencies = [
"fxhash",
"once_cell",
@ -2788,7 +2791,7 @@ dependencies = [
[[package]]
name = "swc_ecma_transforms_typescript"
version = "0.30.0"
version = "0.30.1"
dependencies = [
"fxhash",
"serde",
@ -2835,7 +2838,7 @@ dependencies = [
[[package]]
name = "swc_ecmascript"
version = "0.48.0"
version = "0.49.0"
dependencies = [
"swc_ecma_ast",
"swc_ecma_codegen",
@ -2940,7 +2943,7 @@ dependencies = [
"env_logger 0.7.1",
"log",
"once_cell",
"pretty_assertions 0.6.1",
"pretty_assertions 0.7.2",
"regex",
"swc_common",
"testing_macros",

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.48.0"
version = "0.49.0"
[package.metadata.docs.rs]
all-features = true
@ -34,7 +34,7 @@ typescript = ["typescript-parser", "swc_ecma_transforms/typescript"]
swc_ecma_ast = {version = "0.49.0", path = "./ast"}
swc_ecma_codegen = {version = "0.64.0", path = "./codegen", optional = true}
swc_ecma_dep_graph = {version = "0.33.0", path = "./dep-graph", optional = true}
swc_ecma_minifier = {version = "0.14.0", path = "./minifier", optional = true}
swc_ecma_minifier = {version = "0.15.0", path = "./minifier", optional = true}
swc_ecma_parser = {version = "0.65.0", path = "./parser", optional = true, default-features = false}
swc_ecma_transforms = {version = "0.61.0", path = "./transforms", optional = true}
swc_ecma_utils = {version = "0.41.0", path = "./utils", optional = true}

View File

@ -7,7 +7,7 @@ include = ["Cargo.toml", "src/**/*.rs"]
license = "Apache-2.0/MIT"
name = "swc_ecma_codegen"
repository = "https://github.com/swc-project/swc.git"
version = "0.64.1"
version = "0.64.2"
[dependencies]
bitflags = "1"

View File

@ -2543,6 +2543,13 @@ fn escape_without_source(v: &str, target: JscTarget, single_quote: bool) -> Stri
let _ = write!(buf, "\\x{:x}", c as u8);
}
'\u{2028}' => {
buf.push_str("\\u2028");
}
'\u{2029}' => {
buf.push_str("\\u2029");
}
_ => {
buf.push(c);
}

View File

@ -560,7 +560,7 @@ fn test_escape_without_source() {
es2020("abcde", "abcde");
es2020(
"\x00\r\n\u{85}\u{2028}\u{2029};",
"\\x00\\r\\n\\x85\u{2028}\u{2029};",
"\\x00\\r\\n\\x85\\u2028\\u2029;",
);
es2020("\n", "\\n");

View File

@ -0,0 +1,47 @@
{
"parser": "babel-eslint",
"rules": {
"indent": [
"error",
4
],
"semi": [
"error",
"always"
],
"space-unary-ops": 2,
"no-undef": "off",
"consistent-return": "off",
"no-prototype-builtins": "off",
"no-cond-assign": "off",
"multiline-ternary": [
"error",
"always-multiline"
],
"object-property-newline": [
"error",
{
"allowAllPropertiesOnSameLine": false
}
],
"function-call-argument-newline": [
"error",
"always"
],
"object-curly-newline": [
"error",
"always"
],
"comma-dangle": [
"error",
"always"
],
"function-paren-newline": [
"error",
"always"
],
"no-trailing-spaces": [
"error"
]
}
}

2
ecmascript/minifier/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Used to see ideal output using terser
lab.js

View File

@ -7,13 +7,14 @@ 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.14.0"
version = "0.15.0"
[features]
debug = []
[dependencies]
fxhash = "0.2.1"
indexmap = "1.7.0"
log = "0.4"
once_cell = "1.5.2"
pretty_assertions = {version = "0.6.1", optional = true}
@ -31,10 +32,12 @@ swc_ecma_transforms = {version = "0.61.0", path = "../transforms/", features = [
swc_ecma_transforms_base = {version = "0.24.0", path = "../transforms/base"}
swc_ecma_utils = {version = "0.41.0", path = "../utils"}
swc_ecma_visit = {version = "0.35.0", path = "../visit"}
unicode-xid = "0.2.2"
[dev-dependencies]
ansi_term = "0.12.1"
anyhow = "1"
pretty_assertions = "0.6.1"
swc_node_base = {version = "0.2.0", path = "../../node/base"}
testing = {version = "0.12.0", path = "../../testing"}
walkdir = "2.3.1"

View File

@ -8,6 +8,26 @@ Currently name mangler is very simple. To focus on creating a MVP, I (kdy1) will
## Testing
### Real-library tests
You can use vscode to see diffs. Select a file from `tests/projects/output` and select the corresponding file from `tests/projects/refs`. Then you can right click to see an option to open diff.
#### Splitting a library test
Extracting only subset from a library test makes development much easier.
Split status:
- [ ] angular-1.2.5
- [x] backbone-1.1.0
- [ ] jquery-1.9.1
- [ ] jquery.mobile-1.4.2
- [ ] mootools-1.4.5
- [x] underscore-1.5.2
- [ ] yui-3.12.0
### Tests from terser
Tests ported from terser has
- input.js
@ -16,6 +36,6 @@ Tests ported from terser has
- output.js (if exists and can be modified if our one is better or due to lack of frequency-aware base54)
- output.terser.js (if exists)
## Copying tests
#### Copying tests
Replace the content of `terser/test/compress.js` with it of `scripts/compress.js` and run `npm run test:compress`

View File

@ -0,0 +1,20 @@
{
"name": "minifier",
"private": true,
"version": "1.0.0",
"description": "EcmaScript minifier for the swc project. This is basically a port of terser.",
"main": "lab.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.29.0"
}
}

View File

@ -6,10 +6,10 @@ set -eu
export SWC_RUN=0
cargo test --test compress --all-features \
| grep 'terser__compress' \
cargo test --test compress --all-features fixture_tests__terser__compress__ \
| grep 'fixture_tests__terser__compress__' \
| grep 'js .\.\. ok$' \
| sed -e 's!test fixture_terser__compress__!!' \
| sed -e 's!test fixture_tests__terser__compress__!!' \
| sed -e 's! ... ok!!' \
| sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -eux
# Prevent regression
./scripts/run.sh
export RUST_BACKTRACE=1
export RUST_LOG=swc_ecma_minifier=trace
touch tests/compress.rs
UPDATE=1 cargo test --test compress files__projects --all-features || true
# Make it easier to compare
prettier --write tests/projects/output
yarn run eslint --fix ./tests/projects/output/
ls -al ./tests/projects/output
ls -al ./tests/projects/refs

View File

@ -5,4 +5,4 @@ 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
cargo test --test compress ${1-base_exec} -q --all-features

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
#
# This scripts print the ideal output of lab.js
#
set -eux
terser --compress --format beautify -- lab.js
node lab.js

View File

@ -3,10 +3,10 @@ set -eu
export SWC_RUN=0
cargo test --test compress --all-features ${1-''} \
| grep 'terser__compress' \
cargo test --test compress --all-features ${1-'fixture_tests__terser__compress__'} \
| grep 'fixture_tests__terser__compress__' \
| grep 'js .\.\. FAILED$' \
| sed -e 's!test fixture_terser__compress__!!' \
| sed -e 's!test fixture_tests__terser__compress__!!' \
| sed -e 's! ... FAILED!!' \
| sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu
run() {
terser --compress -- ./tests/projects/files/$1 > ./tests/projects/refs/$1
}
mkdir -p ./tests/projects/refs
run angular-1.2.5.js
run backbone-1.1.0.js
run jquery-1.9.1.js
run jquery.mobile-1.4.2.js
run mootools-1.4.5.js
run underscore-1.5.2.js
run yui-3.12.0.js
run react-17.0.2.js
run react-dom-17.0.2.js
prettier --write tests/projects/refs/
yarn run eslint --fix ./tests/projects/refs/

View File

@ -13,6 +13,9 @@ set -eu
export RUST_LOG=swc_ecma_minifier=trace
# Run unit tests.
cargo test --all-features --lib
# To prevent regression, we run base test before real tests.
touch tests/compress.rs
UPDATE=1 ./scripts/base.sh base_fixture
@ -21,7 +24,7 @@ UPDATE=1 ./scripts/base.sh base_fixture
if [ -z "$@" ]; then
./scripts/sort.sh
SWC_RUN=0 GOLDEN_ONLY=1 cargo test --test compress --all-features
SWC_RUN=0 GOLDEN_ONLY=1 cargo test -q --test compress --all-features fixture_tests__terser__compress__
fi
SKIP_GOLDEN=1 cargo test --test compress --all-features $@

View File

@ -12,4 +12,4 @@ set -eu
echo '' > tests/ignored.txt
scripts/add-golden.sh
scripts/ignore.sh ''
scripts/ignore.sh 'fixture_tests__terser__compress__'

View File

@ -17,6 +17,9 @@ impl UsageAnalyzer {
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct Ctx {
pub var_decl_kind_of_pat: Option<VarDeclKind>,
pub in_var_decl_with_no_side_effect_for_member_access: bool,
pub in_pat_of_var_decl: bool,
pub in_pat_of_var_decl_with_init: bool,
pub in_pat_of_param: bool,
@ -38,6 +41,8 @@ pub(super) struct Ctx {
pub in_update_arg: bool,
pub in_assign_lhs: bool,
pub in_cond: bool,
pub inline_prevented: bool,
}
pub(super) struct WithCtx<'a> {

View File

@ -4,6 +4,7 @@ use crate::util::idents_used_by;
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use std::collections::hash_map::Entry;
use std::time::Instant;
use swc_atoms::JsWord;
use swc_common::SyntaxContext;
use swc_common::DUMMY_SP;
@ -24,6 +25,8 @@ pub(crate) fn analyze<N>(n: &N) -> ProgramData
where
N: VisitWith<UsageAnalyzer>,
{
let start_time = Instant::now();
let mut v = UsageAnalyzer {
data: Default::default(),
scope: Default::default(),
@ -33,11 +36,17 @@ where
let top_scope = v.scope;
v.data.top.merge(top_scope, false);
let end_time = Instant::now();
log::debug!("Scope analysis took {:?}", end_time - start_time);
v.data
}
#[derive(Debug, Default)]
pub(crate) struct VarUsageInfo {
pub inline_prevented: bool,
/// The number of reference to this identifier.
pub ref_count: usize,
@ -51,6 +60,8 @@ pub(crate) struct VarUsageInfo {
/// `true` if the enclosing function defines this variable as a parameter.
pub declared_as_fn_param: bool,
pub declared_as_fn_expr: bool,
pub assign_count: usize,
pub mutation_by_call_count: usize,
pub usage_count: usize,
@ -71,6 +82,8 @@ pub(crate) struct VarUsageInfo {
/// functions.
pub is_fn_local: bool,
used_by_nested_fn: bool,
pub used_in_loop: bool,
pub var_kind: Option<VarDeclKind>,
@ -83,6 +96,8 @@ pub(crate) struct VarUsageInfo {
/// Indicates a variable or function is overrided without using it.
pub overriden_without_used: bool,
pub no_side_effect_for_member_access: bool,
/// In `c = b`, `b` inffects `c`.
infects: Vec<Id>,
}
@ -143,9 +158,12 @@ impl ProgramData {
to.merge(scope, false);
}
for (id, var_info) in child.vars {
for (id, mut var_info) in child.vars {
// log::trace!("merge({:?},{}{:?})", kind, id.0, id.1);
match self.vars.entry(id) {
Entry::Occupied(mut e) => {
e.get_mut().inline_prevented |= var_info.inline_prevented;
e.get_mut().ref_count += var_info.ref_count;
e.get_mut().cond_init |= var_info.cond_init;
@ -158,6 +176,7 @@ impl ProgramData {
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;
e.get_mut().declared_as_fn_expr |= var_info.declared_as_fn_expr;
// If a var is registered at a parent scope, it means that it's delcared before
// usages.
@ -172,14 +191,30 @@ impl ProgramData {
e.get_mut().infects.extend(var_info.infects);
e.get_mut().no_side_effect_for_member_access =
e.get_mut().no_side_effect_for_member_access
&& var_info.no_side_effect_for_member_access;
match kind {
ScopeKind::Fn => {
e.get_mut().is_fn_local = false;
e.get_mut().used_by_nested_fn = true;
}
ScopeKind::Block => {
if var_info.used_by_nested_fn {
e.get_mut().is_fn_local = false;
e.get_mut().used_by_nested_fn = true;
}
}
ScopeKind::Block => {}
}
}
Entry::Vacant(e) => {
match kind {
ScopeKind::Fn => {
var_info.used_by_nested_fn = true;
}
ScopeKind::Block => {}
}
e.insert(var_info);
}
}
@ -225,17 +260,25 @@ impl UsageAnalyzer {
}
fn report(&mut self, i: Id, is_modify: bool, dejavu: &mut FxHashSet<Id>) {
// log::trace!("report({}{:?})", i.0, i.1);
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()
let e = self.data.vars.entry(i.clone()).or_insert_with(|| {
// log::trace!("insert({}{:?})", i.0, i.1);
VarUsageInfo {
used_above_decl: true,
..Default::default()
}
});
e.inline_prevented |= self.ctx.inline_prevented;
e.ref_count += 1;
e.reassigned |= is_first && is_modify && self.ctx.is_exact_reassignment;
// Passing object as a argument is possibly modification.
@ -258,7 +301,7 @@ impl UsageAnalyzer {
}
fn report_usage(&mut self, i: &Ident, is_assign: bool) {
self.report(i.to_id(), is_assign, &mut Default::default())
self.report(i.to_id(), is_assign, &mut Default::default());
}
fn declare_decl(
@ -266,7 +309,12 @@ impl UsageAnalyzer {
i: &Ident,
has_init: bool,
kind: Option<VarDeclKind>,
is_fn_decl: bool,
) -> &mut VarUsageInfo {
// log::trace!("declare_decl({}{:?})", i.sym, i.span.ctxt);
let ctx = self.ctx;
let v = self
.data
.vars
@ -282,6 +330,9 @@ impl UsageAnalyzer {
is_fn_local: true,
var_kind: kind,
var_initialized: has_init,
no_side_effect_for_member_access: ctx
.in_var_decl_with_no_side_effect_for_member_access,
..Default::default()
});
self.scope
@ -297,6 +348,8 @@ impl UsageAnalyzer {
}
v.declared_as_catch_param |= self.ctx.in_catch_param;
v.declared_as_fn_expr |= is_fn_decl;
v
}
}
@ -385,8 +438,22 @@ impl Visit for UsageAnalyzer {
}
}
fn visit_class(&mut self, n: &Class, _: &dyn Node) {
n.decorators.visit_with(n, self);
{
let ctx = Ctx {
inline_prevented: true,
..self.ctx
};
n.super_class.visit_with(n, &mut *self.with_ctx(ctx));
}
n.body.visit_with(n, self);
}
fn visit_class_decl(&mut self, n: &ClassDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None);
self.declare_decl(&n.ident, true, None, false);
n.visit_children_with(self);
}
@ -415,8 +482,20 @@ impl Visit for UsageAnalyzer {
}
}
fn visit_fn_expr(&mut self, n: &FnExpr, _: &dyn Node) {
n.visit_children_with(self);
if let Some(id) = &n.ident {
self.data
.vars
.entry(id.to_id())
.or_default()
.declared_as_fn_expr = true;
}
}
fn visit_fn_decl(&mut self, n: &FnDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None);
self.declare_decl(&n.ident, true, None, true);
n.visit_children_with(self);
}
@ -432,12 +511,14 @@ impl Visit for UsageAnalyzer {
};
n.left.visit_with(n, &mut *child.with_ctx(ctx));
n.right.visit_with(n, child);
let ctx = Ctx {
in_loop: true,
in_cond: true,
..child.ctx
};
n.body.visit_with(n, &mut *child.with_ctx(ctx))
n.body.visit_with(n, &mut *child.with_ctx(ctx));
});
}
@ -597,6 +678,7 @@ impl Visit for UsageAnalyzer {
&i.id,
self.ctx.in_pat_of_var_decl_with_init,
self.ctx.var_decl_kind_of_pat,
false,
);
if in_pat_of_param {
@ -735,6 +817,10 @@ impl Visit for UsageAnalyzer {
let ctx = Ctx {
in_pat_of_var_decl: true,
in_pat_of_var_decl_with_init: e.init.is_some(),
in_var_decl_with_no_side_effect_for_member_access: match e.init.as_deref() {
Some(Expr::Array(..) | Expr::Lit(..)) => true,
_ => false,
},
..self.ctx
};
e.name.visit_with(e, &mut *self.with_ctx(ctx));

View File

@ -1,4 +1,3 @@
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::analyzer::UsageAnalyzer;
use crate::util::is_hoisted_var_decl_without_init;
@ -24,21 +23,21 @@ pub(super) struct DeclHoisterConfig {
pub top_level: bool,
}
pub(super) fn decl_hoister(config: DeclHoisterConfig) -> Hoister {
pub(super) fn decl_hoister(config: DeclHoisterConfig, data: &ProgramData) -> Hoister {
Hoister {
config,
changed: false,
data: None,
data,
}
}
pub(super) struct Hoister {
pub(super) struct Hoister<'a> {
config: DeclHoisterConfig,
changed: bool,
data: Option<ProgramData>,
data: &'a ProgramData,
}
impl Repeated for Hoister {
impl Repeated for Hoister<'_> {
fn changed(&self) -> bool {
self.changed
}
@ -48,40 +47,30 @@ impl Repeated for Hoister {
}
}
impl Hoister {
impl Hoister<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike + IsModuleItem,
Vec<T>: VisitMutWith<Self> + VisitWith<UsageAnalyzer>,
Vec<T>: for<'aa> VisitMutWith<Hoister<'aa>> + VisitWith<UsageAnalyzer>,
{
match self.data {
None => {
self.data = Some(analyze(stmts));
}
_ => {}
}
stmts.visit_mut_children_with(self);
let should_hoist = !is_sorted_by_key(stmts.iter(), |stmt| match stmt.as_stmt() {
Some(stmt) => match stmt {
Stmt::Decl(Decl::Fn(..)) if self.config.hoist_fns => 1,
Stmt::Decl(Decl::Var(var)) if self.config.hoist_vars => {
if let Some(data) = &self.data {
let ids: Vec<Id> = find_ids(&var.decls);
let ids: Vec<Id> = find_ids(&var.decls);
if ids.iter().any(|id| {
data.vars
.get(id)
.map(|v| !v.used_above_decl)
.unwrap_or(false)
}) {
2
} else {
3
}
} else {
if ids.iter().any(|id| {
self.data
.vars
.get(id)
.map(|v| !v.used_above_decl)
.unwrap_or(false)
}) {
2
} else {
3
}
}
_ => 3,
@ -133,8 +122,8 @@ impl Hoister {
if decl.init.is_none()
&& self
.data
.as_ref()
.and_then(|v| v.vars.get(&id.to_id()))
.vars
.get(&id.to_id())
.map(|v| v.declared_as_fn_param)
.unwrap_or(false)
{
@ -207,8 +196,8 @@ impl Hoister {
if decl.init.is_none()
&& self
.data
.as_ref()
.and_then(|v| v.vars.get(&name.to_id()))
.vars
.get(&name.to_id())
.map(|v| v.declared_as_fn_param)
.unwrap_or(false)
{
@ -258,7 +247,7 @@ impl Hoister {
}
}
impl VisitMut for Hoister {
impl VisitMut for Hoister<'_> {
noop_visit_mut_type!();
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {

View File

@ -1,9 +1,12 @@
use self::drop_console::drop_console;
use self::hoist_decls::DeclHoisterConfig;
use self::optimize::optimizer;
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::compress::hoist_decls::decl_hoister;
use crate::debug::dump;
use crate::debug::invoke;
use crate::marks::Marks;
use crate::option::CompressOptions;
use crate::util::Optional;
#[cfg(feature = "pretty_assertions")]
@ -13,12 +16,15 @@ use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use swc_common::chain;
use std::time::Instant;
use swc_common::comments::Comments;
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_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;
@ -34,7 +40,9 @@ mod drop_console;
mod hoist_decls;
mod optimize;
pub fn compressor<'a>(
pub(crate) fn compressor<'a>(
cm: Lrc<SourceMap>,
marks: Marks,
options: &'a CompressOptions,
comments: Option<&'a dyn Comments>,
) -> impl 'a + JsPass {
@ -43,10 +51,13 @@ pub fn compressor<'a>(
visitor: drop_console(),
};
let compressor = Compressor {
comments,
cm,
marks,
options,
pass: 0,
comments,
changed: false,
pass: 0,
data: None,
};
chain!(
@ -57,10 +68,13 @@ pub fn compressor<'a>(
}
struct Compressor<'a> {
cm: Lrc<SourceMap>,
marks: Marks,
options: &'a CompressOptions,
comments: Option<&'a dyn Comments>,
changed: bool,
pass: usize,
data: Option<ProgramData>,
}
impl CompilerPass for Compressor<'_> {
@ -77,6 +91,7 @@ impl Repeated for Compressor<'_> {
fn reset(&mut self) {
self.changed = false;
self.pass += 1;
self.data = None;
}
}
@ -84,7 +99,7 @@ impl Compressor<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
Vec<T>: VisitMutWith<Self> + VisitMutWith<hoist_decls::Hoister>,
Vec<T>: VisitMutWith<Self> + for<'aa> VisitMutWith<hoist_decls::Hoister<'aa>>,
{
// Skip if `use asm` exists.
if stmts.iter().any(|stmt| match stmt.as_stmt() {
@ -104,17 +119,6 @@ impl Compressor<'_> {
return;
}
{
let mut v = decl_hoister(DeclHoisterConfig {
hoist_fns: self.options.hoist_fns,
hoist_vars: self.options.hoist_vars,
top_level: self.options.top_level(),
});
stmts.visit_mut_with(&mut v);
self.changed |= v.changed();
}
// TODO: Hoist decls
stmts.visit_mut_children_with(self);
// TODO: drop unused
@ -125,38 +129,56 @@ impl VisitMut for Compressor<'_> {
noop_visit_mut_type!();
fn visit_mut_module(&mut self, n: &mut Module) {
debug_assert!(self.data.is_none());
self.data = Some(analyze(&*n));
if self.options.passes != 0 && self.options.passes + 1 <= self.pass {
let done = dump(&*n);
log::trace!("===== Done =====\n{}", done);
log::debug!("===== Done =====\n{}", done);
return;
}
// Temporary
if self.pass > 10 {
panic!("Infinite loop detected")
if self.pass > 30 {
panic!("Infinite loop detected (current pass = {})", self.pass)
}
let start = if cfg!(feature = "debug") {
let start = dump(&*n);
log::trace!("===== Start =====\n{}", start);
let start = dump(&n.clone().fold_with(&mut fixer(None)));
log::debug!("===== Start =====\n{}", start);
start
} else {
String::new()
};
{
log::info!(
"compress: Running expression simplifier (pass = {})",
self.pass
);
let start_time = Instant::now();
let mut visitor = expr_simplifier();
n.map_with_mut(|m| m.fold_with(&mut visitor));
n.visit_mut_with(&mut visitor);
self.changed |= visitor.changed();
if visitor.changed() {
log::trace!("compressor: Simplified expressions");
log::debug!("compressor: Simplified expressions");
if cfg!(feature = "debug") {
log::trace!("===== Simplified =====\n{}", dump(&*n));
log::debug!("===== Simplified =====\n{}", dump(&*n));
}
}
let end_time = Instant::now();
log::info!(
"compress: expr_simplifier took {:?} (pass = {})",
end_time - start_time,
self.pass
);
if cfg!(feature = "debug") && !visitor.changed() {
let simplified = dump(&*n);
let simplified = dump(&n.clone().fold_with(&mut fixer(None)));
if start != simplified {
assert_eq!(
DebugUsingDisplay(&start),
@ -169,12 +191,31 @@ impl VisitMut for Compressor<'_> {
}
{
log::info!("compress: Running optimizer (pass = {})", self.pass);
let start_time = Instant::now();
// TODO: reset_opt_flags
//
// This is swc version of `node.optimize(this);`.
let mut visitor = optimizer(self.options.clone(), self.comments);
let mut visitor = optimizer(
self.cm.clone(),
self.marks,
self.options,
self.comments,
self.data.as_ref().unwrap(),
);
n.visit_mut_with(&mut visitor);
self.changed |= visitor.changed();
let end_time = Instant::now();
log::info!(
"compress: Optimizer took {:?} (pass = {})",
end_time - start_time,
self.pass
);
// let done = dump(&*n);
// log::debug!("===== Result =====\n{}", done);
}
if self.options.conditionals || self.options.dead_code {
@ -184,14 +225,18 @@ impl VisitMut for Compressor<'_> {
"".into()
};
let start_time = Instant::now();
let mut v = dead_branch_remover();
n.map_with_mut(|n| n.fold_with(&mut v));
let end_time = Instant::now();
if cfg!(feature = "debug") {
let simplified = dump(&*n);
if start != simplified {
log::trace!(
log::debug!(
"===== Removed dead branches =====\n{}\n==== ===== ===== ===== ======\n{}",
start,
simplified
@ -199,6 +244,12 @@ impl VisitMut for Compressor<'_> {
}
}
log::info!(
"compress: dead_branch_remover took {:?} (pass = {})",
end_time - start_time,
self.pass
);
self.changed |= v.changed();
}
@ -207,14 +258,20 @@ impl VisitMut for Compressor<'_> {
invoke(&*n);
}
fn visit_mut_stmt(&mut self, n: &mut Stmt) {
// TODO: Skip if node is already optimized.
// if (has_flag(node, SQUEEZED)) return node;
n.visit_mut_children_with(self);
}
fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
{
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 {
@ -232,6 +289,26 @@ impl VisitMut for Compressor<'_> {
});
}
fn visit_mut_script(&mut self, n: &mut Script) {
debug_assert!(self.data.is_none());
self.data = Some(analyze(&*n));
{
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_stmts(&mut self, stmts: &mut Vec<Stmt>) {
self.handle_stmt_likes(stmts);

View File

@ -1,12 +1,13 @@
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::find_ids;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::private_ident;
use swc_ecma_utils::Id;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
@ -36,7 +37,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!("arguments: Optimizing computed access to arguments");
log::debug!("arguments: Optimizing computed access to arguments");
member.computed = false;
member.prop = Box::new(Expr::Ident(Ident {
span: prop.span,
@ -75,9 +76,9 @@ impl Optimizer<'_> {
{
// If a function has a variable named `arguments`, we abort.
let data = analyze(&f.body);
for (id, var) in &data.vars {
if id.0 == js_word!("arguments") && var.declared {
let data: Vec<Id> = find_ids(&f.body);
for id in &data {
if id.0 == js_word!("arguments") {
return;
}
}
@ -113,7 +114,7 @@ impl ArgReplacer<'_> {
let new_args = idx + 1 - self.params.len();
self.changed = true;
log::trace!("arguments: Injecting {} parameters", new_args);
log::debug!("arguments: Injecting {} parameters", new_args);
let mut start = self.params.len();
self.params.extend(
repeat_with(|| {
@ -191,7 +192,7 @@ impl VisitMut for ArgReplacer<'_> {
if let Some(param) = self.params.get(idx) {
match &param.pat {
Pat::Ident(i) => {
log::trace!(
log::debug!(
"arguments: Replacing access to arguments to \
normal reference",
);

View File

@ -20,7 +20,7 @@ impl Optimizer<'_> {
if s.stmts.len() == 1 {
if let Stmt::Return(s) = &mut s.stmts[0] {
if let Some(arg) = &mut s.arg {
log::trace!("arrows: Optimizing the body of an arrow");
log::debug!("arrows: Optimizing the body of an arrow");
*b = BlockStmtOrExpr::Expr(arg.take());
return;
}

View File

@ -1,71 +1,20 @@
use super::Optimizer;
use crate::compress::optimize::is_pure_undefined;
use crate::compress::optimize::{is_pure_undefined, Ctx};
use crate::debug::dump;
use crate::util::make_bool;
use swc_atoms::js_word;
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::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<'_> {
/// Disabled because it can change semantics.
///
/// - `!foo || bar();` => `foo && bar();`
/// - `!foo && bar();` => `foo || bar();`
pub(super) fn compress_logical_exprs_with_negated_lhs(&mut self, e: &mut Expr) {
if !self.options.bools || true {
return;
}
match e {
Expr::Bin(BinExpr {
span,
op: op @ op!("||"),
left,
right,
..
})
| Expr::Bin(BinExpr {
span,
op: op @ op!("&&"),
left,
right,
..
}) => match &mut **left {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
if *op == op!("&&") {
log::trace!("booleans: Compressing `!foo && bar` as `foo || bar`");
} else {
log::trace!("booleans: Compressing `!foo || bar` as `foo && bar`");
}
self.changed = true;
*e = Expr::Bin(BinExpr {
span: *span,
left: arg.take(),
op: if *op == op!("&&") {
op!("||")
} else {
op!("&&")
},
right: right.take(),
});
return;
}
_ => {}
},
_ => {}
}
}
///
/// - `!condition() || !-3.5` => `!condition()`
///
@ -104,9 +53,9 @@ impl Optimizer<'_> {
if can_remove {
if *op == op!("&&") {
log::trace!("booleans: Compressing `!foo && true` as `!foo`");
log::debug!("booleans: Compressing `!foo && true` as `!foo`");
} else {
log::trace!("booleans: Compressing `!foo || false` as `!foo`");
log::debug!("booleans: Compressing `!foo || false` as `!foo`");
}
self.changed = true;
*e = *left.take();
@ -128,10 +77,7 @@ impl Optimizer<'_> {
match e {
Expr::Unary(UnaryExpr {
span,
op: op!("!"),
arg,
..
op: op!("!"), arg, ..
}) => match &mut **arg {
Expr::Bin(BinExpr {
op: op!("&&"),
@ -139,30 +85,123 @@ impl Optimizer<'_> {
right,
..
}) => {
log::trace!("Optimizing ``!(a && b)` as `!a || !b`");
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;
*e = Expr::Bin(BinExpr {
span: *span,
op: op!("||"),
left: Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: left.take(),
})),
right: Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: right.take(),
})),
});
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.
pub(super) fn optimize_bang_within_logical_ops(
&mut self,
expr: &mut Expr,
is_ret_val_ignored: bool,
) -> bool {
if negate_cost(&expr, is_ret_val_ignored, is_ret_val_ignored).unwrap_or(isize::MAX) >= 0 {
return false;
}
let e = match expr {
Expr::Bin(b) => b,
_ => return false,
};
match e.op {
op!("&&") | op!("||") => {}
_ => return false,
}
if !is_ret_val_ignored {
if let Known(Type::Bool) = e.left.get_type() {
} else {
// Don't change type.
return false;
}
if let Known(Type::Bool) = e.right.get_type() {
} else {
// Don't change type.
return false;
}
}
// `!_ && 'undefined' !== typeof require`
//
// =>
//
// `_ || 'undefined' == typeof require`
log::debug!(
"bools({}): Negating: (!a && !b) => !(a || b) (because both expression are good for \
negation)",
self.line_col(e.span)
);
let start = dump(&*e);
e.op = if e.op == op!("&&") {
op!("||")
} else {
op!("&&")
};
let ctx = Ctx {
in_bool_ctx: true,
..self.ctx
};
self.changed = true;
self.with_ctx(ctx).negate(&mut e.left);
self.with_ctx(ctx).negate(&mut e.right);
if cfg!(feature = "debug") {
log::trace!("[Change] {} => {}", start, dump(&*e));
}
true
}
pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
@ -226,7 +265,7 @@ impl Optimizer<'_> {
} else {
self.changed = true;
let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` as sequence expression");
log::debug!("booleans: Compressing `delete` as sequence expression");
*e = Expr::Seq(SeqExpr {
span,
exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))],
@ -241,26 +280,84 @@ impl Optimizer<'_> {
if convert_to_true {
self.changed = true;
let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` => true");
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 &**arg {
}) => match &mut **arg {
Expr::Lit(Lit::Num(Number { value, .. })) => {
log::trace!("Optimizing: number => number (in bool context)");
log::debug!("Optimizing: number => number (in bool context)");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
@ -268,6 +365,15 @@ impl Optimizer<'_> {
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;
}
_ => {}
},
@ -276,7 +382,7 @@ impl Optimizer<'_> {
op: op!("typeof"),
arg,
}) => {
log::trace!("Optimizing: typeof => true (in bool context)");
log::debug!("Optimizing: typeof => true (in bool context)");
self.changed = true;
match &**arg {
@ -301,7 +407,7 @@ impl Optimizer<'_> {
}
Expr::Lit(Lit::Str(s)) => {
log::trace!("Converting string as boolean expressions");
log::debug!("Converting string as boolean expressions");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
span: s.span,
@ -314,7 +420,7 @@ impl Optimizer<'_> {
return;
}
if self.options.bools {
log::trace!("booleans: Converting number as boolean expressions");
log::debug!("booleans: Converting number as boolean expressions");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
span: num.span,
@ -331,7 +437,7 @@ impl Optimizer<'_> {
}) => {
// Optimize if (a ?? false); as if (a);
if let Value::Known(false) = right.as_pure_bool() {
log::trace!(
log::debug!(
"Dropping right operand of `??` as it's always false (in bool context)"
);
self.changed = true;
@ -348,7 +454,7 @@ impl Optimizer<'_> {
// `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)");
log::debug!("bools: `expr || false` => `expr` (in bool context)");
self.changed = true;
*n = *left.take();
return;
@ -359,8 +465,9 @@ impl Optimizer<'_> {
let span = n.span();
let v = n.as_pure_bool();
if let Known(v) = v {
log::trace!("Optimizing expr as {} (in bool context)", v);
log::debug!("Optimizing expr as {} (in bool context)", v);
*n = make_bool(span, v);
return;
}
}
}
@ -381,7 +488,7 @@ impl Optimizer<'_> {
None => match &mut *stmt.cons {
Stmt::Expr(cons) => {
self.changed = true;
log::trace!("conditionals: `if (foo) bar;` => `foo && bar`");
log::debug!("conditionals: `if (foo) bar;` => `foo && bar`");
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr: Box::new(Expr::Bin(BinExpr {
@ -397,4 +504,286 @@ impl Optimizer<'_> {
},
}
}
///
/// - `"undefined" == typeof value;` => `void 0 === value`
pub(super) fn compress_typeof_undefined(&mut self, e: &mut BinExpr) {
fn opt(o: &mut Optimizer, l: &mut Expr, r: &mut Expr) -> bool {
match (&mut *l, &mut *r) {
(
Expr::Lit(Lit::Str(Str {
value: js_word!("undefined"),
..
})),
Expr::Unary(UnaryExpr {
op: op!("typeof"),
arg,
..
}),
) => {
// TODO?
match &**arg {
Expr::Ident(arg) => {
if let Some(usage) =
o.data.as_ref().and_then(|data| data.vars.get(&arg.to_id()))
{
if !usage.declared {
return false;
}
}
}
_ => {}
}
*l = *undefined(l.span());
*r = *arg.take();
true
}
_ => false,
}
}
match e.op {
op!("==") | op!("!=") | op!("===") | op!("!==") => {}
_ => return,
}
if opt(self, &mut e.left, &mut e.right) || opt(self, &mut e.right, &mut e.left) {
e.op = match e.op {
op!("==") => {
op!("===")
}
op!("!=") => {
op!("!==")
}
_ => e.op,
};
}
}
}
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<isize> {
fn cost(
e: &Expr,
in_bool_ctx: bool,
bin_op: Option<BinaryOp>,
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,
);
}
}

View File

@ -4,6 +4,7 @@ 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;
@ -57,7 +58,7 @@ impl Optimizer<'_> {
return;
}
if self.ctx.in_try_block || self.ctx.executed_multiple_time {
if self.ctx.in_try_block || self.ctx.executed_multiple_time || self.ctx.in_cond {
return;
}
@ -84,6 +85,20 @@ impl Optimizer<'_> {
{
return;
}
if usage.used_in_loop {
match &*assign.right {
Expr::Lit(..) | Expr::Ident(..) => {}
_ => return,
}
}
if usage.usage_count >= 2 {
match &*assign.right {
Expr::Lit(..) => {}
_ => return,
}
}
}
let value = match &*assign.right {
@ -94,6 +109,12 @@ impl Optimizer<'_> {
_ => return,
};
log::debug!(
"collpase_vars: Decided to inline {}{:?}",
left.id.sym,
left.id.span.ctxt
);
self.lits.insert(left.to_id(), value);
}
_ => {}
@ -101,139 +122,284 @@ impl Optimizer<'_> {
}
/// Collapse single-use non-constant variables, side effects permitting.
pub(super) fn collapse_consequtive_vars<T>(&mut self, stmts: &mut Vec<T>)
///
/// 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<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
Vec<T>:
VisitWith<VarWithOutInitCounter> + VisitMutWith<VarMover> + VisitMutWith<VarPrepender>,
{
if !self.options.collapse_vars {
return;
}
let indexes = stmts
.windows(2)
.enumerate()
.filter_map(|(i, stmts)| {
match (stmts[0].as_stmt(), stmts[1].as_stmt()) {
(Some(Stmt::Decl(Decl::Var(v))), Some(r)) => {
// Should be handled by other passes.
if v.kind == VarDeclKind::Const {
return None;
}
{
// let mut found_other = false;
// let mut need_work = false;
// We only inline for some subset of statements.
match r {
Stmt::Expr(..) | Stmt::Return(..) => {}
_ => return None,
}
// 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;
// }
// }
// We only touch last variable delcarator.
if let Some(last) = v.decls.last() {
match last.init.as_deref()? {
Expr::Object(..) => {
// Objects are handled by other passes.
return None;
}
Expr::Arrow(..) | Expr::Fn(..) => {
// Handled by other passes
return None;
}
// _ => {
// found_other = true;
// }
// }
// }
_ => {
let mut chekcer = InlinabiltyChecker { can_inline: true };
last.init
.visit_with(&Invalid { span: DUMMY_SP }, &mut chekcer);
if !chekcer.can_inline {
return None;
}
}
}
match &last.name {
Pat::Ident(name) => {
//
if let Some(var) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&name.to_id()))
{
if var.usage_count != 1
|| var.mutated
|| !SimpleUsageFinder::find(&name.id, r)
{
return None;
}
return Some(i);
}
None
}
_ => None,
}
} else {
None
}
}
_ => None,
}
})
.collect::<Vec<_>>();
if indexes.is_empty() {
return;
// 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::trace!("collapse_vars: Collapsing {:?}", indexes);
log::debug!("collapse_vars: Collapsing variables without an initializer");
let mut new = Vec::with_capacity(stmts.len());
let mut values = FxHashMap::default();
for (idx, stmt) in stmts.take().into_iter().enumerate() {
match stmt.try_into_stmt() {
Ok(mut stmt) => match stmt {
Stmt::Decl(Decl::Var(mut v)) if indexes.contains(&idx) => {
if let Some(last) = v.decls.pop() {
match last.name {
Pat::Ident(name) => {
values.insert(name.to_id(), last.init);
}
_ => unreachable!(),
}
} else {
unreachable!()
}
}
_ => {
stmt.visit_mut_with(&mut Inliner {
values: &mut values,
});
let vars = {
let mut v = VarMover {
vars: Default::default(),
var_decl_kind: Default::default(),
};
stmts.visit_mut_with(&mut v);
new.push(T::from_stmt(stmt));
}
},
Err(item) => {
new.push(item);
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;
}
}
}
*stmts = new;
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<VarDeclarator>,
var_decl_kind: Option<VarDeclKind>,
}
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<VarDeclOrExpr>) {
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<VarDeclarator>) {
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;
}
}
/// Checks inlinabilty of variable initializer.
struct InlinabiltyChecker {
can_inline: bool,
pub(super) struct VarPrepender {
vars: Vec<VarDeclarator>,
}
impl Visit for InlinabiltyChecker {
noop_visit_type!();
impl VisitMut for VarPrepender {
noop_visit_mut_type!();
fn visit_update_expr(&mut self, _: &UpdateExpr, _: &dyn Node) {
self.can_inline = false;
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> {
@ -249,6 +415,8 @@ impl VisitMut for Inliner<'_> {
match e {
Expr::Ident(i) => {
if let Some(value) = self.values.remove(&i.to_id()) {
log::debug!("collapse_vars: Inlining {}{:?}", i.sym, i.span.ctxt);
*e = *value.expect("should be used only once");
}
}
@ -256,63 +424,3 @@ impl VisitMut for Inliner<'_> {
}
}
}
/// Finds usage of `ident`, but except assignment to it.
struct SimpleUsageFinder<'a> {
ident: &'a Ident,
found: bool,
}
impl<'a> Visit for SimpleUsageFinder<'a> {
noop_visit_type!();
fn visit_pat(&mut self, n: &Pat, _: &dyn Node) {
match n {
Pat::Ident(..) => {}
_ => {
n.visit_children_with(self);
}
}
}
fn visit_update_expr(&mut self, _: &UpdateExpr, _: &dyn Node) {}
fn visit_expr_or_spread(&mut self, n: &ExprOrSpread, _: &dyn Node) {
if n.spread.is_some() {
return;
}
n.visit_children_with(self);
}
fn visit_assign_expr(&mut self, e: &AssignExpr, _: &dyn Node) {
e.right.visit_with(e, self);
}
fn visit_ident(&mut self, i: &Ident, _: &dyn Node) {
if i.span.ctxt() == self.ident.span.ctxt() && i.sym == self.ident.sym {
self.found = true;
}
}
fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) {
e.obj.visit_with(e as _, self);
if e.computed {
e.prop.visit_with(e as _, self);
}
}
}
impl<'a> SimpleUsageFinder<'a> {
pub fn find<N>(ident: &'a Ident, node: &N) -> bool
where
N: VisitWith<Self>,
{
let mut v = SimpleUsageFinder {
ident,
found: false,
};
node.visit_with(&Invalid { span: DUMMY_SP } as _, &mut v);
v.found
}
}

View File

@ -1,6 +1,11 @@
use super::Optimizer;
use crate::compress::optimize::bools::negate_cost;
use crate::compress::optimize::Ctx;
use crate::compress::optimize::DISABLE_BUGGY_PASSES;
use crate::debug::dump;
use crate::util::make_bool;
use crate::util::SpanExt;
use std::mem::swap;
use swc_common::EqIgnoreSpan;
use swc_common::Spanned;
use swc_common::DUMMY_SP;
@ -18,6 +23,104 @@ 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() {
Some(v) => v,
_ => return,
};
match &*stmt.cons {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => return,
_ => {}
}
if negate_cost(&stmt.test, true, false).unwrap_or(isize::MAX) < 0 {
self.changed = true;
log::debug!("if_return: Negating `cond` of an if statement which has cons and alt");
let ctx = Ctx {
in_bool_ctx: true,
..self.ctx
};
self.with_ctx(ctx).negate(&mut stmt.test);
swap(alt, &mut *stmt.cons);
return;
}
match &*alt {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => {
self.changed = true;
log::debug!(
"if_return: Negating an if statement because the alt is return / continue"
);
self.negate(&mut stmt.test);
swap(alt, &mut *stmt.cons);
}
_ => return,
}
}
/// This method may change return value.
///
/// - `a ? b : false` => `a && b`
pub(super) fn compress_cond_to_logical_ignoring_return_value(&mut self, e: &mut Expr) {
let cond = match e {
Expr::Cond(cond) => cond,
_ => return,
};
if !cond.cons.may_have_side_effects() {
self.changed = true;
log::debug!("conditionals: `cond ? useless : alt` => `cond || alt`");
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
if !cond.alt.may_have_side_effects() {
self.changed = true;
log::debug!("conditionals: `cond ? cons : useless` => `cond && cons`");
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("&&"),
left: cond.test.take(),
right: cond.cons.take(),
});
return;
}
}
/// Removes useless operands of an logical expressions.
pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) {
if !self.options.conditionals {
@ -47,7 +150,7 @@ impl Optimizer<'_> {
// `!!b || true` => true
if let Known(true) = rb {
self.changed = true;
log::trace!("conditionals: `!!foo || true` => `true`");
log::debug!("conditionals: `!!foo || true` => `true`");
*e = make_bool(bin.span, true);
return;
}
@ -70,7 +173,7 @@ impl Optimizer<'_> {
(Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt)
if (*cons.right).eq_ignore_span(&*alt) =>
{
log::trace!("conditionals: `x ? y || z : z` => `x || y && z`");
log::debug!("conditionals: `x ? y || z : z` => `x || y && z`");
self.changed = true;
*e = Expr::Bin(BinExpr {
@ -91,13 +194,10 @@ impl Optimizer<'_> {
}
///
/// - `foo ? 1 : false` => `!!foo && 1`
/// - `!foo ? true : 0` => `!foo || 0`
/// - `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) {
if !self.options.conditionals {
return;
}
let cond = match e {
Expr::Cond(cond) => cond,
_ => return,
@ -107,7 +207,7 @@ impl Optimizer<'_> {
if let Known(Type::Bool) = lt {
let lb = cond.cons.as_pure_bool();
if let Known(true) = lb {
log::trace!("conditionals: `foo ? true : bar` => `!!foo || bar`");
log::debug!("conditionals: `foo ? true : bar` => `!!foo || bar`");
// Negate twice to convert `test` to boolean.
self.negate_twice(&mut cond.test);
@ -123,21 +223,19 @@ impl Optimizer<'_> {
}
// TODO: Verify this rule.
if false {
if let Known(false) = lb {
log::trace!("conditionals: `foo ? false : bar` => `!foo && bar`");
if let Known(false) = lb {
log::debug!("conditionals: `foo ? false : bar` => `!foo && bar`");
self.changed = true;
self.negate(&mut cond.test);
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;
}
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("&&"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
}
@ -145,7 +243,7 @@ impl Optimizer<'_> {
if let Known(Type::Bool) = rt {
let rb = cond.alt.as_pure_bool();
if let Known(false) = rb {
log::trace!("conditionals: `foo ? 1 : false` => `!!foo && 1`");
log::debug!("conditionals: `foo ? bar : false` => `!!foo && bar`");
self.changed = true;
// Negate twice to convert `test` to boolean.
@ -159,6 +257,22 @@ impl Optimizer<'_> {
});
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;
}
}
}
@ -199,7 +313,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!("conditionals: Merging if statements with same `cons`");
log::debug!("conditionals: Merging if statements with same `cons`");
let mut cur: Option<IfStmt> = None;
let mut new = Vec::with_capacity(stmts.len());
@ -272,15 +386,28 @@ impl Optimizer<'_> {
/// }
/// ```
pub(super) fn compress_if_stmt_as_cond(&mut self, s: &mut Stmt) {
if !self.options.conditionals {
return;
}
let stmt = match s {
Stmt::If(v) => v,
_ => return,
};
match &*stmt.cons {
Stmt::Empty(..) => {
if self.options.conditionals || self.options.unused {
if stmt.alt.is_none() {
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr: stmt.test.take(),
});
self.changed = true;
log::debug!("conditionals: `if (foo);` => `foo` ");
return;
}
}
}
_ => {}
}
// If alt does not exist, an if statement is better than a conditional
// expression.
let alt = match &mut stmt.alt {
@ -303,7 +430,7 @@ impl Optimizer<'_> {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
log::trace!("Optimizing `if (!foo); else bar();` as `foo && bar();`");
log::debug!("Optimizing `if (!foo); else bar();` as `foo && bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
@ -318,7 +445,7 @@ impl Optimizer<'_> {
});
}
_ => {
log::trace!("Optimizing `if (foo); else bar();` as `foo || bar();`");
log::debug!("Optimizing `if (foo); else bar();` as `foo || bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
@ -356,29 +483,27 @@ impl Optimizer<'_> {
None => {}
}
// if (a) b(); else c(); => a ? b() : c()
log::trace!(
"Compressing if statement as conditional expression (even though cons and alt is not \
compressable)"
);
self.changed = true;
*s = Stmt::Expr(ExprStmt {
span: stmt.span.with_mark(self.done),
expr: Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: stmt.test.take(),
cons: Box::new(cons.take()),
alt: Box::new(alt.take()),
})),
})
if self.options.conditionals || self.options.bools {
// if (a) b(); else c(); => a ? b() : c()
log::debug!(
"Compressing if statement as conditional expression (even though cons and alt is \
not compressable)"
);
self.changed = true;
*s = Stmt::Expr(ExprStmt {
span: stmt.span.with_mark(self.done),
expr: Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: stmt.test.take(),
cons: Box::new(cons.take()),
alt: Box::new(alt.take()),
})),
})
}
}
/// Compress a conditional expression if cons and alt is simillar
pub(super) fn compress_cond_expr_if_simillar(&mut self, e: &mut Expr) {
if !self.options.conditionals {
return;
}
let cond = match e {
Expr::Cond(expr) => expr,
_ => return,
@ -398,7 +523,7 @@ impl Optimizer<'_> {
// x ? x : y => x || y
if cond.test.is_ident() && cond.test.eq_ignore_span(&cond.cons) {
log::trace!("Compressing `x ? x : y` as `x || y`");
log::debug!("Compressing `x ? x : y` as `x || y`");
self.changed = true;
*e = Expr::Bin(BinExpr {
span: cond.span,
@ -417,43 +542,24 @@ impl Optimizer<'_> {
alt: &mut Expr,
is_for_if_stmt: bool,
) -> Option<Expr> {
if cons.eq_ignore_span(alt) {
if cons.eq_ignore_span(alt)
&& match &*cons {
Expr::Yield(..) | Expr::Fn(..) => false,
_ => true,
}
{
log::debug!("conditionals: cons is same as alt");
return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![test.take(), Box::new(cons.take())],
}));
}
match &**test {
Expr::Call(CallExpr {
callee: ExprOrSuper::Expr(callee),
..
}) => match &**callee {
Expr::Ident(callee) => {
let side_effect_free = self
.data
.as_ref()
.and_then(|data| data.vars.get(&callee.to_id()))
.map(|v| v.is_fn_local)
.unwrap_or(false);
if !side_effect_free {
match cons {
Expr::Call(..) => {
return None;
}
_ => {}
}
}
}
_ => return None,
},
_ => {}
}
match (cons, alt) {
(Expr::Call(cons), Expr::Call(alt)) => {
let cons_callee = cons.callee.as_expr().and_then(|e| e.as_ident())?;
//
if !cons.callee.eq_ignore_span(&alt.callee) {
return None;
}
@ -462,7 +568,7 @@ impl Optimizer<'_> {
.data
.as_ref()
.and_then(|data| data.vars.get(&cons_callee.to_id()))
.map(|v| v.is_fn_local)
.map(|v| v.is_fn_local && v.declared)
.unwrap_or(false);
if side_effect_free
@ -476,8 +582,9 @@ impl Optimizer<'_> {
.zip(alt.args.iter())
.filter(|(cons, alt)| !cons.eq_ignore_span(alt))
.count();
if diff_count == 1 {
log::trace!(
log::debug!(
"conditionals: Merging cons and alt as only one argument differs"
);
self.changed = true;
@ -543,7 +650,7 @@ impl Optimizer<'_> {
})),
});
log::trace!(
log::debug!(
"Compreessing if into cond as there's no side effect and the number of \
arguments is 1"
);
@ -556,7 +663,7 @@ impl Optimizer<'_> {
}
if !side_effect_free && is_for_if_stmt {
log::trace!("Compreessing if into cond while preserving side effects");
log::debug!("Compreessing if into cond while preserving side effects");
return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: test.take(),
@ -601,7 +708,7 @@ impl Optimizer<'_> {
})),
});
log::trace!(
log::debug!(
"Compreessing if statement into a condiotnal expression of `new` as \
there's no side effect and the number of arguments is 1"
);
@ -623,7 +730,7 @@ impl Optimizer<'_> {
&& cons.left.eq_ignore_span(&alt.left)
&& is_simple_lhs(&cons.left) =>
{
log::trace!("Merging assignments in cons and alt of if statement");
log::debug!("Merging assignments in cons and alt of if statement");
Some(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: cons.op,
@ -639,7 +746,7 @@ impl Optimizer<'_> {
// a ? b ? c() : d() : d() => a && b ? c() : d()
(Expr::Cond(cons), alt) if (*cons.alt).eq_ignore_span(&*alt) => {
log::trace!("conditionals: a ? b ? c() : d() : d() => a && b ? c() : d()");
log::debug!("conditionals: a ? b ? c() : d() : d() => a && b ? c() : d()");
return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: Box::new(Expr::Bin(BinExpr {
@ -656,16 +763,19 @@ impl Optimizer<'_> {
// z ? "fuji" : (condition(), "fuji");
// =>
// (z || condition(), "fuji");
(cons, Expr::Seq(alt))
if alt.exprs.len() == 2 && (**alt.exprs.last().unwrap()).eq_ignore_span(&*cons) =>
{
log::trace!("conditionals: Reducing seq expr in alt");
(cons, Expr::Seq(alt)) if (**alt.exprs.last().unwrap()).eq_ignore_span(&*cons) => {
self.changed = true;
log::debug!("conditionals: Reducing seq expr in alt");
//
alt.exprs.pop();
let first = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: test.take(),
op: op!("||"),
right: alt.exprs[0].take(),
right: Box::new(Expr::Seq(SeqExpr {
span: alt.span,
exprs: alt.exprs.take(),
})),
}));
return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP,
@ -673,9 +783,164 @@ impl Optimizer<'_> {
}));
}
// z ? (condition(), "fuji") : "fuji"
// =>
// (z && condition(), "fuji");
(Expr::Seq(cons), alt) if (**cons.exprs.last().unwrap()).eq_ignore_span(&*alt) => {
self.changed = true;
log::debug!("conditionals: Reducing seq expr in cons");
//
cons.exprs.pop();
let first = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: test.take(),
op: op!("&&"),
right: Box::new(Expr::Seq(SeqExpr {
span: cons.span,
exprs: cons.exprs.take(),
})),
}));
return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![first, Box::new(alt.take())],
}));
}
_ => None,
}
}
/// Currently disabled.
pub(super) fn inject_else(&mut self, stmts: &mut Vec<Stmt>) {
if DISABLE_BUGGY_PASSES {
return;
}
let len = stmts.len();
let pos_of_if = stmts.iter().enumerate().rposition(|(idx, s)| {
idx != len - 1
&& match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => match &**cons {
Stmt::Block(b) => {
b.stmts.len() == 2
&& match b.stmts.first() {
Some(Stmt::If(..) | Stmt::Expr(..)) => false,
_ => true,
}
&& match b.stmts.last() {
Some(Stmt::Return(ReturnStmt { arg: None, .. })) => true,
_ => false,
}
}
_ => false,
},
_ => false,
}
});
let pos_of_if = match pos_of_if {
Some(v) => v,
_ => return,
};
self.changed = true;
log::debug!("if_return: Injecting else because it's shorter");
let mut new = vec![];
new.reserve(pos_of_if + 1);
new.extend(stmts.drain(..pos_of_if));
let alt = stmts.drain(1..).collect::<Vec<_>>();
let if_stmt = stmts.take().into_iter().next().unwrap();
match if_stmt {
Stmt::If(mut s) => {
match &mut *s.cons {
Stmt::Block(cons) => {
cons.stmts.pop();
}
_ => {
unreachable!()
}
}
assert_eq!(s.alt, None);
s.alt = Some(if alt.len() == 1 {
Box::new(alt.into_iter().next().unwrap())
} else {
Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: alt,
}))
});
new.push(Stmt::If(s))
}
_ => {
unreachable!()
}
}
*stmts = new;
}
/// if (foo) return bar()
/// else baz()
///
/// `else` token can be removed from the code above.
pub(super) fn drop_else_token<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
// Find an if statement with else token.
let need_work = stmts.iter().any(|stmt| match stmt.as_stmt() {
Some(Stmt::If(IfStmt {
cons,
alt: Some(..),
..
})) => always_terminates(cons),
_ => false,
});
if !need_work {
return;
}
//
let mut new_stmts = vec![];
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => match stmt {
Stmt::If(IfStmt {
span,
test,
cons,
alt: Some(alt),
..
}) if always_terminates(&cons) => {
new_stmts.push(T::from_stmt(Stmt::If(IfStmt {
span,
test,
cons,
alt: None,
})));
new_stmts.push(T::from_stmt(*alt));
}
_ => {
new_stmts.push(T::from_stmt(stmt));
}
},
Err(stmt) => new_stmts.push(stmt),
}
}
self.changed = true;
log::debug!("conditionals: Dropped useless `else` token");
*stmts = new_stmts;
}
}
fn extract_expr_stmt(s: &mut Stmt) -> Option<&mut Expr> {
@ -693,7 +958,23 @@ fn is_simple_lhs(l: &PatOrExpr) -> bool {
},
PatOrExpr::Pat(l) => match &**l {
Pat::Ident(_) => return true,
Pat::Expr(e) => match &**e {
Expr::Ident(..) => return true,
_ => false,
},
_ => false,
},
}
}
pub(super) fn always_terminates(s: &Stmt) -> bool {
match s {
Stmt::Return(..) | Stmt::Throw(..) | Stmt::Break(..) | Stmt::Continue(..) => true,
Stmt::If(IfStmt { cons, alt, .. }) => {
always_terminates(&cons) && alt.as_deref().map(always_terminates).unwrap_or(false)
}
Stmt::Block(s) => s.stmts.iter().any(always_terminates),
_ => false,
}
}

View File

@ -55,7 +55,7 @@ impl Optimizer<'_> {
.map(|var| var.is_fn_local)
.unwrap_or(false)
{
log::trace!(
log::debug!(
"dead_code: Dropping an assigment to a varaible declared in \
function because function is being terminated"
);

View File

@ -65,7 +65,7 @@ impl Optimizer<'_> {
sym: js_word!("undefined"),
..
}) => {
log::trace!("evaluate: `undefined` -> `void 0`");
log::debug!("evaluate: `undefined` -> `void 0`");
self.changed = true;
*e = *undefined(*span);
return;
@ -76,7 +76,7 @@ impl Optimizer<'_> {
sym: js_word!("Infinity"),
..
}) => {
log::trace!("evaluate: `Infinity` -> `1 / 0`");
log::debug!("evaluate: `Infinity` -> `1 / 0`");
self.changed = true;
*e = Expr::Bin(BinExpr {
span: span.with_ctxt(self.done_ctxt),
@ -149,7 +149,7 @@ impl Optimizer<'_> {
1 => match &*args[0].expr {
Expr::Lit(Lit::Str(exp)) => {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Converting RegExpr call into a regexp literal `/{}/`",
exp.value
);
@ -165,7 +165,7 @@ impl Optimizer<'_> {
_ => match (&*args[0].expr, &*args[1].expr) {
(Expr::Lit(Lit::Str(exp)), Expr::Lit(Lit::Str(flags))) => {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Converting RegExpr call into a regexp literal `/{}/{}`",
exp.value,
flags.value
@ -209,7 +209,7 @@ impl Optimizer<'_> {
match char::from_u32(v) {
Some(v) => {
self.changed = true;
log::trace!(
log::debug!(
"evanluate: Evaluated `String.charCodeAt({})` as `{}`",
char_code,
v
@ -362,7 +362,7 @@ impl Optimizer<'_> {
match c {
Some(v) => {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Evaluated `charCodeAt` of a string literal as `{}`",
v
);
@ -373,7 +373,7 @@ impl Optimizer<'_> {
}
None => {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Evaluated `charCodeAt` of a string literal as `NaN`",
);
*e = Expr::Ident(Ident::new(
@ -389,7 +389,7 @@ impl Optimizer<'_> {
};
self.changed = true;
log::trace!("evaluate: Evaluated `{}` of a string literal", method);
log::debug!("evaluate: Evaluated `{}` of a string literal", method);
*e = Expr::Lit(Lit::Str(Str {
value: new_val.into(),
..s
@ -412,7 +412,7 @@ impl Optimizer<'_> {
..
}) => {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Reducing a call to `Number` into an unary operation"
);
@ -442,7 +442,7 @@ impl Optimizer<'_> {
Expr::Call(..) => {
if let Some(value) = self.eval_as_number(&e) {
self.changed = true;
log::trace!("evaluate: Evaluated an expression as `{}`", value);
log::debug!("evaluate: Evaluated an expression as `{}`", value);
*e = Expr::Lit(Lit::Num(Number {
span: e.span(),
@ -462,7 +462,7 @@ impl Optimizer<'_> {
if let Known(l) = l {
if let Known(r) = r {
self.changed = true;
log::trace!("evaluate: Evaulated `{:?} ** {:?}`", l, r);
log::debug!("evaluate: Evaulated `{:?} ** {:?}`", l, r);
let value = l.powf(r);
*e = Expr::Lit(Lit::Num(Number {
@ -507,7 +507,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!("evaluate: `0 / 0` => `NaN`");
log::debug!("evaluate: `0 / 0` => `NaN`");
// Sign does not matter for NaN
*e = Expr::Ident(Ident::new(
@ -518,7 +518,7 @@ impl Optimizer<'_> {
}
(FpCategory::Normal, FpCategory::Zero) => {
self.changed = true;
log::trace!("evaluate: `constant / 0` => `Infinity`");
log::debug!("evaluate: `constant / 0` => `Infinity`");
// Sign does not matter for NaN
*e = if ln.is_sign_positive() == rn.is_sign_positive() {
@ -590,7 +590,7 @@ impl Optimizer<'_> {
let value = num_to_fixed(num.value, precision + 1);
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Evaluating `{}.toFixed({})` as `{}`",
num,
precision,
@ -816,7 +816,7 @@ impl Optimizer<'_> {
match call.args.len() {
0 => {
self.changed = true;
log::trace!("evaluate: Dropping array.slice call");
log::debug!("evaluate: Dropping array.slice call");
*e = *obj.take();
return;
}
@ -825,7 +825,7 @@ impl Optimizer<'_> {
let start = start.floor() as usize;
self.changed = true;
log::trace!("evaluate: Reducing array.slice({}) call", start);
log::debug!("evaluate: Reducing array.slice({}) call", start);
if start >= arr.elems.len() {
*e = Expr::Array(ArrayLit {
@ -850,7 +850,7 @@ impl Optimizer<'_> {
if let Known(end) = end {
let end = end.floor() as usize;
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Reducing array.slice({}, {}) call",
start,
end
@ -936,7 +936,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!(
log::debug!(
"evaludate: Reduced `funtion.valueOf()` into a function expression"
);
@ -977,7 +977,7 @@ impl Optimizer<'_> {
// As we used as_pure_bool, we can drop it.
if v && e.op == op!("&&") {
self.changed = true;
log::trace!("Removing `b` from `a && b && c` because b is always truthy");
log::debug!("Removing `b` from `a && b && c` because b is always truthy");
left.right.take();
return;
@ -985,7 +985,7 @@ impl Optimizer<'_> {
if !v && e.op == op!("||") {
self.changed = true;
log::trace!("Removing `b` from `a || b || c` because b is always falsy");
log::debug!("Removing `b` from `a || b || c` because b is always falsy");
left.right.take();
return;
@ -1011,7 +1011,7 @@ impl Optimizer<'_> {
//
if is_pure_undefined_or_null(&obj) {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Reduced an optioanl chaining operation because object is \
always null or undefined"
);
@ -1028,7 +1028,7 @@ impl Optimizer<'_> {
}) => {
if is_pure_undefined_or_null(&callee) {
self.changed = true;
log::trace!(
log::debug!(
"evaluate: Reduced a call expression with optioanl chaining operation \
because object is always null or undefined"
);

View File

@ -0,0 +1,97 @@
use super::Optimizer;
use crate::util::{sort::is_sorted_by, MoudleItemExt};
use std::cmp::Ordering;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
impl Optimizer<'_> {
/// Calls `reorder_stmts_inner` after splitting stmts.
pub(super) fn reorder_stmts<T>(&mut self, stmts: &mut Vec<T>)
where
T: MoudleItemExt,
{
if self.ctx.in_asm {
return;
}
self.reorder_stmts_inner(stmts);
}
/// Sorts given statements.
fn reorder_stmts_inner<T>(&mut self, stmts: &mut Vec<T>)
where
T: MoudleItemExt,
{
if !self.options.join_vars {
return;
}
// Sort by position
if is_sorted_by(stmts.iter(), |a, b| {
// Check for function declarations.
match (a.as_module_decl(), b.as_module_decl()) {
(
Err(Stmt::Decl(
Decl::Fn(..)
| Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
)),
Err(Stmt::Decl(
Decl::Fn(..)
| Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
)),
) => Some(Ordering::Less),
(_, Err(Stmt::Decl(Decl::Fn(..)))) => Some(Ordering::Greater),
_ => Some(Ordering::Less),
}
}) {
return;
}
self.changed = true;
log::debug!("fns: Reordering statements");
let mut fns = vec![];
let mut other = vec![];
for stmt in stmts.take() {
let stmt = stmt.into_module_item();
match stmt {
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(..)))
| ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(..),
..
}))
| ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
})))
| ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl:
Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
..
})) => {
fns.push(T::from_module_item(stmt));
}
_ => {
other.push(T::from_module_item(stmt));
}
}
}
fns.extend(other);
*stmts = fns;
}
}

View File

@ -88,7 +88,7 @@ impl Optimizer<'_> {
match &p.key {
PropName::Str(s) => {
log::trace!(
log::debug!(
"hoist_props: Storing a varaible to inline \
properties"
);
@ -96,7 +96,7 @@ impl Optimizer<'_> {
.insert((name.to_id(), s.value.clone()), value);
}
PropName::Ident(i) => {
log::trace!(
log::debug!(
"hoist_props: Storing a varaible to inline \
properties"
);
@ -118,9 +118,12 @@ impl Optimizer<'_> {
.data
.as_ref()
.and_then(|data| {
data.vars
.get(&name.to_id())
.map(|v| v.ref_count == 1 && v.has_property_access)
data.vars.get(&name.to_id()).map(|v| {
v.ref_count == 1
&& v.has_property_access
&& v.is_fn_local
&& !v.used_in_loop
})
})
.unwrap_or(false)
{
@ -132,6 +135,15 @@ impl Optimizer<'_> {
None => return,
};
match &*init {
Expr::This(..) => {
n.init = Some(init);
return;
}
_ => {}
}
match self.vars_for_prop_hoisting.insert(name.to_id(), init) {
Some(prev) => {
panic!(
@ -140,7 +152,13 @@ impl Optimizer<'_> {
prev
);
}
None => {}
None => {
log::debug!(
"hoist_props: Stored {}{:?} to inline property access",
name.id.sym,
name.id.span.ctxt
);
}
}
}
_ => {}
@ -161,7 +179,7 @@ impl Optimizer<'_> {
if let Some(value) = self.vars_for_prop_hoisting.remove(&obj.to_id()) {
member.obj = ExprOrSuper::Expr(value);
self.changed = true;
log::trace!("hoist_props: Inlined a property");
log::debug!("hoist_props: Inlined a property");
return;
}
@ -174,7 +192,7 @@ impl Optimizer<'_> {
if let Some(value) =
self.simple_props.get(&(obj.to_id(), prop.sym.clone()))
{
log::trace!("hoist_props: Inlining `{}.{}`", obj.sym, prop.sym);
log::debug!("hoist_props: Inlining `{}.{}`", obj.sym, prop.sym);
self.changed = true;
*e = *value.clone()
}

View File

@ -1,9 +1,12 @@
use super::Optimizer;
use crate::compress::optimize::is_pure_undefined;
use crate::debug::dump;
use crate::util::ExprOptExt;
use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::prepend;
use swc_ecma_utils::undefined;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_type;
@ -14,6 +17,216 @@ 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
/// function f(a, b) {
/// if (a) return;
/// console.log(b);
/// }
/// ```
///
/// # Output
/// ```js
/// function f(a, b) {
/// if (!a)
/// console.log(b);
/// }
/// ```
pub(super) fn negate_if_terminate(
&mut self,
stmts: &mut Vec<Stmt>,
handle_return: bool,
handle_continue: bool,
) {
if handle_return {
if !self.options.if_return || !self.options.bools {
return;
}
for s in stmts.iter_mut() {
match s {
Stmt::If(s) => match &mut *s.cons {
Stmt::Block(cons) => {
self.negate_if_terminate(&mut cons.stmts, true, false);
}
_ => {}
},
_ => {}
}
}
}
let len = stmts.len();
let pos_of_if = stmts.iter().enumerate().rposition(|(idx, s)| {
idx != len - 1
&& match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => match &**cons {
Stmt::Return(ReturnStmt { arg: None, .. }) => handle_return,
Stmt::Continue(ContinueStmt { label: None, .. }) => handle_continue,
_ => false,
},
_ => false,
}
});
let pos_of_if = match pos_of_if {
Some(v) => v,
_ => return,
};
self.changed = true;
log::debug!(
"if_return: Negating `foo` in `if (foo) return; bar()` to make it `if (!foo) bar()`"
);
let mut new = vec![];
new.extend(stmts.drain(..pos_of_if));
let cons = stmts.drain(1..).collect::<Vec<_>>();
let if_stmt = stmts.take().into_iter().next().unwrap();
match if_stmt {
Stmt::If(mut s) => {
assert_eq!(s.alt, None);
self.negate(&mut s.test);
s.cons = if cons.len() == 1 {
Box::new(cons.into_iter().next().unwrap())
} else {
Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: cons,
}))
};
new.push(Stmt::If(s))
}
_ => {
unreachable!()
}
}
*stmts = new;
}
pub(super) fn merge_nested_if(&mut self, s: &mut IfStmt) {
if !self.options.conditionals && !self.options.bools {
return;
}
if s.alt.is_some() {
return;
}
match &mut *s.cons {
Stmt::If(IfStmt {
test,
cons,
alt: None,
..
}) => {
self.changed = true;
log::debug!("if_return: Merging nested if statements");
s.test = Box::new(Expr::Bin(BinExpr {
span: s.test.span(),
op: op!("&&"),
left: s.test.take(),
right: test.take(),
}));
s.cons = cons.take();
}
_ => {}
}
}
pub(super) fn merge_else_if(&mut self, s: &mut IfStmt) {
match s.alt.as_deref_mut() {
Some(Stmt::If(IfStmt {
span: span_of_alt,
test: test_of_alt,
cons: cons_of_alt,
alt: Some(alt_of_alt),
..
})) => {
match &**cons_of_alt {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => {}
_ => return,
}
match &mut **alt_of_alt {
Stmt::Block(..) => {}
Stmt::Expr(..) => {
*alt_of_alt = Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: vec![*alt_of_alt.take()],
}));
}
_ => {
return;
}
}
self.changed = true;
log::debug!("if_return: Merging `else if` into `else`");
match &mut **alt_of_alt {
Stmt::Block(alt_of_alt) => {
prepend(
&mut alt_of_alt.stmts,
Stmt::If(IfStmt {
span: *span_of_alt,
test: test_of_alt.take(),
cons: cons_of_alt.take(),
alt: None,
}),
);
}
_ => {
unreachable!()
}
}
s.alt = Some(alt_of_alt.take());
return;
}
_ => {}
}
}
fn merge_nested_if_returns(&mut self, s: &mut Stmt) {
match s {
Stmt::Block(s) => self.merge_if_returns(&mut s.stmts),
Stmt::If(s) => {
self.merge_nested_if_returns(&mut s.cons);
if let Some(alt) = &mut s.alt {
self.merge_nested_if_returns(&mut **alt);
}
}
_ => {}
}
}
/// Merge simple return statements in if statements.
///
/// # Example
@ -34,33 +247,94 @@ impl Optimizer<'_> {
/// return a ? foo() : bar();
/// }
/// ```
pub(super) fn merge_if_returns<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
Vec<T>: VisitWith<ReturnFinder>,
{
if !self.options.if_return || stmts.is_empty() {
pub(super) fn merge_if_returns(&mut self, stmts: &mut Vec<Stmt>) {
if !self.options.if_return {
return;
}
for stmt in stmts.iter_mut() {
self.merge_nested_if_returns(stmt)
}
if stmts.len() <= 1 {
return;
}
let idx_of_not_mergable = stmts.iter().rposition(|stmt| match stmt.as_stmt() {
Some(v) => !self.can_merge_stmt_as_if_return(v),
None => true,
});
let skip = idx_of_not_mergable.map(|v| v + 1).unwrap_or(0);
log::trace!("if_return: Skip = {}", skip);
if stmts.len() <= skip + 1 {
log::trace!("if_return: [x] Aborting because of skip");
return;
}
{
let return_count = count_leaping_returns(&*stmts);
let stmts = &stmts[skip..];
let return_count: usize = stmts.iter().map(|v| count_leaping_returns(v)).sum();
let ends_with_if = stmts
.last()
.map(|stmt| match stmt.as_stmt() {
Some(Stmt::If(..)) => true,
// There's no return statment so merging requires injecting unnecessary `void 0`
if return_count == 0 {
log::trace!("if_return: [x] Aborting because we failed to find return");
return;
}
// If the last statement is a return statement and last - 1 is an if statement
// is without return, we don't need to fold it as `void 0` is too much for such
// cases.
let if_return_count = stmts
.iter()
.filter(|s| match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => always_terminates_with_return_arg(&cons),
_ => false,
})
.unwrap();
if return_count <= 1 && ends_with_if {
return;
.count();
if stmts.len() >= 2 {
match (
&stmts[stmts.len() - 2].as_stmt(),
&stmts[stmts.len() - 1].as_stmt(),
) {
(_, Some(Stmt::If(IfStmt { alt: None, .. }) | Stmt::Expr(..)))
if if_return_count <= 1 =>
{
log::trace!(
"if_return: [x] Aborting because last stmt is a not return stmt"
);
return;
}
(
Some(Stmt::If(IfStmt {
cons, alt: None, ..
})),
Some(Stmt::Return(..)),
) => match &**cons {
Stmt::Return(ReturnStmt { arg: Some(..), .. }) => {}
_ => {
log::trace!(
"if_return: [x] Aborting because stmt before last is an if stmt \
and cons of it is not a return stmt"
);
return;
}
},
_ => {}
}
}
}
{
let start = stmts
.iter()
.skip(skip)
.position(|stmt| match stmt.as_stmt() {
Some(v) => self.can_merge_stmt_as_if_return(v),
None => false,
@ -70,16 +344,16 @@ impl Optimizer<'_> {
let ends_with_mergable = stmts
.last()
.map(|stmt| match stmt.as_stmt() {
Some(Stmt::Return(..)) | Some(Stmt::Expr(..)) => true,
Some(s) => self.can_merge_stmt_as_if_return(s),
_ => false,
})
.unwrap();
if stmts.len() - start == 1 && ends_with_mergable {
if stmts.len() == start + skip + 1 || !ends_with_mergable {
return;
}
let can_merge = stmts.iter().skip(start).all(|stmt| match stmt.as_stmt() {
let can_merge = stmts.iter().skip(skip).all(|stmt| match stmt.as_stmt() {
Some(s) => self.can_merge_stmt_as_if_return(s),
_ => false,
});
@ -88,28 +362,33 @@ impl Optimizer<'_> {
}
}
log::trace!("if_return: Merging returns");
log::debug!("if_return: Merging returns");
if cfg!(feature = "debug") {
let block = BlockStmt {
span: DUMMY_SP,
stmts: stmts.clone(),
};
log::trace!("if_return: {}", dump(&block))
}
self.changed = true;
let mut cur: Option<Box<Expr>> = None;
let mut new = Vec::with_capacity(stmts.len());
for stmt in stmts.take() {
let stmt = match stmt.try_into_stmt() {
Ok(stmt) => {
if !self.can_merge_stmt_as_if_return(&stmt) {
debug_assert_eq!(cur, None);
new.push(T::from_stmt(stmt));
continue;
} else {
stmt
}
}
Err(item) => {
debug_assert_eq!(cur, None);
new.push(item);
for (idx, stmt) in stmts.take().into_iter().enumerate() {
if let Some(not_mergable) = idx_of_not_mergable {
if idx < not_mergable {
new.push(stmt);
continue;
}
}
let stmt = if !self.can_merge_stmt_as_if_return(&stmt) {
debug_assert_eq!(cur, None);
new.push(stmt);
continue;
} else {
stmt
};
let is_nonconditional_return = match stmt {
Stmt::Return(..) => true,
@ -203,17 +482,17 @@ impl Optimizer<'_> {
let expr = self.ignore_return_value(&mut cur);
if let Some(cur) = expr {
new.push(T::from_stmt(Stmt::Expr(ExprStmt {
new.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(cur),
})))
}))
}
}
_ => {
new.push(T::from_stmt(Stmt::Return(ReturnStmt {
new.push(Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(cur),
})));
}));
}
}
}
@ -227,6 +506,11 @@ impl Optimizer<'_> {
fn merge_if_returns_to(&mut self, stmt: Stmt, mut exprs: Vec<Box<Expr>>) -> Expr {
//
match stmt {
Stmt::Block(s) => {
assert_eq!(s.stmts.len(), 1);
self.merge_if_returns_to(s.stmts.into_iter().next().unwrap(), exprs)
}
Stmt::If(IfStmt {
span,
test,
@ -276,9 +560,10 @@ impl Optimizer<'_> {
}
fn can_merge_stmt_as_if_return(&self, s: &Stmt) -> bool {
match s {
let res = match s {
Stmt::Expr(..) | Stmt::Return(..) => true,
Stmt::If(stmt) if self.options.conditionals => {
Stmt::Block(s) => s.stmts.len() == 1 && self.can_merge_stmt_as_if_return(&s.stmts[0]),
Stmt::If(stmt) => {
self.can_merge_stmt_as_if_return(&stmt.cons)
&& stmt
.alt
@ -287,7 +572,12 @@ impl Optimizer<'_> {
.unwrap_or(true)
}
_ => false,
}
};
// if !res {
// log::trace!("Cannot merge: {}", dump(s));
// }
res
}
}
@ -323,3 +613,19 @@ impl Visit for ReturnFinder {
fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
}
fn always_terminates_with_return_arg(s: &Stmt) -> bool {
match s {
Stmt::Return(ReturnStmt { arg: Some(..), .. }) => true,
Stmt::If(IfStmt { cons, alt, .. }) => {
always_terminates_with_return_arg(&cons)
&& alt
.as_deref()
.map(always_terminates_with_return_arg)
.unwrap_or(false)
}
Stmt::Block(s) => s.stmts.iter().any(always_terminates_with_return_arg),
_ => false,
}
}

View File

@ -2,7 +2,6 @@ use super::Optimizer;
use crate::compress::optimize::Ctx;
use crate::util::idents_used_by;
use crate::util::make_number;
use crate::util::IdentUsageCollector;
use fxhash::FxHashMap;
use std::collections::HashMap;
use std::mem::replace;
@ -13,20 +12,17 @@ use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::find_ids;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::undefined;
use swc_ecma_utils::DestructuringFinder;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::ExprFactory;
use swc_ecma_utils::Id;
use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::VisitWith;
/// Methods related to the option `negate_iife`.
impl Optimizer<'_> {
/// Negates iife, while ignore return value.
pub(super) fn negate_iife_ignoring_ret(&mut self, e: &mut Expr) {
if !self.options.negate_iife || self.ctx.in_bang_arg {
if !self.options.negate_iife || self.ctx.in_bang_arg || self.ctx.dont_use_negated_iife {
return;
}
@ -42,7 +38,7 @@ impl Optimizer<'_> {
match callee {
Expr::Fn(..) => {
log::trace!("negate_iife: Negating iife");
log::debug!("negate_iife: Negating iife");
*e = Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
@ -54,38 +50,72 @@ impl Optimizer<'_> {
}
}
/// Returns true if it did any work.
///
///
/// - `iife ? foo : bar` => `!iife ? bar : foo`
pub(super) fn negate_iife_in_cond(&mut self, e: &mut Expr) {
pub(super) fn negate_iife_in_cond(&mut self, e: &mut Expr) -> bool {
let cond = match e {
Expr::Cond(v) => v,
_ => return,
_ => return false,
};
let test_call = match &mut *cond.test {
Expr::Call(e) => e,
_ => return,
_ => return false,
};
let callee = match &mut test_call.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Super(_) => return false,
ExprOrSuper::Expr(e) => &mut **e,
};
match callee {
Expr::Fn(..) => {
log::trace!("negate_iife: Swapping cons and alt");
log::debug!("negate_iife: Swapping cons and alt");
cond.test = Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: cond.test.take(),
}));
swap(&mut cond.cons, &mut cond.alt);
return;
return true;
}
_ => {}
_ => false,
}
}
pub(super) fn restore_negated_iife(&mut self, cond: &mut CondExpr) {
if !self.ctx.dont_use_negated_iife {
return;
}
match &mut *cond.test {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => match &mut **arg {
Expr::Call(CallExpr {
span: call_span,
callee: ExprOrSuper::Expr(callee),
args,
..
}) => match &**callee {
Expr::Fn(..) => {
cond.test = Box::new(Expr::Call(CallExpr {
span: *call_span,
callee: callee.take().as_callee(),
args: args.take(),
type_args: Default::default(),
}));
swap(&mut cond.cons, &mut cond.alt);
}
_ => {}
},
_ => {}
},
_ => {}
};
}
}
/// Methods related to iife.
@ -168,21 +198,33 @@ impl Optimizer<'_> {
if usage.reassigned {
continue;
}
if usage.ref_count != 1 {
continue;
}
}
if let Some(arg) = arg {
// NOTE
//
// This function is misdesigned and should be removed.
// This is wrong because the order of execution is not guaranteed.
match &**arg {
Expr::Ident(..) | Expr::Lit(..) => {}
_ => continue,
}
let should_be_inlined = self.can_be_inlined_for_iife(arg);
if should_be_inlined {
log::trace!(
"iife: Trying to inline {}{:?}",
log::debug!(
"iife: Trying to inline argument ({}{:?})",
param.id.sym,
param.id.span.ctxt
);
vars.insert(param.to_id(), arg.clone());
}
} else {
log::trace!(
"iife: Trying to inline {}{:?} (undefined)",
log::debug!(
"iife: Trying to inline argument ({}{:?}) (undefined)",
param.id.sym,
param.id.span.ctxt
);
@ -206,10 +248,11 @@ impl Optimizer<'_> {
}
}
fn inline_vars_in_node<N>(&mut self, n: &mut N, vars: FxHashMap<Id, Box<Expr>>)
pub(super) fn inline_vars_in_node<N>(&mut self, n: &mut N, vars: FxHashMap<Id, Box<Expr>>)
where
N: VisitMutWith<Self>,
{
log::debug!("inline: inline_vars_in_node");
let ctx = Ctx {
inline_prevented: false,
..self.ctx
@ -249,10 +292,6 @@ impl Optimizer<'_> {
}
}
if self.ctx.inline_prevented {
return;
}
let call = match e {
Expr::Call(v) => v,
_ => return,
@ -267,31 +306,44 @@ impl Optimizer<'_> {
ExprOrSuper::Expr(e) => &mut **e,
};
if call.args.iter().any(|arg| match &*arg.expr {
Expr::Member(MemberExpr {
computed: false, ..
}) => false,
_ => arg.expr.may_have_side_effects(),
}) {
if self.ctx.inline_prevented {
log::trace!("iife: [x] Inline is prevented");
return;
}
match callee {
Expr::Arrow(f) => {
if self.ctx.in_top_level() && !self.ctx.in_call_arg && self.options.negate_iife {
match &f.body {
BlockStmtOrExpr::BlockStmt(body) => {
let has_decl = body.stmts.iter().any(|stmt| match stmt {
Stmt::Decl(..) => true,
_ => false,
});
if has_decl {
return;
}
}
BlockStmtOrExpr::Expr(_) => {}
}
}
if f.params.iter().any(|param| !param.is_ident()) {
return;
}
if is_param_used_by_body(&f.params, &f.body) {
return;
}
let param_ids = f
.params
.iter()
.map(|p| p.clone().ident().unwrap().id)
.collect::<Vec<_>>();
match &mut f.body {
BlockStmtOrExpr::BlockStmt(body) => {
let new = self.inline_fn_like(body);
let new = self.inline_fn_like(&param_ids, body, &mut call.args);
if let Some(new) = new {
self.changed = true;
log::trace!("inline: Inlining a function call (arrow)");
log::debug!("inline: Inlining a function call (arrow)");
*e = new;
}
@ -306,23 +358,6 @@ impl Optimizer<'_> {
_ => {}
},
}
let mut inline_map = HashMap::default();
for (idx, param) in f.params.iter().enumerate() {
let arg_val = if let Some(arg) = call.args.get(idx) {
arg.expr.clone()
} else {
undefined(DUMMY_SP)
};
match param {
Pat::Ident(param) => {
inline_map.insert(param.id.to_id(), arg_val);
}
_ => {}
}
}
self.inline_vars_in_node(&mut f.body, inline_map);
match &mut f.body {
BlockStmtOrExpr::BlockStmt(_) => {
@ -330,17 +365,74 @@ impl Optimizer<'_> {
}
BlockStmtOrExpr::Expr(body) => {
self.changed = true;
log::trace!("inline: Inlining a call to an arrow function");
{
let vars = f
.params
.iter()
.cloned()
.map(|name| VarDeclarator {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
name,
init: Default::default(),
definite: Default::default(),
})
.collect::<Vec<_>>();
if !vars.is_empty() {
self.prepend_stmts.push(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: vars,
})));
}
}
let mut exprs = vec![];
exprs.push(Box::new(make_number(DUMMY_SP, 0.0)));
for (idx, param) in f.params.iter().enumerate() {
if let Some(arg) = call.args.get_mut(idx) {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(param.clone())),
right: arg.expr.take(),
})));
}
}
if call.args.len() > f.params.len() {
for arg in &mut call.args[f.params.len()..] {
exprs.push(arg.expr.take());
}
}
exprs.push(body.take());
log::debug!("inline: Inlining a call to an arrow function");
*e = Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![Box::new(make_number(DUMMY_SP, 0.0)), body.take()],
exprs,
});
return;
}
}
}
Expr::Fn(f) => {
if self.ctx.in_top_level() && !self.ctx.in_call_arg && self.options.negate_iife {
let body = f.function.body.as_ref().unwrap();
let has_decl = body.stmts.iter().any(|stmt| match stmt {
Stmt::Decl(..) => true,
_ => false,
});
if has_decl {
log::trace!("iife: [x] Found decl");
return;
}
}
if f.function.is_generator {
log::trace!("iife: [x] Cannot inline generator");
return;
}
@ -349,17 +441,29 @@ impl Optimizer<'_> {
Pat::Object(..) | Pat::Array(..) | Pat::Assign(..) | Pat::Rest(..) => true,
_ => false,
}) {
log::trace!("iife: [x] Found complex pattern");
return;
}
if let Some(i) = &f.ident {
if idents_used_by(&f.function.body).contains(&i.to_id()) {
log::trace!("iife: [x] Recursive?");
return;
}
}
if is_param_used_by_body(&f.function.params, &f.function.body) {
return;
for arg in &call.args {
if arg.spread.is_some() {
log::trace!("iife: [x] Found spread argument");
return;
}
match &*arg.expr {
Expr::Fn(..) | Expr::Arrow(..) => {
log::trace!("iife: [x] Found callable argument");
return;
}
_ => {}
}
}
let body = f.function.body.as_mut().unwrap();
@ -368,10 +472,22 @@ impl Optimizer<'_> {
return;
}
let new = self.inline_fn_like(body);
let param_ids = f
.function
.params
.iter()
.map(|p| p.pat.clone().ident().unwrap().id)
.collect::<Vec<_>>();
if !self.can_inline_fn_like(&param_ids, body) {
log::trace!("iife: [x] Body is not inliable");
return;
}
let new = self.inline_fn_like(&param_ids, body, &mut call.args);
if let Some(new) = new {
self.changed = true;
log::trace!("inline: Inlining a function call");
log::debug!("inline: Inlining a function call");
*e = new;
}
@ -382,11 +498,39 @@ impl Optimizer<'_> {
}
}
fn inline_fn_like(&mut self, body: &mut BlockStmt) -> Option<Expr> {
fn can_inline_fn_like(&self, param_ids: &[Ident], body: &BlockStmt) -> bool {
if !body.stmts.iter().all(|stmt| match stmt {
Stmt::Expr(e) if e.expr.is_await_expr() => false,
Stmt::Decl(Decl::Var(VarDecl {
kind: VarDeclKind::Var | VarDeclKind::Let,
decls,
..
})) => {
if decls.iter().any(|decl| match decl.name {
Pat::Ident(..) => false,
_ => true,
}) {
return false;
}
if self.ctx.executed_multiple_time {
return false;
}
true
}
Stmt::Expr(e) => match &*e.expr {
Expr::Await(..) => false,
// TODO: Check if paramter is used and inline if call is not related to parameters.
Expr::Call(e) => {
let used = idents_used_by(e);
param_ids.iter().all(|param| !used.contains(&param.to_id()))
}
_ => true,
},
Stmt::Expr(..) => true,
Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
Some(Expr::Await(..)) => false,
@ -401,12 +545,92 @@ impl Optimizer<'_> {
},
_ => false,
}) {
return false;
}
if idents_used_by(&*body)
.iter()
.any(|v| v.0 == js_word!("arguments"))
{
return false;
}
true
}
fn inline_fn_like(
&mut self,
params: &[Ident],
body: &mut BlockStmt,
args: &mut [ExprOrSpread],
) -> Option<Expr> {
if !self.can_inline_fn_like(&params, &*body) {
return None;
}
self.changed = true;
log::debug!("inline: Inling an iife");
let mut exprs = vec![];
for stmt in body.stmts.take() {
{
let vars = params
.iter()
.cloned()
.map(BindingIdent::from)
.map(Pat::Ident)
.map(|name| VarDeclarator {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
name,
init: Default::default(),
definite: Default::default(),
})
.collect::<Vec<_>>();
if !vars.is_empty() {
self.prepend_stmts.push(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: vars,
})));
}
}
for (idx, param) in params.iter().enumerate() {
if let Some(arg) = args.get_mut(idx) {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(Pat::Ident(param.clone().into()))),
right: arg.expr.take(),
})));
}
}
if args.len() > params.len() {
for arg in &mut args[params.len()..] {
exprs.push(arg.expr.take());
}
}
for mut stmt in body.stmts.take() {
match stmt {
Stmt::Decl(Decl::Var(ref mut var)) => {
for decl in &mut var.decls {
if decl.init.is_some() {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(decl.name.clone())),
right: decl.init.take().unwrap(),
})))
}
}
self.prepend_stmts.push(stmt);
}
Stmt::Expr(stmt) => {
exprs.push(stmt.expr);
}
@ -513,29 +737,3 @@ impl Optimizer<'_> {
}
}
}
// We can't remove a function call if a parameter is declared by function and
// the body of the function uses it.
fn is_param_used_by_body<P, B>(params: &P, body: &B) -> bool
where
P: for<'any> VisitWith<DestructuringFinder<'any, Id>>,
B: VisitWith<IdentUsageCollector>,
{
let declared: Vec<Id> = find_ids(params);
let used = idents_used_by(body);
for id in declared {
if used.contains(&id) {
return true;
}
}
for (sym, _) in used {
if sym == js_word!("arguments") {
return true;
}
}
false
}

View File

@ -1,9 +1,13 @@
use crate::compress::optimize::util::class_has_side_effect;
use crate::compress::optimize::util::is_valid_for_lhs;
use crate::debug::dump;
use crate::util::has_mark;
use crate::util::idents_used_by;
use super::Optimizer;
use swc_atoms::js_word;
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;
@ -21,9 +25,18 @@ impl Optimizer<'_> {
None => return,
};
let should_preserve = (!self.options.top_level() && self.options.top_retain.is_empty())
let should_preserve = !has_mark(var.span, self.marks.non_top_level)
&& (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level();
if cfg!(feature = "debug") {
log::trace!(
"inline: store_var_for_inining({}, should_preserve = {:?})",
dump(&var.name),
should_preserve
);
}
if self
.data
.as_ref()
@ -57,13 +70,34 @@ impl Optimizer<'_> {
}
if should_preserve && usage.var_kind != Some(VarDeclKind::Const) {
if cfg!(feature = "debug") {
log::trace!(
"inline: Preserving non-const variable `{}` because it's top-level",
dump(&var.name)
);
}
return;
}
if usage.cond_init || usage.used_above_decl {
if cfg!(feature = "debug") {
log::trace!("inline: [x] It's cond init or used before decl",);
}
return;
}
if !usage.is_fn_local {
match &**init {
Expr::Lit(..) => {}
_ => {
if cfg!(feature = "debug") {
log::trace!("inline: [x] It's not fn-local");
}
return;
}
}
}
if !usage.reassigned {
match &**init {
Expr::Fn(..) | Expr::Arrow(..) => {
@ -95,14 +129,12 @@ impl Optimizer<'_> {
&& (!usage.mutated || (usage.assign_count == 0 && !usage.reassigned))
&& match &**init {
Expr::Lit(lit) => match lit {
Lit::Str(_)
| Lit::Bool(_)
| Lit::Null(_)
| Lit::Num(_)
| Lit::BigInt(_) => true,
Lit::Str(_) => true,
Lit::Bool(_) | Lit::Null(_) | Lit::Num(_) | Lit::BigInt(_) => true,
Lit::Regex(_) => self.options.unsafe_regexp,
_ => false,
},
Expr::This(..) => usage.is_fn_local,
Expr::Arrow(arr) => is_arrow_simple_enough(arr),
_ => false,
}
@ -114,7 +146,7 @@ impl Optimizer<'_> {
_ => true,
}
{
log::trace!(
log::debug!(
"inline: Decided to inline '{}{:?}' because it's simple",
i.id.sym,
i.id.span.ctxt
@ -122,7 +154,7 @@ impl Optimizer<'_> {
self.lits.insert(i.to_id(), init.take());
} else {
log::trace!(
log::debug!(
"inline: Decided to copy '{}{:?}' because it's simple",
i.id.sym,
i.id.span.ctxt
@ -133,6 +165,13 @@ impl Optimizer<'_> {
return;
}
if self.ctx.inline_as_assignment {
if cfg!(feature = "debug") {
log::trace!("inline: [x] inline_as_assignment is true");
}
return;
}
// Single use => inlined
if is_inline_enabled
&& !should_preserve
@ -158,7 +197,11 @@ impl Optimizer<'_> {
_ => {}
}
match &**init {
Expr::Lit(Lit::Regex(..)) => return,
Expr::Lit(Lit::Regex(..)) => {
if !usage.is_fn_local || usage.used_in_loop {
return;
}
}
Expr::This(..) => {
// Don't inline this if it passes function boundaries.
@ -170,17 +213,20 @@ impl Optimizer<'_> {
_ => {}
}
if init.may_have_side_effects() {
if !self
.vars_accessible_without_side_effect
.contains(&i.to_id())
{
// TODO: Inline partially
return;
if usage.used_in_loop {
match &**init {
Expr::Lit(..) | Expr::Ident(..) | Expr::Fn(..) => {}
_ => {
return;
}
}
}
log::trace!(
if init.may_have_side_effects() {
return;
}
log::debug!(
"inline: Decided to inline '{}{:?}' because it's used only once",
i.id.sym,
i.id.span.ctxt
@ -210,6 +256,10 @@ impl Optimizer<'_> {
_ => {}
},
Stmt::Try(TryStmt { block, .. }) => {
return self.is_fn_body_simple_enough_to_inline(block)
}
_ => {}
}
}
@ -234,7 +284,7 @@ impl Optimizer<'_> {
.and_then(|data| data.vars.get(&i.to_id()))
{
if !usage.reassigned {
log::trace!("typeofs: Storing typeof `{}{:?}`", i.sym, i.span.ctxt);
log::debug!("typeofs: Storing typeof `{}{:?}`", i.sym, i.span.ctxt);
match &*decl {
Decl::Fn(..) => {
self.typeofs.insert(i.to_id(), js_word!("function"));
@ -286,6 +336,10 @@ impl Optimizer<'_> {
return;
}
if self.ctx.inline_as_assignment {
return;
}
if let Some(usage) = self
.data
.as_ref()
@ -295,7 +349,7 @@ impl Optimizer<'_> {
return;
}
if usage.reassigned {
if usage.reassigned || usage.inline_prevented {
return;
}
@ -305,14 +359,14 @@ impl Optimizer<'_> {
match &f.function.body {
Some(body) => {
if self.is_fn_body_simple_enough_to_inline(body) {
log::trace!(
log::debug!(
"inline: Decided to inline function '{}{:?}' as it's very \
simple",
f.ident.sym,
f.ident.span.ctxt
);
self.lits.insert(
self.vars_for_inlining.insert(
i.to_id(),
match decl {
Decl::Fn(f) => Box::new(Expr::Fn(FnExpr {
@ -351,14 +405,14 @@ impl Optimizer<'_> {
self.changed = true;
match &decl {
Decl::Class(c) => {
log::trace!(
log::debug!(
"inline: Decided to inline class '{}{:?}' as it's used only once",
c.ident.sym,
c.ident.span.ctxt
);
}
Decl::Fn(f) => {
log::trace!(
log::debug!(
"inline: Decided to inline function '{}{:?}' as it's used only once",
f.ident.sym,
f.ident.span.ctxt
@ -366,6 +420,7 @@ impl Optimizer<'_> {
}
_ => {}
}
self.vars_for_inlining.insert(
i.to_id(),
match decl.take() {
@ -399,33 +454,92 @@ impl Optimizer<'_> {
return;
}
//
if let Some(value) = self.lits.get(&i.to_id()).cloned() {
if let Some(value) = self
.lits
.get(&i.to_id())
.and_then(|v| {
// Prevent infinite recursion.
let ids = idents_used_by(&**v);
if ids.contains(&i.to_id()) {
None
} else {
Some(v)
}
})
.cloned()
{
match &*value {
Expr::Lit(Lit::Num(..)) => {
if self.ctx.in_lhs_of_assign {
return;
}
}
Expr::Member(..) => {
if self.ctx.executed_multiple_time {
return;
}
}
_ => {}
}
self.changed = true;
log::trace!("inline: Replacing a variable with cheap expression");
log::debug!("inline: Replacing a variable with cheap expression");
*e = *value;
} else if let Some(value) = self.vars_for_inlining.get(&i.to_id()) {
return;
}
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;
}
match &**value {
Expr::Member(..) => {
if self.ctx.executed_multiple_time {
return;
}
}
_ => {}
}
}
if self.ctx.inline_as_assignment {
if let Some(value) = self.vars_for_inlining.remove(&i.to_id()) {
self.changed = true;
log::debug!(
"inline: Inlining '{}{:?}' using assignment",
i.sym,
i.span.ctxt
);
*e = Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: op!("="),
left: PatOrExpr::Pat(Box::new(Pat::Ident(i.clone().into()))),
right: value,
});
if cfg!(feature = "debug") {
log::trace!("inline: [Change] {}", dump(&*e))
}
return;
}
}
if let Some(value) = self.vars_for_inlining.get(&i.to_id()) {
self.changed = true;
log::trace!(
log::debug!(
"inline: Replacing '{}{:?}' with an expression",
i.sym,
i.span.ctxt
);
*e = *value.clone();
if cfg!(feature = "debug") {
log::trace!("inline: [Change] {}", dump(&*e))
}
}
}
_ => {}

View File

@ -46,7 +46,7 @@ impl Optimizer<'_> {
}
}
log::trace!("join_vars: Joining variables");
log::debug!("join_vars: Joining variables");
self.changed = true;
let mut cur: Option<VarDecl> = None;

View File

@ -1,5 +1,6 @@
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;
@ -19,7 +20,7 @@ impl Optimizer<'_> {
match s {
Stmt::While(stmt) => {
self.changed = true;
log::trace!("loops: Converting a while loop to a for loop");
log::debug!("loops: Converting a while loop to a for loop");
*s = Stmt::For(ForStmt {
span: stmt.span,
init: None,
@ -32,7 +33,7 @@ impl Optimizer<'_> {
let val = stmt.test.as_pure_bool();
if let Known(true) = val {
self.changed = true;
log::trace!("loops: Converting an always-true do-while loop to a for loop");
log::debug!("loops: Converting an always-true do-while loop to a for loop");
*s = Stmt::For(ForStmt {
span: stmt.span,
@ -79,7 +80,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!("loops: Removing a for loop with instant break");
log::debug!("loops: Removing a for loop with instant break");
self.prepend_stmts
.extend(f.init.take().map(|init| match init {
VarDeclOrExpr::VarDecl(var) => Stmt::Decl(Decl::Var(var)),
@ -97,6 +98,70 @@ 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) {
@ -112,7 +177,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(stmt);
self.changed |= changed;
if changed {
log::trace!(
log::debug!(
"loops: Removing unreachable while statement without side effects"
);
}
@ -120,7 +185,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(&mut w.body);
self.changed |= changed;
if changed {
log::trace!("loops: Removing unreachable body of a while statement");
log::debug!("loops: Removing unreachable body of a while statement");
}
}
}
@ -132,7 +197,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(&mut f.body);
self.changed |= changed;
if changed {
log::trace!("loops: Removing unreachable body of a for statement");
log::debug!("loops: Removing unreachable body of a for statement");
}
self.changed |= f.init.is_some() | f.update.is_some();
@ -154,7 +219,7 @@ impl Optimizer<'_> {
} else if let Known(true) = val {
if purity.is_pure() {
self.changed = true;
log::trace!(
log::debug!(
"loops: Remving `test` part of a for stmt as it's always true"
);
f.test = None;
@ -190,7 +255,7 @@ impl Optimizer<'_> {
} else {
s.init = None;
self.changed = true;
log::trace!(
log::debug!(
"loops: Removed side-effect-free expressions in `init` of a for stmt"
);
}

View File

@ -0,0 +1,38 @@
use super::Optimizer;
use swc_ecma_ast::*;
use unicode_xid::UnicodeXID;
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 s.value.starts_with(|c: char| c.is_xid_start())
&& s.value.chars().all(|c: char| c.is_xid_continue())
&& !s.value.contains("𝒶")
{
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<Stmt>) {
if let Some(Stmt::Return(ReturnStmt { arg: None, .. })) = stmts.last() {
self.changed = true;
log::debug!("misc: Removing useless return");
stmts.pop();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ impl Optimizer<'_> {
let value = if value.is_empty() { 0f64 } else { 1f64 };
self.changed = true;
log::trace!("numbers: Converting a string literal to {:?}", value);
log::debug!("numbers: Converting a string literal to {:?}", value);
*e = Expr::Lit(Lit::Num(Number { span: *span, value }));
return;
}
@ -37,7 +37,7 @@ impl Optimizer<'_> {
..
}) => {
self.changed = true;
log::trace!("numbers: Lifting `-`");
log::debug!("numbers: Lifting `-`");
*e = Expr::Unary(UnaryExpr {
span: arg.span,
@ -55,7 +55,7 @@ impl Optimizer<'_> {
Expr::Lit(Lit::Num(Number { span, value, .. })) => {
if value.is_sign_negative() {
self.changed = true;
log::trace!("numbers: Lifting `-` in a literal");
log::debug!("numbers: Lifting `-` in a literal");
*e = Expr::Unary(UnaryExpr {
span: arg.span,

View File

@ -1,4 +1,8 @@
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::util::make_bool;
use crate::util::ValueExt;
use std::mem::swap;
@ -74,7 +78,7 @@ impl Optimizer<'_> {
match (lt, rt) {
(Type::Undefined, Type::Null) | (Type::Null, Type::Undefined) => {
if op == op!("===") {
log::trace!(
log::debug!(
"Reducing `!== null || !== undefined` check to `!= null`"
);
return Some(BinExpr {
@ -84,7 +88,7 @@ impl Optimizer<'_> {
right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
});
} else {
log::trace!(
log::debug!(
"Reducing `=== null || === undefined` check to `== null`"
);
return Some(BinExpr {
@ -167,7 +171,7 @@ impl Optimizer<'_> {
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);
log::debug!("Reducing comparison of same variable ({})", e.op);
e.op = if e.op == op!("===") {
op!("==")
@ -187,7 +191,7 @@ impl Optimizer<'_> {
if lt == rt {
e.op = op!("==");
self.changed = true;
log::trace!("Reduced `===` to `==` because types of operands are identical")
log::debug!("Reduced `===` to `==` because types of operands are identical")
}
}
}
@ -209,7 +213,7 @@ impl Optimizer<'_> {
let value = if n.op == op!("==") { l == r } else { l != r };
log::trace!("Optimizing: literal comparison => bool");
log::debug!("Optimizing: literal comparison => bool");
self.changed = true;
return Some(Expr::Lit(Lit::Bool(Bool {
span: n.span,
@ -249,7 +253,7 @@ impl Optimizer<'_> {
| Expr::Bin(BinExpr { op: op!(">"), .. }) => {
if let Known(Type::Bool) = arg.get_type() {
self.changed = true;
log::trace!("Optimizing: `!!expr` => `expr`");
log::debug!("Optimizing: `!!expr` => `expr`");
*e = *arg.take();
}
@ -278,9 +282,9 @@ impl Optimizer<'_> {
///
/// TODO: Handle special cases like !1 or !0
pub(super) fn negate(&mut self, e: &mut Expr) {
self.changed = true;
let arg = Box::new(e.take());
let start_str = dump(&*e);
self.changed = true;
match e {
Expr::Bin(bin @ BinExpr { op: op!("=="), .. })
| Expr::Bin(bin @ BinExpr { op: op!("!="), .. })
@ -304,15 +308,68 @@ impl Optimizer<'_> {
}
};
self.changed = true;
log::trace!("negate: binary");
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::trace!("negate: !!bool => !bool");
log::debug!("negate: !!bool => !bool");
*e = *arg.take();
return;
}
@ -321,22 +378,33 @@ impl Optimizer<'_> {
op: op!("instanceof"),
..
}) => {
log::trace!("negate: !bool => bool");
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::trace!("negate: e => !e");
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) {
@ -352,7 +420,7 @@ impl Optimizer<'_> {
if exprs.is_empty() {
return;
}
log::trace!("optimizing negated sequences");
log::debug!("optimizing negated sequences");
{
let last = exprs.last_mut().unwrap();
@ -427,24 +495,60 @@ impl Optimizer<'_> {
_ => return,
};
log::trace!("Compressing: `e = 3 & e` => `e &= 3`");
log::debug!("Compressing: `e = 3 & e` => `e &= 3`");
self.changed = true;
e.op = op;
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::Ident(l), Expr::Ident(r)) => {
self.options.comparisons && (is_for_rel || l.sym > r.sym)
(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::Ident(..) | Expr::Member(..),
Expr::Unary(UnaryExpr {
op: op!("void"), ..
op: op!("void") | op!("!"),
..
}),
)
| (
@ -479,7 +583,7 @@ impl Optimizer<'_> {
}
if self.can_swap_bin_operands(&left, &right, false) {
log::trace!("Swapping operands of binary exprssion");
log::debug!("Swapping operands of binary exprssion");
swap(left, right);
return true;
}
@ -494,7 +598,7 @@ impl Optimizer<'_> {
| 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);
log::debug!("comparisons: Swapping operands of {}", e.op);
e.op = if e.op == op!("<=") {
op!(">=")
@ -568,13 +672,13 @@ impl Optimizer<'_> {
if rb {
self.changed = true;
log::trace!("Optimizing: e && true => !!e");
log::debug!("Optimizing: e && true => !!e");
self.negate_twice(&mut bin.left);
*e = *bin.left.take();
} else {
self.changed = true;
log::trace!("Optimizing: e && false => e");
log::debug!("Optimizing: e && false => e");
*e = *bin.left.take();
}
@ -588,7 +692,7 @@ impl Optimizer<'_> {
if !rb {
self.changed = true;
log::trace!("Optimizing: e || false => !!e");
log::debug!("Optimizing: e || false => !!e");
self.negate_twice(&mut bin.left);
*e = *bin.left.take();
@ -648,7 +752,7 @@ impl Optimizer<'_> {
match l {
Expr::Lit(Lit::Null(..)) => {
log::trace!("Removing null from lhs of ??");
log::debug!("Removing null from lhs of ??");
self.changed = true;
*e = r.take();
return;
@ -658,7 +762,7 @@ impl Optimizer<'_> {
| Expr::Lit(Lit::BigInt(..))
| Expr::Lit(Lit::Bool(..))
| Expr::Lit(Lit::Regex(..)) => {
log::trace!("Removing rhs of ?? as lhs cannot be null nor undefined");
log::debug!("Removing rhs of ?? as lhs cannot be null nor undefined");
self.changed = true;
*e = l.take();
return;
@ -682,7 +786,7 @@ impl Optimizer<'_> {
}) => match &**arg {
Expr::Ident(arg) => {
if let Some(value) = self.typeofs.get(&arg.to_id()).cloned() {
log::trace!(
log::debug!(
"Converting typeof of variable to literal as we know the value"
);
self.changed = true;
@ -697,7 +801,7 @@ impl Optimizer<'_> {
}
Expr::Arrow(..) | Expr::Fn(..) => {
log::trace!("Converting typeof to 'function' as we know the value");
log::debug!("Converting typeof to 'function' as we know the value");
self.changed = true;
*e = Expr::Lit(Lit::Str(Str {
span: *span,
@ -709,7 +813,7 @@ impl Optimizer<'_> {
}
Expr::Array(..) | Expr::Object(..) => {
log::trace!("Converting typeof to 'object' as we know the value");
log::debug!("Converting typeof to 'object' as we know the value");
self.changed = true;
*e = Expr::Lit(Lit::Str(Str {
span: *span,

View File

@ -60,6 +60,10 @@ 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 {
@ -87,7 +91,7 @@ impl Optimizer<'_> {
Prop::Shorthand(_) => {}
Prop::KeyValue(p) => {
if prop_name_eq(&p.key, &key.sym) {
log::trace!("properties: Inlining a key-value property `{}`", key.sym);
log::debug!("properties: Inlining a key-value property `{}`", key.sym);
self.changed = true;
*e = *p.value.clone();
return;

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,7 @@ impl Optimizer<'_> {
match &*args[0].expr {
Expr::Lit(Lit::Str(..)) => {
self.changed = true;
log::trace!(
log::debug!(
"strings: Unsafely reduced `RegExp` call in a string context"
);
@ -87,7 +87,7 @@ impl Optimizer<'_> {
Expr::Lit(Lit::Str(..)) => {
*n = *e.expr.take();
self.changed = true;
log::trace!("string: Removed a paren in a string context");
log::debug!("string: Removed a paren in a string context");
}
_ => {}
}
@ -101,7 +101,7 @@ impl Optimizer<'_> {
let value = n.as_string();
if let Known(value) = value {
self.changed = true;
log::trace!(
log::debug!(
"strings: Converted an expression into a string literal (in string context)"
);
*n = Expr::Lit(Lit::Str(Str {
@ -116,7 +116,7 @@ impl Optimizer<'_> {
match n {
Expr::Lit(Lit::Num(v)) => {
self.changed = true;
log::trace!(
log::debug!(
"strings: Converted a numeric literal ({}) into a string literal (in string \
context)",
v.value
@ -136,7 +136,7 @@ impl Optimizer<'_> {
return;
}
self.changed = true;
log::trace!(
log::debug!(
"strings: Converted a regex (/{}/{}) into a string literal (in string context)",
v.exp,
v.flags
@ -163,7 +163,7 @@ impl Optimizer<'_> {
.unwrap_or(false)
{
self.changed = true;
log::trace!(
log::debug!(
"strings: Converting a reference ({}{:?}) into `undefined` (in string \
context)",
i.sym,
@ -274,7 +274,7 @@ impl Optimizer<'_> {
if let Some(l_last) = l.quasis.last_mut() {
self.changed = true;
log::trace!(
log::debug!(
"template: Concatted a string (`{}`) on rhs of `+` to a template literal",
rs.value
);
@ -294,7 +294,7 @@ impl Optimizer<'_> {
if let Some(r_first) = r.quasis.first_mut() {
self.changed = true;
log::trace!(
log::debug!(
"template: Prepended a string (`{}`) on lhs of `+` to a template literal",
ls.value
);
@ -332,7 +332,7 @@ impl Optimizer<'_> {
debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{:?} is invalid", l);
self.changed = true;
log::trace!("strings: Merged to template literals");
log::debug!("strings: Merged to template literals");
}
_ => {}
@ -365,7 +365,7 @@ impl Optimizer<'_> {
let left_span = left.span;
self.changed = true;
log::trace!(
log::debug!(
"string: Concatting `{} + {}` to `{}`",
second_str,
third_str,
@ -413,7 +413,7 @@ impl Optimizer<'_> {
..
})) => {
self.changed = true;
log::trace!(
log::debug!(
"string: Dropping empty string literal (in lhs) because it \
does not changes type"
);
@ -430,7 +430,7 @@ impl Optimizer<'_> {
..
})) => {
self.changed = true;
log::trace!(
log::debug!(
"string: Dropping empty string literal (in rhs) because it \
does not changes type"
);

View File

@ -1,3 +1,5 @@
use std::mem::take;
use super::Optimizer;
use crate::util::ExprOptExt;
use swc_common::EqIgnoreSpan;
@ -5,7 +7,9 @@ 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::ExprExt;
use swc_ecma_utils::StmtExt;
use swc_ecma_utils::Type;
use swc_ecma_utils::Value::Known;
use swc_ecma_visit::noop_visit_type;
@ -45,6 +49,7 @@ impl Optimizer<'_> {
});
if let Some(case_idx) = matching_case {
let mut var_ids = vec![];
let mut stmts = vec![];
let should_preserve_switch = stmt.cases.iter().skip(case_idx).any(|case| {
@ -60,9 +65,9 @@ impl Optimizer<'_> {
return;
}
log::trace!("switches: Removing unreachable cases from a constant switch");
log::debug!("switches: Removing unreachable cases from a constant switch");
} else {
log::trace!("switches: Removing a constant switch");
log::debug!("switches: Removing a constant switch");
}
self.changed = true;
@ -81,6 +86,21 @@ impl Optimizer<'_> {
}
}
for case in &stmt.cases[..case_idx] {
for cons in &case.cons {
var_ids.extend(
cons.extract_var_ids()
.into_iter()
.map(|name| VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(name.into()),
init: None,
definite: Default::default(),
}),
);
}
}
for case in stmt.cases.iter_mut().skip(case_idx) {
let mut found_break = false;
case.cons.retain(|stmt| match stmt {
@ -110,6 +130,18 @@ impl Optimizer<'_> {
}
}
if !var_ids.is_empty() {
prepend(
&mut stmts,
Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: take(&mut var_ids),
})),
)
}
let inner = if should_preserve_switch {
let mut cases = stmt.cases.take();
let case = SwitchCase {
@ -193,7 +225,7 @@ impl Optimizer<'_> {
if !preserve_cases {
if let Some(last_non_empty) = last_non_empty {
if last_non_empty + 1 != cases.len() {
log::trace!("switches: Removing empty cases at the end");
log::debug!("switches: Removing empty cases at the end");
self.changed = true;
cases.drain(last_non_empty + 1..);
}
@ -203,7 +235,7 @@ impl Optimizer<'_> {
if let Some(last) = cases.last_mut() {
match last.cons.last() {
Some(Stmt::Break(BreakStmt { label: None, .. })) => {
log::trace!("switches: Removing `break` at the end");
log::debug!("switches: Removing `break` at the end");
self.changed = true;
last.cons.pop();
}
@ -215,12 +247,23 @@ impl Optimizer<'_> {
/// If a case ends with break but content is same with the consequtive case
/// except the break statement, we merge them.
fn merge_cases_with_same_cons(&mut self, cases: &mut Vec<SwitchCase>) {
let stop_pos = cases.iter().position(|case| match case.test.as_deref() {
Some(Expr::Update(..)) => true,
_ => false,
});
let mut found = None;
'l: for (li, l) in cases.iter().enumerate().rev() {
if l.cons.is_empty() {
continue;
}
if let Some(stop_pos) = stop_pos {
if li > stop_pos {
continue;
}
}
if let Some(l_last) = l.cons.last() {
match l_last {
Stmt::Break(BreakStmt { label: None, .. }) => {}
@ -253,7 +296,7 @@ impl Optimizer<'_> {
if let Some(idx) = found {
self.changed = true;
log::trace!("switches: Merging cases with same cons");
log::debug!("switches: Merging cases with same cons");
cases[idx].cons.clear();
}
}

View File

@ -1,19 +1,77 @@
use crate::compress::optimize::util::class_has_side_effect;
use crate::option::PureGetterOption;
use super::Optimizer;
use crate::compress::optimize::util::class_has_side_effect;
use crate::debug::dump;
use crate::option::PureGetterOption;
use crate::util::has_mark;
use swc_atoms::js_word;
use swc_common::Span;
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_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<T>(&mut self, stmts: &mut Vec<T>)
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() {
@ -22,7 +80,7 @@ impl Optimizer<'_> {
op: op!("void"),
arg,
})) => {
log::trace!("unused: Removing `return void` in end of a function");
log::debug!("unused: Removing `return void` in end of a function");
self.changed = true;
*s = Stmt::Expr(ExprStmt {
span: *span,
@ -48,7 +106,7 @@ impl Optimizer<'_> {
..
}) => {}
_ => {
self.drop_unused_vars(&mut var.name, Some(init));
self.drop_unused_vars(var.span, &mut var.name, Some(init));
if var.name.is_invalid() {
let side_effects = self.ignore_return_value(init);
@ -62,7 +120,7 @@ impl Optimizer<'_> {
}
},
None => {
self.drop_unused_vars(&mut var.name, var.init.as_deref_mut());
self.drop_unused_vars(var.span, &mut var.name, var.init.as_deref_mut());
}
}
}
@ -87,30 +145,53 @@ impl Optimizer<'_> {
return;
}
self.take_pat_if_unused(pat, None)
self.take_pat_if_unused(DUMMY_SP, pat, None)
}
pub(super) fn drop_unused_vars(&mut self, name: &mut Pat, init: Option<&mut Expr>) {
if !self.options.unused || self.ctx.in_var_decl_of_for_in_or_of_loop || self.ctx.is_exported
{
return;
pub(super) fn drop_unused_vars(
&mut self,
var_declarator_span: Span,
name: &mut Pat,
init: Option<&mut Expr>,
) {
let has_mark = has_mark(var_declarator_span, self.marks.non_top_level);
if !has_mark {
if !self.options.unused
|| self.ctx.in_var_decl_of_for_in_or_of_loop
|| self.ctx.is_exported
{
return;
}
}
if cfg!(feature = "debug") {
log::trace!("unused: drop_unused_vars({})", dump(&*name));
}
// 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;
if !has_mark {
match self.ctx.var_kind {
Some(VarDeclKind::Var) => {
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving `var` `{}` because it's top-level",
dump(&*name)
);
}
return;
}
}
}
Some(VarDeclKind::Let) | Some(VarDeclKind::Const) => {
if !self.options.top_level() && self.ctx.is_top_level_for_block_level_vars() {
return;
Some(VarDeclKind::Let) | Some(VarDeclKind::Const) => {
if !self.options.top_level() && self.ctx.is_top_level_for_block_level_vars() {
return;
}
}
None => {}
}
None => {}
}
if let Some(scope) = self
@ -127,12 +208,12 @@ impl Optimizer<'_> {
return;
}
self.take_pat_if_unused(name, init);
self.take_pat_if_unused(var_declarator_span, 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);
self.take_pat_if_unused(DUMMY_SP, &mut param.pat, None);
if !param.pat.is_invalid() {
return;
@ -140,7 +221,13 @@ impl Optimizer<'_> {
}
}
pub(super) fn take_pat_if_unused(&mut self, name: &mut Pat, mut init: Option<&mut Expr>) {
/// `parent_span` should be [Span] of [VarDeclarator] or [AssignExpr]
pub(super) fn take_pat_if_unused(
&mut self,
parent_span: Span,
name: &mut Pat,
mut init: Option<&mut Expr>,
) {
let had_value = init.is_some();
let can_drop_children = had_value;
@ -153,7 +240,9 @@ impl Optimizer<'_> {
match name {
Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) {
if !has_mark(parent_span, self.marks.non_top_level)
&& self.options.top_retain.contains(&i.id.sym)
{
return;
}
@ -165,7 +254,7 @@ impl Optimizer<'_> {
.unwrap_or(false)
{
self.changed = true;
log::trace!(
log::debug!(
"unused: Dropping a variable '{}{:?}' because it is not used",
i.id.sym,
i.id.span.ctxt
@ -173,6 +262,10 @@ impl Optimizer<'_> {
// This will remove variable.
name.take();
return;
} else {
if cfg!(feature = "debug") {
log::trace!("unused: Cannot drop ({}) becaue it's used", dump(&*i));
}
}
}
@ -188,7 +281,7 @@ impl Optimizer<'_> {
.as_mut()
.and_then(|expr| self.access_numeric_property(expr, idx));
self.take_pat_if_unused(p, elem);
self.take_pat_if_unused(parent_span, p, elem);
}
None => {}
}
@ -224,7 +317,7 @@ impl Optimizer<'_> {
continue;
}
self.take_pat_if_unused(&mut p.value, prop);
self.take_pat_if_unused(parent_span, &mut p.value, prop);
}
ObjectPatProp::Assign(_) => {}
ObjectPatProp::Rest(_) => {}
@ -303,7 +396,7 @@ impl Optimizer<'_> {
.unwrap_or(false)
{
self.changed = true;
log::trace!(
log::debug!(
"unused: Dropping a decl '{}{:?}' because it is not used",
ident.sym,
ident.span.ctxt
@ -327,7 +420,14 @@ impl Optimizer<'_> {
}
pub(super) fn drop_unused_assignments(&mut self, e: &mut Expr) {
if !self.options.unused {
let assign = match e {
Expr::Assign(e) => e,
_ => return,
};
let has_mark = has_mark(assign.span, self.marks.non_top_level);
if !has_mark && !self.options.unused {
return;
}
@ -344,19 +444,36 @@ impl Optimizer<'_> {
return;
}
if (!self.options.top_level() && self.options.top_retain.is_empty())
if cfg!(feature = "debug") {
log::trace!(
"unused: drop_unused_assignments: Target: `{}`",
dump(&assign.left)
)
}
if !has_mark
&& (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because it's top-level",
dump(&assign.left)
)
}
return;
}
let assign = match e {
Expr::Assign(e) => e,
_ => return,
};
match &mut assign.left {
PatOrExpr::Expr(_) => return,
PatOrExpr::Expr(_) => {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because it's an expression",
dump(&assign.left)
)
}
return;
}
PatOrExpr::Pat(left) => match &**left {
Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) {
@ -369,7 +486,7 @@ impl Optimizer<'_> {
.and_then(|data| data.vars.get(&i.to_id()))
{
if var.is_fn_local && var.usage_count == 0 {
log::trace!(
log::debug!(
"unused: Dropping assignment to var '{}{:?}', which is never used",
i.id.sym,
i.id.span.ctxt
@ -377,6 +494,14 @@ impl Optimizer<'_> {
self.changed = true;
*e = *assign.right.take();
return;
} else {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because of usage: {:?}",
dump(&assign.left),
var
)
}
}
}
}
@ -405,7 +530,7 @@ impl Optimizer<'_> {
if can_remove_ident {
self.changed = true;
log::trace!("Removing ident of an class / function expression");
log::debug!("Removing ident of an class / function expression");
*name = None;
}
}
@ -429,7 +554,7 @@ impl Optimizer<'_> {
}
self.changed = true;
log::trace!(
log::debug!(
"unused: Removing the name of a function expression because it's not used by \
it'"
);

View File

@ -12,6 +12,12 @@ use swc_ecma_utils::prop_name_eq;
use swc_ecma_utils::ExprExt;
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,
@ -224,6 +230,40 @@ 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,

View File

@ -41,6 +41,10 @@ pub(crate) fn dump<N>(node: &N) -> String
where
N: swc_ecma_codegen::Node + Clone + VisitMutWith<DropSpan> + VisitMutWith<Debugger>,
{
if !cfg!(feature = "debug") {
return String::new();
}
let mut node = node.clone();
node.visit_mut_with(&mut Debugger);
node = drop_span(node);
@ -109,5 +113,5 @@ pub(crate) fn invoke(module: &Module) {
);
}
log::debug!("[SWC_RUN]\n{}", String::from_utf8_lossy(&output.stdout))
log::info!("[SWC_RUN]\n{}", String::from_utf8_lossy(&output.stdout))
}

View File

@ -14,6 +14,7 @@
use crate::compress::compressor;
use crate::hygiene::unique_marker;
use crate::marks::Marks;
use crate::option::ExtraOptions;
use crate::option::MinifyOptions;
use crate::pass::compute_char_freq::compute_char_freq;
@ -23,9 +24,13 @@ 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::single::single_pass_optimizer;
use crate::pass::precompress::precompress_optimizer;
use analyzer::analyze;
use pass::postcompress::postcompress_optimizer;
use std::time::Instant;
use swc_common::comments::Comments;
use swc_common::sync::Lrc;
use swc_common::SourceMap;
use swc_ecma_ast::Module;
use swc_ecma_visit::FoldWith;
use swc_ecma_visit::VisitMutWith;
@ -35,6 +40,7 @@ mod analyzer;
mod compress;
mod debug;
mod hygiene;
mod marks;
pub mod option;
mod pass;
pub mod timing;
@ -43,11 +49,15 @@ mod util;
#[inline]
pub fn optimize(
mut m: Module,
cm: Lrc<SourceMap>,
comments: Option<&dyn Comments>,
mut timings: Option<&mut Timings>,
options: &MinifyOptions,
extra: &ExtraOptions,
) -> Module {
let marks = Marks::new();
let start = Instant::now();
if let Some(defs) = options.compress.as_ref().map(|c| &c.global_defs) {
// Apply global defs.
//
@ -60,10 +70,13 @@ pub fn optimize(
m.visit_mut_with(&mut global_defs::globals_defs(defs, extra.top_level_mark));
}
}
log::info!("global_defs took {:?}", Instant::now() - start);
m.visit_mut_with(&mut single_pass_optimizer(
options.compress.clone().unwrap_or_default(),
));
if let Some(options) = &options.compress {
let start = Instant::now();
m.visit_mut_with(&mut precompress_optimizer(options.clone()));
log::info!("precompress took {:?}", Instant::now() - start);
}
m.visit_mut_with(&mut unique_marker());
@ -95,8 +108,14 @@ pub fn optimize(
t.section("compress");
}
if let Some(options) = &options.compress {
m = m.fold_with(&mut compressor(&options, comments));
let start = Instant::now();
m = m.fold_with(&mut compressor(cm.clone(), marks, &options, comments));
log::info!("compressor took {:?}", Instant::now() - start);
// Again, we don't need to validate ast
let start = Instant::now();
m.visit_mut_with(&mut postcompress_optimizer(options));
log::info!("postcompressor took {:?}", Instant::now() - start);
}
if let Some(ref mut _t) = timings {

View File

@ -0,0 +1,18 @@
use swc_common::Mark;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Marks {
/// [Mark] applied to non-top level varaibles which is injected while
/// inlining.
pub(crate) non_top_level: Mark,
}
impl Marks {
pub fn new() -> Marks {
fn m() -> Mark {
Mark::fresh(Mark::root())
}
Marks { non_top_level: m() }
}
}

View File

@ -55,6 +55,9 @@ pub struct MangleOptions {
#[serde(default, alias = "keep_fnames")]
pub keep_fn_names: bool,
#[serde(default, alias = "keep_private_props")]
pub keep_private_props: bool,
#[serde(default, alias = "ie8")]
pub ie8: bool,
@ -171,6 +174,7 @@ pub struct CompressOptions {
#[serde(alias = "hoist_vars")]
pub hoist_vars: bool,
/// No effect.
#[serde(default)]
#[serde(alias = "ie8")]
pub ie8: bool,

View File

@ -40,6 +40,21 @@ impl HygieneAnalyzer<'_> {
let ret = op(self);
let mut ids = vec![];
{
let scope = self.scope();
for (sym, ctxts) in &scope.declared_symbols {
if ctxts.len() == 1 {
let id = (sym.clone(), *ctxts.iter().next().unwrap());
ids.push(id)
}
}
}
for id in ids {
self.hygiene.preserved.remove(&id);
self.hygiene.modified.insert(id);
}
self.cur_scope = old;
ret

View File

@ -69,7 +69,7 @@ impl VisitMut for Optimizer {
}
fn visit_mut_module(&mut self, n: &mut Module) {
log::debug!("hygiene: Analyzing span hygiene");
log::info!("hygiene: Analyzing span hygiene");
let start = Instant::now();
let mut analyzer = HygieneAnalyzer {
@ -82,12 +82,12 @@ impl VisitMut for Optimizer {
self.hygiene = analyzer.hygiene;
let end = Instant::now();
log::debug!("hygiene: Span hygiene analysis took {:?}", end - start);
log::info!("hygiene: Span hygiene analysis took {:?}", end - start);
let start = end;
log::debug!("hygiene: Optimizing span hygiene");
log::info!("hygiene: Optimizing span hygiene");
n.visit_mut_children_with(self);
let end = Instant::now();
log::debug!("hygiene: Span hygiene optimiation took {:?}", end - start);
log::info!("hygiene: Span hygiene optimiation took {:?}", end - start);
}
}

View File

@ -28,9 +28,12 @@ pub fn name_mangler(options: MangleOptions, _char_freq_info: CharFreqInfo) -> im
struct Mangler {
options: MangleOptions,
n: usize,
private_n: usize,
preserved: FxHashSet<Id>,
preserved_symbols: FxHashSet<JsWord>,
renamed: FxHashMap<Id, JsWord>,
renamed_private: FxHashMap<Id, JsWord>,
data: Option<ProgramData>,
}
@ -65,6 +68,23 @@ impl Mangler {
break;
}
}
fn rename_private(&mut self, private_name: &mut PrivateName) {
let id = private_name.id.to_id();
let new_sym = if let Some(cached) = self.renamed_private.get(&id) {
cached.clone()
} else {
let sym: JsWord = base54(self.private_n).into();
self.private_n += 1;
self.renamed_private.insert(id.clone(), sym.clone());
sym
};
private_name.id.sym = new_sym;
}
}
impl VisitMut for Mangler {
@ -95,6 +115,12 @@ impl VisitMut for Mangler {
}
}
fn visit_mut_private_name(&mut self, private_name: &mut PrivateName) {
if !self.options.keep_private_props {
self.rename_private(private_name);
}
}
fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) {
self.rename(&mut n.ident);
n.function.visit_mut_with(self);

View File

@ -6,8 +6,8 @@ use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use swc_atoms::JsWord;
use swc_ecma_ast::{
CallExpr, Expr, ExprOrSuper, Ident, KeyValueProp, Lit, MemberExpr, Module, PrivateName, Prop,
PropName, Str, StrKind,
CallExpr, Expr, ExprOrSuper, Ident, KeyValueProp, Lit, MemberExpr, Module, Prop, PropName, Str,
StrKind,
};
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{VisitMut, VisitMutWith};
@ -37,11 +37,9 @@ struct ManglePropertiesState {
// Cache of already mangled names
cache: HashMap<JsWord, JsWord>,
private_cache: HashMap<JsWord, JsWord>,
// Numbers to pass to base54()
n: usize,
private_n: usize,
}
impl ManglePropertiesState {
@ -103,23 +101,6 @@ impl ManglePropertiesState {
None
}
}
fn gen_private_name(&mut self, name: &JsWord) -> JsWord {
// Always mangleable
if let Some(cached) = self.private_cache.get(&name) {
cached.clone()
} else {
let private_n = self.private_n;
self.private_n += 1;
let mangled_name: JsWord = base54(private_n).into();
self.private_cache
.insert(name.clone(), mangled_name.clone());
mangled_name
}
}
}
pub fn mangle_properties<'a>(m: &mut Module, options: ManglePropertiesOptions) {
@ -309,8 +290,4 @@ impl VisitMut for Mangler<'_> {
}
}
}
fn visit_mut_private_name(&mut self, private_name: &mut PrivateName) {
private_name.id.sym = self.state.gen_private_name(&private_name.id.sym);
}
}

View File

@ -4,4 +4,5 @@ pub mod global_defs;
pub mod hygiene;
pub mod mangle_names;
pub mod mangle_props;
pub mod single;
pub mod postcompress;
pub mod precompress;

View File

@ -0,0 +1,76 @@
use crate::option::CompressOptions;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
pub fn postcompress_optimizer<'a>(options: &'a CompressOptions) -> impl 'a + VisitMut {
PostcompressOptimizer { options }
}
struct PostcompressOptimizer<'a> {
options: &'a CompressOptions,
}
impl PostcompressOptimizer<'_> {
fn optimize_in_bool_ctx(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
}
// Note: `||` is not handled because of precedence.
match e {
Expr::Bin(BinExpr {
op: op @ op!("&&"),
right,
left,
..
}) => {
match &**left {
Expr::Bin(BinExpr { op: op!("&&"), .. }) => return,
_ => {}
}
match &mut **right {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) if arg.is_ident() => {
let new_op = if *op == op!("&&") {
op!("||")
} else {
op!("&&")
};
log::debug!(
"bools: `(a {} !b)` => `(a {} b)` (in bool context)",
*op,
new_op
);
*op = new_op;
*right = arg.take();
return;
}
_ => {}
}
}
_ => {}
}
}
}
impl VisitMut for PostcompressOptimizer<'_> {
noop_visit_mut_type!();
fn visit_mut_cond_expr(&mut self, e: &mut CondExpr) {
e.visit_mut_children_with(self);
self.optimize_in_bool_ctx(&mut *e.test);
}
fn visit_mut_if_stmt(&mut self, s: &mut IfStmt) {
s.visit_mut_children_with(self);
self.optimize_in_bool_ctx(&mut *s.test);
}
}

View File

@ -9,10 +9,11 @@ use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::{ident::IdentLike, Id};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
/// Optimizer invoked before invoking compressor.
///
/// - Remove parens.
pub fn single_pass_optimizer(options: CompressOptions) -> impl VisitMut {
SinglePassOptimizer {
pub fn precompress_optimizer(options: CompressOptions) -> impl VisitMut {
PrecompressOptimizer {
options,
data: Default::default(),
fn_decl_count: Default::default(),
@ -21,7 +22,7 @@ pub fn single_pass_optimizer(options: CompressOptions) -> impl VisitMut {
}
#[derive(Debug, Default)]
struct SinglePassOptimizer {
struct PrecompressOptimizer {
options: CompressOptions,
data: ProgramData,
fn_decl_count: FxHashMap<Id, usize>,
@ -33,7 +34,7 @@ struct Ctx {
in_var_pat: bool,
}
impl VisitMut for SinglePassOptimizer {
impl VisitMut for PrecompressOptimizer {
noop_visit_mut_type!();
fn visit_mut_decl(&mut self, n: &mut Decl) {

View File

@ -9,6 +9,7 @@ use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id;
use swc_ecma_utils::ModuleItemLike;
use swc_ecma_utils::StmtLike;
use swc_ecma_utils::Value;
use swc_ecma_visit::noop_visit_type;
@ -26,6 +27,55 @@ pub(crate) fn make_number(span: Span, value: f64) -> Expr {
Expr::Lit(Lit::Num(Number { span, value }))
}
pub trait MoudleItemExt: StmtLike + ModuleItemLike {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt>;
fn from_module_item(item: ModuleItem) -> Self;
fn into_module_item(self) -> ModuleItem {
match self.into_module_decl() {
Ok(v) => ModuleItem::ModuleDecl(v),
Err(v) => ModuleItem::Stmt(v),
}
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt>;
}
impl MoudleItemExt for Stmt {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt> {
Err(self)
}
fn from_module_item(item: ModuleItem) -> Self {
item.expect_stmt()
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt> {
Err(self)
}
}
impl MoudleItemExt for ModuleItem {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt> {
match self {
ModuleItem::ModuleDecl(v) => Ok(v),
ModuleItem::Stmt(v) => Err(v),
}
}
fn from_module_item(item: ModuleItem) -> Self {
item
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt> {
match self {
ModuleItem::ModuleDecl(v) => Ok(v),
ModuleItem::Stmt(v) => Err(v),
}
}
}
///
/// - `!0` for true
/// - `!1` for false
@ -45,6 +95,18 @@ pub(crate) trait ExprOptExt: Sized {
fn as_expr(&self) -> &Expr;
fn as_mut(&mut self) -> &mut Expr;
fn first_expr_mut(&mut self) -> &mut Expr {
let expr = self.as_mut();
match expr {
Expr::Seq(seq) => seq
.exprs
.first_mut()
.expect("Sequence expressions should have at least one element")
.first_expr_mut(),
expr => expr,
}
}
/// This returns itself for normal expressions and returns last exprssions
/// for sequence expressions.
fn value_mut(&mut self) -> &mut Expr {
@ -279,11 +341,36 @@ where
#[derive(Default)]
pub(crate) struct IdentUsageCollector {
ids: FxHashSet<Id>,
ignore_nested: bool,
}
impl Visit for IdentUsageCollector {
noop_visit_type!();
fn visit_block_stmt_or_expr(&mut self, n: &BlockStmtOrExpr, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_constructor(&mut self, n: &Constructor, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_function(&mut self, n: &Function, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_ident(&mut self, n: &Ident, _: &dyn Node) {
self.ids.insert(n.to_id());
}
@ -295,13 +382,37 @@ impl Visit for IdentUsageCollector {
n.prop.visit_with(n, self);
}
}
fn visit_prop_name(&mut self, n: &PropName, _: &dyn Node) {
match n {
PropName::Computed(..) => {
n.visit_children_with(self);
}
_ => {}
}
}
}
pub(crate) fn idents_used_by<N>(n: &N) -> FxHashSet<Id>
where
N: VisitWith<IdentUsageCollector>,
{
let mut v = IdentUsageCollector::default();
let mut v = IdentUsageCollector {
ignore_nested: false,
..Default::default()
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.ids
}
pub(crate) fn idents_used_by_ignoring_nested<N>(n: &N) -> FxHashSet<Id>
where
N: VisitWith<IdentUsageCollector>,
{
let mut v = IdentUsageCollector {
ignore_nested: true,
..Default::default()
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.ids
}

View File

@ -1,3 +1,5 @@
extern crate swc_node_base;
use ansi_term::Color;
use anyhow::bail;
use anyhow::Context;
@ -11,6 +13,7 @@ use std::panic::catch_unwind;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::time::Instant;
use swc_common::comments::SingleThreadedComments;
use swc_common::errors::Handler;
use swc_common::sync::Lrc;
@ -132,6 +135,8 @@ fn run(
let top_level_mark = Mark::fresh(Mark::root());
let minification_start = Instant::now();
let lexer = Lexer::new(
Default::default(),
Default::default(),
@ -155,8 +160,10 @@ fn run(
_ => return None,
};
let optimization_start = Instant::now();
let output = optimize(
program,
cm.clone(),
Some(&comments),
None,
&MinifyOptions {
@ -174,9 +181,22 @@ fn run(
..Default::default()
},
&ExtraOptions { top_level_mark },
)
.fold_with(&mut hygiene())
.fold_with(&mut fixer(None));
);
let end = Instant::now();
log::info!(
"optimize({}) took {:?}",
input.display(),
end - optimization_start
);
let output = output.fold_with(&mut hygiene()).fold_with(&mut fixer(None));
let end = Instant::now();
log::info!(
"process({}) took {:?}",
input.display(),
end - minification_start
);
Some(output)
}
@ -199,8 +219,52 @@ fn stdout_of(code: &str) -> Result<String, Error> {
Ok(String::from_utf8_lossy(&actual_output.stdout).to_string())
}
fn find_config(dir: &Path) -> String {
let mut cur = Some(dir);
while let Some(dir) = cur {
let config = dir.join("config.json");
if config.exists() {
let config = read_to_string(&config).expect("failed to read config.json");
return config;
}
cur = dir.parent();
}
panic!("failed to find config file for {}", dir.display())
}
#[testing::fixture("tests/compress/fixture/**/input.js")]
fn base_fixture(input: PathBuf) {
let dir = input.parent().unwrap();
let config = find_config(&dir);
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()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
println!("{}", input.display());
NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js"))
.unwrap();
Ok(())
})
.unwrap()
}
#[testing::fixture("tests/projects/files/*.js")]
fn projects(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");
@ -213,14 +277,19 @@ fn base_fixture(input: PathBuf) {
None => return Ok(()),
};
let output = print(cm.clone(), &[output_module.clone()]);
let output = print(cm.clone(), &[output_module.clone()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
println!("{}", input.display());
NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js"))
.compare_to_file(
dir.parent()
.unwrap()
.join("output")
.join(input.file_name().unwrap()),
)
.unwrap();
Ok(())
@ -241,7 +310,7 @@ fn base_exec(input: PathBuf) {
let output = run(cm.clone(), &handler, &input, &config, None);
let output = output.expect("Parsing in base test should not fail");
let output = print(cm.clone(), &[output]);
let output = print(cm.clone(), &[output], false);
eprintln!(
"---- {} -----\n{}",
@ -297,7 +366,7 @@ fn fixture(input: PathBuf) {
None => return Ok(()),
};
let output = print(cm.clone(), &[output_module.clone()]);
let output = print(cm.clone(), &[output_module.clone()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
@ -327,7 +396,7 @@ fn fixture(input: PathBuf) {
ModuleItem::Stmt(Stmt::Empty(..)) => false,
_ => true,
});
print(cm.clone(), &[expected])
print(cm.clone(), &[expected], false)
};
if output == expected {
@ -368,7 +437,7 @@ fn fixture(input: PathBuf) {
});
}
let output_str = print(cm.clone(), &[drop_span(output_module.clone())]);
let output_str = print(cm.clone(), &[drop_span(output_module.clone())], false);
assert_eq!(DebugUsingDisplay(&output_str), DebugUsingDisplay(&expected));
@ -377,12 +446,12 @@ fn fixture(input: PathBuf) {
.unwrap()
}
fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N]) -> String {
fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N], minify: bool) -> String {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Default::default(),
cfg: swc_ecma_codegen::Config { minify },
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),

View File

@ -0,0 +1,10 @@
function isUndefined(
value
) {
return "undefined" == typeof value;
}
function isDefined(
value
) {
return "undefined" != typeof value;
}

View File

@ -0,0 +1,6 @@
function isUndefined(value) {
return void 0 === value;
}
function isDefined(value) {
return void 0 !== value;
}

View File

@ -0,0 +1,12 @@
var h = destination.$$hashKey;
forEach(
destination,
function (
value, key
) {
delete destination[key];
}
);
for (var key in source) destination[key] = copy(
source[key]
);

View File

@ -0,0 +1,4 @@
var h = destination.$$hashKey;
for(var key in forEach(destination, function(value, key) {
delete destination[key];
}), source)destination[key] = copy(source[key]);

View File

@ -0,0 +1,5 @@
if (foo) {
if (bar) {
var baz = console.log('Foo bar');
}
}

View File

@ -0,0 +1 @@
if (foo && bar) var baz = console.log("Foo bar");

View File

@ -0,0 +1,15 @@
export const obj = {
eq: function (
jqLite,
index
) {
return index >= 0
? jqLite(
this[index]
)
: jqLite(
this[this.length + index]
);
},
}

View File

@ -0,0 +1,5 @@
export const obj = {
eq: function(jqLite, index) {
return jqLite(index >= 0 ? this[index] : this[this.length + index]);
}
};

View File

@ -0,0 +1,24 @@
function forEach(obj, iterator, context) {
var key;
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key);
}
}
} else if (obj.forEach && obj.forEach !== forEach) {
obj.forEach(iterator, context);
} else if (isArrayLike(obj)) {
for (key = 0; key < obj.length; key++)
iterator.call(context, obj[key], key);
} else {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key);
}
}
}
}
return obj;
}

View File

@ -0,0 +1,8 @@
function forEach(obj, iterator, context) {
var key;
if (obj) if (isFunction(obj)) for(key in obj)"prototype" != key && "length" != key && "name" != key && obj.hasOwnProperty(key) && iterator.call(context, obj[key], key);
else if (obj.forEach && obj.forEach !== forEach) obj.forEach(iterator, context);
else if (isArrayLike(obj)) for(key = 0; key < obj.length; key++)iterator.call(context, obj[key], key);
else for(key in obj)obj.hasOwnProperty(key) && iterator.call(context, obj[key], key);
return obj;
}

View File

@ -0,0 +1,2 @@
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');

View File

@ -0,0 +1,2 @@
var _ = root._;
_ || "undefined" == typeof require || (_ = require("underscore"));

View File

@ -0,0 +1,15 @@
const obj = {
_ensureElement: function () {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
}

View File

@ -0,0 +1,12 @@
const obj = {
_ensureElement: function() {
if (this.el) this.setElement(_.result(this, "el"), !1);
else {
var attrs = _.extend({
}, _.result(this, "attributes"));
this.id && (attrs.id = _.result(this, "id")), this.className && (attrs.class = _.result(this, "className"));
var $el = Backbone.$("<" + _.result(this, "tagName") + ">").attr(attrs);
this.setElement($el, !1);
}
}
};

View File

@ -0,0 +1,8 @@
const obj = {
navigate: function (fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = { trigger: !!options };
}
}

View File

@ -0,0 +1,8 @@
const obj = {
navigate: function(fragment, options) {
if (!History.started) return !1;
options && !0 !== options || (options = {
trigger: !!options
});
}
};

View File

@ -0,0 +1,6 @@
var names, i, l;
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
}

View File

@ -0,0 +1,2 @@
var names, i, l;
for(i = 0, l = (names = name ? [name] : _.keys(this._events)).length; i < l; i++)name = names[i];

View File

@ -0,0 +1,10 @@
export const obj = {
_validate: function (attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, { validationError: error }));
return false;
}
}

View File

@ -0,0 +1,11 @@
export const obj = {
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return !0;
attrs = _.extend({
}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
return !error || (this.trigger("invalid", this, error, _.extend(options, {
validationError: error
})), !1);
}
};

View File

@ -0,0 +1,68 @@
export const obj = {
set: function (key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
}

View File

@ -0,0 +1,18 @@
export const obj = {
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (null == key) return this;
if ("object" == typeof key ? (attrs = key, options = val) : (attrs = {
})[key] = val, options || (options = {
}), !this._validate(attrs, options)) return !1;
for(attr in unset = options.unset, silent = options.silent, changes = [], changing = this._changing, this._changing = !0, changing || (this._previousAttributes = _.clone(this.attributes), this.changed = {
}), current = this.attributes, prev = this._previousAttributes, this.idAttribute in attrs && (this.id = attrs[this.idAttribute]), attrs)val = attrs[attr], _.isEqual(current[attr], val) || changes.push(attr), _.isEqual(prev[attr], val) ? delete this.changed[attr] : this.changed[attr] = val, unset ? delete current[attr] : current[attr] = val;
if (!silent) {
changes.length && (this._pending = !0);
for(var i = 0, l = changes.length; i < l; i++)this.trigger("change:" + changes[i], this, current[changes[i]], options);
}
if (changing) return this;
if (!silent) for(; this._pending;)this._pending = !1, this.trigger("change", this, options);
return this._pending = !1, this._changing = !1, this;
}
};

View File

@ -0,0 +1,12 @@
export const obj = {
changedAttributes: function (diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
}
}

View File

@ -0,0 +1,9 @@
export const obj = {
changedAttributes: function(diff) {
if (!diff) return !!this.hasChanged() && _.clone(this.changed);
var val, changed = !1, old = this._changing ? this._previousAttributes : this.attributes;
for(var attr in diff)_.isEqual(old[attr], val = diff[attr]) || ((changed || (changed = {
}))[attr] = val);
return changed;
}
};

View File

@ -0,0 +1,23 @@
export const obj = {
remove: function (models, options) {
var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = models[i] = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return singular ? models[0] : models;
},
}

View File

@ -0,0 +1,8 @@
export const obj = {
remove: function(models, options) {
var i, l, index, model, singular = !_.isArray(models);
for(options || (options = {
}), i = 0, l = (models = singular ? [models] : _.clone(models)).length; i < l; i++)(model = models[i] = this.get(models[i])) && (delete this._byId[model.id], delete this._byId[model.cid], index = this.indexOf(model), this.models.splice(index, 1), this.length--, options.silent || (options.index = index, model.trigger("remove", model, this, options)), this._removeReference(model));
return singular ? models[0] : models;
}
};

View File

@ -0,0 +1,15 @@
export const obj = {
create: function (model, options) {
options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false;
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function (model, resp, options) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
model.save(null, options);
return model;
},
}

View File

@ -0,0 +1,11 @@
export const obj = {
create: function(model, options) {
if (options = options ? _.clone(options) : {
}, !(model = this._prepareModel(model, options))) return !1;
options.wait || this.add(model, options);
var collection = this, success = options.success;
return options.success = function(model, resp, options) {
options.wait && collection.add(model, options), success && success(model, resp, options);
}, model.save(null, options), model;
}
};

View File

@ -0,0 +1,11 @@
export const obj = {
_routeToRegExp: function (route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function (match, optional) {
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
}
}

View File

@ -0,0 +1,7 @@
export const obj = {
_routeToRegExp: function(route) {
return route = route.replace(escapeRegExp, "\\$&").replace(optionalParam, "(?:$1)?").replace(namedParam, function(match, optional) {
return optional ? match : "([^/]+)";
}).replace(splatParam, "(.*?)"), new RegExp("^" + route + "$");
}
};

View File

@ -0,0 +1,40 @@
export const obj = {
navigate: function (fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = { trigger: !!options };
var url = this.root + (fragment = this.getFragment(fragment || ''));
// Strip the fragment of the query and hash for matching.
fragment = fragment.replace(pathStripper, '');
if (this.fragment === fragment) return;
this.fragment = fragment;
// Don't include a trailing slash on the root.
if (fragment === '' && url !== '/') url = url.slice(0, -1);
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if (!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
if (options.trigger) return this.loadUrl(fragment);
},
}

View File

@ -0,0 +1,18 @@
export const obj = {
navigate: function(fragment, options) {
if (!History.started) return !1;
options && !0 !== options || (options = {
trigger: !!options
});
var url = this.root + (fragment = this.getFragment(fragment || ""));
if (fragment = fragment.replace(pathStripper, ""), this.fragment !== fragment) {
if (this.fragment = fragment, "" === fragment && "/" !== url && (url = url.slice(0, -1)), this._hasPushState) this.history[options.replace ? "replaceState" : "pushState"]({
}, document.title, url);
else {
if (!this._wantsHashChange) return this.location.assign(url);
this._updateHash(this.location, fragment, options.replace), this.iframe && fragment !== this.getFragment(this.getHash(this.iframe)) && (options.replace || this.iframe.document.open().close(), this._updateHash(this.iframe.location, fragment, options.replace));
}
if (options.trigger) return this.loadUrl(fragment);
}
}
};

View File

@ -0,0 +1,13 @@
function foo() {
if (!eventsApi(this, "on", name, [callback, context]) || !callback)
return this;
return (
this._events || (this._events = {}),
(this._events[name] || (this._events[name] = [])).push({
callback: callback,
context: context,
ctx: context || this,
}),
this
);
}

View File

@ -0,0 +1,9 @@
function foo() {
return eventsApi(this, "on", name, [callback,
context]) && callback && (this._events || (this._events = {
}), (this._events[name] || (this._events[name] = [])).push({
callback: callback,
context: context,
ctx: context || this
})), this;
}

View File

@ -0,0 +1,92 @@
export const E = {
set: function (models, options) {
options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options);
var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : _.clone(models);
var i, l, id, model, attrs, existing, sort;
var at = options.at;
var targetModel = this.model;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = models.length; i < l; i++) {
attrs = models[i];
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute];
}
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(id)) {
if (remove) modelMap[existing.cid] = true;
if (merge) {
attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
if (order) order.push(existing || model);
}
// Remove nonexistent models if appropriate.
if (remove) {
for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
if (toRemove.length) this.remove(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
}
}
// Silently sort the collection if appropriate.
if (sort) this.sort({ silent: true });
// Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
for (i = 0, l = toAdd.length; i < l; i++) {
(model = toAdd[i]).trigger('add', model, this, options);
}
if (sort || (order && order.length)) this.trigger('sort', this, options);
}
// Return the added (or merged) model (or models).
return singular ? models[0] : models;
}
}

View File

@ -0,0 +1,35 @@
export const E = {
set: function(models, options) {
(options = _.defaults({
}, options, setOptions)).parse && (models = this.parse(models, options));
var 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 = {
}, 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;
else if (add) {
if (!(model = models[i] = this._prepareModel(attrs, options))) continue;
toAdd.push(model), model.on("all", this._onModelEvent, this), this._byId[model.cid] = model, null != model.id && (this._byId[model.id] = model);
}
order && order.push(existing || model);
}
if (remove) {
for(i = 0, l = this.length; i < l; ++i)modelMap[(model = this.models[i]).cid] || toRemove.push(model);
toRemove.length && this.remove(toRemove, options);
}
if (toAdd.length || order && order.length) if (sortable && (sort = !0), this.length += toAdd.length, null != at) for(i = 0, l = toAdd.length; i < l; i++)this.models.splice(at + i, 0, toAdd[i]);
else {
order && (this.models.length = 0);
var orderedModels = order || toAdd;
for(i = 0, l = orderedModels.length; i < l; i++)this.models.push(orderedModels[i]);
}
if (sort && this.sort({
silent: !0
}), !options.silent) {
for(i = 0, l = toAdd.length; i < l; i++)(model = toAdd[i]).trigger("add", model, this, options);
(sort || order && order.length) && this.trigger("sort", this, options);
}
return singular ? models[0] : models;
}
};

View File

@ -0,0 +1,4 @@
if (!name && !callback && !context) {
console.log('foo')
}

View File

@ -0,0 +1 @@
name || callback || context || console.log("foo");

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