feat(es/minifier): Implement minifier partially (#1302)

Co-authored-by: Fábio Santos <fabiosantosart@gmail.com>
This commit is contained in:
강동윤 2021-05-20 13:51:30 +09:00 committed by GitHub
parent b6589af92b
commit c6b22c57f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6904 changed files with 65882 additions and 47 deletions

4
.github/.prettierrc vendored Normal file
View File

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}

View File

@ -117,6 +117,7 @@ jobs:
- swc_ecma_dep_graph
- swc_ecma_ext_transforms
- swc_ecma_loader
- swc_ecma_minifier
- swc_ecma_parser
- swc_ecma_preset_env
- swc_ecma_transforms

4
.gitmodules vendored
View File

@ -1,3 +1,7 @@
[submodule "ecmascript/test262-parser-tests"]
path = ecmascript/parser/tests/test262-parser
url = https://github.com/tc39/test262-parser-tests.git
shallow = true
[submodule "vendor/terser"]
path = vendor/terser
url = https://github.com/terser/terser.git

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"tabWidth": 4,
"useTabs": false
}

View File

@ -21,6 +21,19 @@ impl EqIgnoreSpan for Span {
}
}
impl<T> EqIgnoreSpan for [T]
where
T: EqIgnoreSpan,
{
fn eq_ignore_span(&self, other: &Self) -> bool {
self.len() == other.len()
&& self
.iter()
.zip(other.iter())
.all(|(a, b)| a.eq_ignore_span(b))
}
}
impl<T> EqIgnoreSpan for Option<T>
where
T: EqIgnoreSpan,

View File

@ -14,6 +14,7 @@ all-features = true
[features]
codegen = ["swc_ecma_codegen"]
dep_graph = ["swc_ecma_dep_graph"]
minifier = ["swc_ecma_minifier"]
parser = ["swc_ecma_parser"]
transforms = ["swc_ecma_transforms"]
utils = ["swc_ecma_utils"]
@ -30,6 +31,7 @@ typescript = ["swc_ecma_transforms/typescript"]
swc_ecma_ast = {version = "0.45.0", path = "./ast"}
swc_ecma_codegen = {version = "0.55.0", path = "./codegen", optional = true}
swc_ecma_dep_graph = {version = "0.25.0", path = "./dep-graph", optional = true}
swc_ecma_minifier = {version = "0.1.0-beta.0", path = "./minifier", optional = true}
swc_ecma_parser = {version = "0.57.0", path = "./parser", optional = true}
swc_ecma_transforms = {version = "0.50.0", path = "./transforms", optional = true}
swc_ecma_utils = {version = "0.36.0", path = "./utils", optional = true}

View File

@ -0,0 +1,35 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Ecmascript code minifier."
documentation = "https://swc.rs/rustdoc/swc_ecma_minifier/"
edition = "2018"
include = ["Cargo.toml", "src/**/*.rs"]
license = "Apache-2.0/MIT"
name = "swc_ecma_minifier"
repository = "https://github.com/swc-project/swc.git"
version = "0.1.0-beta.0"
[dependencies]
fxhash = "0.2.1"
log = "0.4"
once_cell = "1.5.2"
pretty_assertions = "0.6.1"
regex = "1.5.3"
retain_mut = "0.1.2"
serde = {version = "1.0.118", features = ["derive"]}
serde_json = "1.0.61"
serde_regex = "1.1.0"
swc_atoms = {version = "0.2", path = "../../atoms"}
swc_common = {version = "0.10.8", path = "../../common"}
swc_ecma_ast = {version = "0.45.0", path = "../ast"}
swc_ecma_codegen = {version = "0.55.0", path = "../codegen"}
swc_ecma_transforms = {version = "0.50.0", path = "../transforms/", features = ["optimization"]}
swc_ecma_transforms_base = {version = "0.15.0", path = "../transforms/base"}
swc_ecma_utils = {version = "0.36.0", path = "../utils"}
swc_ecma_visit = {version = "0.31.0", path = "../visit"}
[dev-dependencies]
ansi_term = "0.12.1"
swc_ecma_parser = {version = "0.57.0", path = "../parser"}
testing = {version = "0.10.2", path = "../../testing"}
walkdir = "2.3.1"

View File

@ -0,0 +1,7 @@
# Minifier
EcmaScript minifier for the swc project. This is basically a port of terser.
## 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,15 @@
#!/usr/bin/env bash
#
# This script automatically add tests to golden.
# Note that this is append-only.
set -eu
cargo test --test compress \
| grep 'js .\.\. ok$' \
| sed -e 's!test fixture_terser__compress__!!' \
| sed -e 's! ... ok!!' \
| sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \
>> tests/golden.txt
./scripts/sort.sh

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eu
cargo test --test compress $1 \
| grep 'js .\.\. FAILED$' \
| sed -e 's!test fixture_terser__compress__!!' \
| sed -e 's! ... FAILED!!' \
| sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \
>> tests/ignored.txt
./scripts/sort.sh

View File

@ -0,0 +1,47 @@
// Run this script to build jsprops.json
const fs = require('fs')
// Compatibility fix for some standard defined globals not defined on every js environment
var new_globals = ["Symbol", "Map", "Promise", "Proxy", "Reflect", "Set", "WeakMap", "WeakSet"];
var objects = {};
var global_ref = typeof global === "object" ? global : self;
new_globals.forEach(function (new_global) {
objects[new_global] = global_ref[new_global] || new Function();
});
const addedProps = new Set();
const add = propName => addedProps.add(propName);
[
"null",
"true",
"false",
"NaN",
"Infinity",
"-Infinity",
"undefined",
].forEach(add);
[ Object, Array, Function, Number,
String, Boolean, Error, Math,
Date, RegExp, objects.Symbol, ArrayBuffer,
DataView, decodeURI, decodeURIComponent,
encodeURI, encodeURIComponent, eval, EvalError,
Float32Array, Float64Array, Int8Array, Int16Array,
Int32Array, isFinite, isNaN, JSON, objects.Map, parseFloat,
parseInt, objects.Promise, objects.Proxy, RangeError, ReferenceError,
objects.Reflect, objects.Set, SyntaxError, TypeError, Uint8Array,
Uint8ClampedArray, Uint16Array, Uint32Array, URIError,
objects.WeakMap, objects.WeakSet
].forEach((ctor) => {
Object.getOwnPropertyNames(ctor).map(add);
if (ctor.prototype) {
Object.getOwnPropertyNames(ctor.prototype).map(add);
}
});
const propsJSON = JSON.stringify([...addedProps].sort(), null, 4)
fs.writeFileSync(__dirname + '/../src/lists/jsprops.json', propsJSON)

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
#
# This script exists to prevent regression.
#
# This script invoked tests two time.
# For first, it runs tests in no-regression mode.
# In the mode, only tests listed in tests/golden.txt will be tested.
#
# For second invokation, arguments are passed to cargo test so the user can
# filter tests.
#
set -eu
if [ -z "$@" ]; then
./scripts/sort.sh
export RUST_LOG=swc_ecma_minifier=trace
GOLDEN_ONLY=1 cargo test --test compress
fi
cargo test --test compress $@

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -eu
# Fix sorting differences between Linux and Mac
export LC_ALL=C
cat tests/golden.txt | awk NF | sort | uniq | awk '{$1=$1};1' | uniq | sort > tests/golden_sorted.txt
mv tests/golden_sorted.txt tests/golden.txt
cat tests/ignored.txt | awk NF | sort | uniq | awk '{$1=$1};1' | uniq | sort > tests/ignored_sorted.txt
mv tests/ignored_sorted.txt tests/ignored.txt

View File

@ -0,0 +1,517 @@
import * as AST from "../lib/ast.js";
import { Compressor } from "../lib/compress/index.js";
import { OutputStream } from "../lib/output.js";
import { parse } from "../lib/parse.js";
import { mangle_properties, reserve_quoted_keys } from "../lib/propmangle.js";
import { base54 } from "../lib/scope.js";
import { defaults, string_template } from "../lib/utils/index.js";
import { minify } from "../main.js";
import * as sandbox from "./sandbox.js"
import assert from "assert";
import fs, { mkdirSync } from "fs";
import path from "path";
import semver from "semver";
import { fileURLToPath } from "url";
/* globals module, __dirname, console */
import "source-map-support/register.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
var tests_dir = __dirname;
var failed_files = {};
var minify_options = JSON.parse(fs.readFileSync(path.join(__dirname, "ufuzz.json"), 'utf-8')).map(JSON.stringify);
run_compress_tests().catch(e => {
console.error(e);
process.exit(1);
});
/* -----[ utils ]----- */
function HOP(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
function tmpl() {
return string_template.apply(this, arguments);
}
function log() {
var txt = tmpl.apply(this, arguments);
console.log("%s", txt);
}
function log_directory(dir) {
log("*** Entering [{dir}]", { dir: dir });
}
function log_start_file(file) {
log("--- {file}", { file: file });
}
function log_test(name) {
log(" Running test [{name}]", { name: name });
}
function find_test_files(dir) {
var files = fs.readdirSync(dir).filter(function (name) {
return /\.js$/i.test(name);
});
if (process.argv.length > 2) {
var x = process.argv.slice(2);
files = files.filter(function (f) {
return x.includes(f);
});
}
return files;
}
function test_directory(dir) {
return path.resolve(tests_dir, dir);
}
function as_toplevel(input, mangle_options) {
if (!(input instanceof AST.AST_BlockStatement))
throw new Error("Unsupported input syntax");
for (var i = 0; i < input.body.length; i++) {
var stat = input.body[i];
if (stat instanceof AST.AST_SimpleStatement && stat.body instanceof AST.AST_String)
input.body[i] = new AST.AST_Directive(stat.body);
else break;
}
var toplevel = new AST.AST_Toplevel(input);
toplevel.figure_out_scope(mangle_options);
return toplevel;
}
async function run_compress_tests() {
var failures = 0;
var dir = test_directory("compress");
log_directory("compress");
var files = find_test_files(dir);
async function test_file(file) {
log_start_file(file);
async function test_case(test) {
log_test(test.name);
const dir = path.join(__dirname, `../../../ecmascript/minifier/tests/terser/compress/${file.replace('.js', '')}/${test.name}`);
console.log(dir)
mkdirSync(dir, { recursive: true })
var output_options = test.beautify || {};
var expect;
if (test.expect) {
expect = make_code(as_toplevel(test.expect, test.mangle), output_options);
} else {
expect = test.expect_exact;
}
fs.writeFileSync(path.join(dir, 'output.js'), expect || '');
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(test.options, undefined, 4));
if (test.expect_error && (test.expect || test.expect_exact || test.expect_stdout)) {
log("!!! Test cannot have an `expect_error` with other expect clauses\n", {});
return false;
}
if (test.input instanceof AST.AST_SimpleStatement
&& test.input.body instanceof AST.AST_TemplateString) {
if (test.input.body.segments.length == 1) {
try {
var input = parse(test.input.body.segments[0].value);
} catch (ex) {
if (!test.expect_error) {
log("!!! Test is missing an `expect_error` clause\n", {});
return false;
}
if (test.expect_error instanceof AST.AST_SimpleStatement
&& test.expect_error.body instanceof AST.AST_Object) {
var expect_error = eval("(" + test.expect_error.body.print_to_string() + ")");
var ex_normalized = JSON.parse(JSON.stringify(ex));
ex_normalized.name = ex.name;
for (var prop in expect_error) {
if (prop == "name" || HOP(expect_error, prop)) {
if (expect_error[prop] != ex_normalized[prop]) {
log("!!! Failed `expect_error` property `{prop}`:\n\n---expect_error---\n{expect_error}\n\n---ACTUAL exception--\n{actual_ex}\n\n", {
prop: prop,
expect_error: JSON.stringify(expect_error, null, 2),
actual_ex: JSON.stringify(ex_normalized, null, 2),
});
return false;
}
}
}
return true;
}
log("!!! Test `expect_error` clause must be an object literal\n---expect_error---\n{expect_error}\n\n", {
expect_error: test.expect_error.print_to_string(),
});
return false;
}
var input_code = make_code(input, output_options);
var input_formatted = test.input.body.segments[0].value;
} else {
log("!!! Test input template string cannot use unescaped ${} expressions\n---INPUT---\n{input}\n\n", {
input: test.input.body.print_to_string(),
});
return false;
}
} else if (test.expect_error) {
log("!!! Test cannot have an `expect_error` clause without a template string `input`\n", {});
return false;
} else {
var input = as_toplevel(test.input, test.mangle);
var input_code = make_code(input, output_options);
var input_formatted = make_code(test.input, {
ecma: 2015,
beautify: true,
quote_style: 3,
keep_quoted_props: true
});
}
try {
fs.writeFileSync(path.join(dir, 'input.js'), input_code);
parse(input_code);
} catch (ex) {
log("!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n", {
input: input_formatted,
error: ex,
});
return false;
}
var ast = input.to_mozilla_ast();
var mozilla_options = {
ecma: output_options.ecma,
ascii_only: output_options.ascii_only,
comments: false,
};
var ast_as_string = AST.AST_Node.from_mozilla_ast(ast).print_to_string(mozilla_options);
var input_string = input.print_to_string(mozilla_options);
if (input_string !== ast_as_string) {
log("!!! Mozilla AST I/O corrupted input\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n\n", {
input: input_string,
output: ast_as_string,
});
return false;
}
var options = defaults(test.options, {});
if (test.mangle) {
fs.writeFileSync(path.join(dir, 'mangle.json'), JSON.stringify(test.mangle));
}
if (test.mangle && test.mangle.properties && test.mangle.properties.keep_quoted) {
var quoted_props = test.mangle.properties.reserved;
if (!Array.isArray(quoted_props)) quoted_props = [];
test.mangle.properties.reserved = quoted_props;
if (test.mangle.properties.keep_quoted !== "strict") {
reserve_quoted_keys(input, quoted_props);
}
}
if (test.rename) {
input.figure_out_scope(test.mangle);
input.expand_names(test.mangle);
}
var cmp = new Compressor(options, {
false_by_default: options.defaults === undefined ? true : !options.defaults,
mangle_options: test.mangle
});
var output = cmp.compress(input);
output.figure_out_scope(test.mangle);
if (test.mangle) {
base54.reset();
output.compute_char_frequency(test.mangle);
(function (cache) {
if (!cache) return;
if (!("props" in cache)) {
cache.props = new Map();
} else if (!(cache.props instanceof Map)) {
const props = new Map();
for (const key in cache.props) {
if (HOP(cache.props, key) && key.charAt(0) === "$") {
props.set(key.substr(1), cache.props[key]);
}
}
cache.props = props;
}
})(test.mangle.cache);
output.mangle_names(test.mangle);
if (test.mangle.properties) {
output = mangle_properties(output, test.mangle.properties);
}
}
output = make_code(output, output_options);
if (test.expect_stdout && typeof expect == "undefined") {
fs.writeFileSync(path.join(dir, 'expected.stdout'), test.expect_stdout);
// Do not verify generated `output` against `expect` or `expect_exact`.
// Rely on the pending `expect_stdout` check below.
} else if (expect != output && !process.env.TEST_NO_COMPARE) {
log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", {
input: input_formatted,
output: output,
expected: expect
});
return false;
}
try {
parse(output);
} catch (ex) {
log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", {
input: input_formatted,
output: output,
error: ex.stack,
});
return false;
}
if (test.expect_stdout
&& (!test.node_version || semver.satisfies(process.version, test.node_version))
&& !process.env.TEST_NO_SANDBOX
) {
var stdout = sandbox.run_code(input_code, test.prepend_code);
if (test.expect_stdout === true) {
test.expect_stdout = stdout;
}
if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", {
input: input_formatted,
expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: test.expect_stdout,
actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
actual: stdout,
});
return false;
}
stdout = sandbox.run_code(output, test.prepend_code);
if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", {
input: input_formatted,
output: output,
expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: test.expect_stdout,
actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
actual: stdout,
});
return false;
}
if (test.reminify && !await reminify(test, input_code, input_formatted)) {
return false;
}
}
return true;
}
var tests = parse_test(path.resolve(dir, file));
for (var i in tests) if (tests.hasOwnProperty(i)) {
if (!await test_case(tests[i])) {
failures++;
failed_files[file] = 1;
if (process.env.TEST_FAIL_FAST) return false;
}
}
return true;
}
for (const file of files) {
if (!await test_file(file)) {
break;
}
}
if (failures) {
console.error("\n!!! Failed " + failures + " test cases.");
console.error("!!! " + Object.keys(failed_files).join(", "));
process.exit(1);
}
}
function parse_test(file) {
var script = fs.readFileSync(file, "utf8");
// TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS2/issues/348
try {
var ast = parse(script, {
filename: file
});
} catch (e) {
console.log("Caught error while parsing tests in " + file + "\n");
console.log(e);
throw e;
}
var tests = {};
var tw = new AST.TreeWalker(function (node, descend) {
if (
node instanceof AST.AST_LabeledStatement
&& tw.parent() instanceof AST.AST_Toplevel
) {
var name = node.label.name;
if (name in tests) {
throw new Error('Duplicated test name "' + name + '" in ' + file);
}
tests[name] = get_one_test(name, node.body);
return true;
}
if (!(node instanceof AST.AST_Toplevel)) croak(node);
});
ast.walk(tw);
return tests;
function croak(node) {
throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
file: file,
line: node.start.line,
col: node.start.col,
code: make_code(node, { beautify: false })
}));
}
function read_boolean(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
if (body instanceof AST.AST_Boolean) {
return body.value;
}
}
throw new Error("Should be boolean");
}
function read_string(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
switch (body.TYPE) {
case "String":
return body.value;
case "Array":
return body.elements.map(function (element) {
if (element.TYPE !== "String")
throw new Error("Should be array of strings");
return element.value;
}).join("\n");
}
}
throw new Error("Should be string or array of strings");
}
function get_one_test(name, block) {
var test = {
name: name,
options: {},
reminify: true,
};
var tw = new AST.TreeWalker(function (node, descend) {
if (node instanceof AST.AST_Assign) {
if (!(node.left instanceof AST.AST_SymbolRef)) {
croak(node);
}
var name = node.left.name;
test[name] = evaluate(node.right);
return true;
}
if (node instanceof AST.AST_LabeledStatement) {
var label = node.label;
assert.ok(
[
"input",
"prepend_code",
"expect",
"expect_error",
"expect_exact",
"expect_stdout",
"node_version",
"reminify",
].includes(label.name),
tmpl("Unsupported label {name} [{line},{col}]", {
name: label.name,
line: label.start.line,
col: label.start.col
})
);
var stat = node.body;
if (label.name == "expect_exact" || label.name == "node_version") {
test[label.name] = read_string(stat);
} else if (label.name == "reminify") {
var value = read_boolean(stat);
test.reminify = value == null || value;
} else if (label.name == "expect_stdout") {
var body = stat.body;
if (body instanceof AST.AST_Boolean) {
test[label.name] = body.value;
} else if (body instanceof AST.AST_Call) {
var ctor = global[body.expression.name];
assert.ok(ctor === Error || ctor.prototype instanceof Error, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
test[label.name] = ctor.apply(null, body.args.map(function (node) {
assert.ok(node instanceof AST.AST_Constant, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
return node.value;
}));
} else {
test[label.name] = read_string(stat) + "\n";
}
} else if (label.name === "prepend_code") {
test[label.name] = read_string(stat);
} else {
test[label.name] = stat;
}
return true;
}
});
block.walk(tw);
return test;
}
}
function make_code(ast, options) {
var stream = OutputStream(options);
ast.print(stream);
return stream.get();
}
function evaluate(code) {
if (code instanceof AST.AST_Node)
code = make_code(code, { beautify: true });
return new Function("return(" + code + ")")();
}
// Try to reminify original input with standard options
// to see if it matches expect_stdout.
async function reminify(test, input_code, input_formatted) {
if (process.env.TEST_NO_REMINIFY) return true;
const { options: orig_options, expect_stdout } = test;
for (var i = 0; i < minify_options.length; i++) {
var options = JSON.parse(minify_options[i]);
options.keep_fnames = orig_options.keep_fnames;
options.keep_classnames = orig_options.keep_classnames;
if (orig_options.compress) {
options.compress.keep_classnames = orig_options.compress.keep_classnames;
options.compress.keep_fargs = orig_options.compress.keep_fargs;
options.compress.keep_fnames = orig_options.compress.keep_fnames;
}
if (orig_options.mangle) {
options.mangle.keep_classnames = orig_options.mangle.keep_classnames;
options.mangle.keep_fnames = orig_options.mangle.keep_fnames;
}
var options_formatted = JSON.stringify(options, null, 4);
var result = await minify(input_code, options);
if (result.error) {
log("!!! failed input reminify\n---INPUT---\n{input}\n--ERROR---\n{error}\n\n", {
input: input_formatted,
error: result.error.stack,
});
return false;
} else if (!process.env.TEST_NO_SANDBOX) {
var stdout = sandbox.run_code(result.code, test.prepend_code);
if (typeof expect_stdout != "string" && typeof stdout != "string" && expect_stdout.name == stdout.name) {
stdout = expect_stdout;
}
if (!sandbox.same_stdout(expect_stdout, stdout)) {
log("!!! failed running reminified input\n---INPUT---\n{input}\n---OPTIONS---\n{options}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", {
input: input_formatted,
options: options_formatted,
output: result.code,
expected_type: typeof expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: expect_stdout,
actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
actual: stdout,
});
return false;
}
}
}
return true;
}

View File

@ -0,0 +1,58 @@
use swc_ecma_ast::VarDeclKind;
use super::UsageAnalyzer;
use std::ops::Deref;
use std::ops::DerefMut;
impl UsageAnalyzer {
pub(super) fn with_ctx(&mut self, ctx: Ctx) -> WithCtx {
let orig_ctx = self.ctx;
self.ctx = ctx;
WithCtx {
analyzer: self,
orig_ctx,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct Ctx {
pub var_decl_kind_of_pat: Option<VarDeclKind>,
pub in_pat_of_var_decl: bool,
pub in_pat_of_var_decl_with_init: bool,
pub in_pat_of_param: bool,
pub in_catch_param: bool,
pub in_left_of_for_loop: bool,
pub in_loop: bool,
/// Are we handling argument of an update exprssion.
pub in_update_arg: bool,
pub in_assign_lhs: bool,
pub in_cond: bool,
}
pub(super) struct WithCtx<'a> {
analyzer: &'a mut UsageAnalyzer,
orig_ctx: Ctx,
}
impl Deref for WithCtx<'_> {
type Target = UsageAnalyzer;
fn deref(&self) -> &Self::Target {
&self.analyzer
}
}
impl DerefMut for WithCtx<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.analyzer
}
}
impl Drop for WithCtx<'_> {
fn drop(&mut self) {
self.analyzer.ctx = self.orig_ctx;
}
}

View File

@ -0,0 +1,600 @@
use self::ctx::Ctx;
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use std::collections::hash_map::Entry;
use swc_atoms::JsWord;
use swc_common::SyntaxContext;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
mod ctx;
/// TODO: Track assignments to variables via `arguments`.
/// TODO: Scope-local. (Including block)
pub(crate) fn analyze<N>(n: &N) -> ProgramData
where
N: VisitWith<UsageAnalyzer>,
{
let mut v = UsageAnalyzer {
data: Default::default(),
scope: Default::default(),
ctx: Default::default(),
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
let top_scope = v.scope;
v.data.top.merge(top_scope);
v.data
}
#[derive(Debug, Default)]
pub(crate) struct VarUsageInfo {
/// # of reference to this identifier.
pub ref_count: usize,
/// `true` if a varaible is conditionally initialized.
pub cond_init: bool,
/// `false` if it's only used.
pub declared: bool,
/// `true` if the enclosing function defines this variable as a parameter.
pub declared_as_fn_param: bool,
pub assign_count: usize,
pub usage_count: usize,
pub reassigned: bool,
pub has_property_access: bool,
pub accessed_props: FxHashSet<JsWord>,
pub exported: bool,
/// True if used **above** the declaration. (Not eval order).
pub used_above_decl: bool,
/// `true` if it's declared by function parameters or variables declared in
/// a closest function and used only within it and not used by child
/// functions.
pub is_fn_local: bool,
pub used_in_loop: bool,
pub var_kind: Option<VarDeclKind>,
pub declared_as_catch_param: bool,
/// TODO: Implement this.
///
/// Indicates a variable or function is overrided without using it.
pub overriden_without_used: bool,
/// In `c = b`, `b` inffects `c`.
infects: Vec<Id>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScopeKind {
Fn,
Block,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct ScopeData {
pub has_with_stmt: bool,
pub has_eval_call: bool,
}
impl ScopeData {
fn merge(&mut self, other: ScopeData) {
self.has_with_stmt |= other.has_with_stmt;
self.has_eval_call |= other.has_eval_call;
}
}
/// Analyzed info of a whole program we are working on.
#[derive(Debug, Default)]
pub(crate) struct ProgramData {
pub vars: FxHashMap<Id, VarUsageInfo>,
pub top: ScopeData,
pub scopes: FxHashMap<SyntaxContext, ScopeData>,
}
impl ProgramData {
fn merge(&mut self, kind: ScopeKind, child: ProgramData) {
for (ctxt, scope) in child.scopes {
let to = self.scopes.entry(ctxt).or_default();
self.top.merge(scope.clone());
to.merge(scope);
}
for (id, var_info) in child.vars {
match self.vars.entry(id) {
Entry::Occupied(mut e) => {
e.get_mut().ref_count += var_info.ref_count;
e.get_mut().cond_init |= var_info.cond_init;
e.get_mut().reassigned |= var_info.reassigned;
e.get_mut().has_property_access |= var_info.has_property_access;
e.get_mut().exported |= var_info.exported;
e.get_mut().declared |= var_info.declared;
e.get_mut().declared_as_fn_param |= var_info.declared_as_fn_param;
// If a var is registered at a parent scope, it means that it's delcared before
// usages.
//
// e.get_mut().used_above_decl |= var_info.used_above_decl;
e.get_mut().used_in_loop |= var_info.used_in_loop;
e.get_mut().assign_count += var_info.assign_count;
e.get_mut().usage_count += var_info.usage_count;
e.get_mut().declared_as_catch_param |= var_info.declared_as_catch_param;
e.get_mut().infects.extend(var_info.infects);
match kind {
ScopeKind::Fn => {
e.get_mut().is_fn_local = false;
}
ScopeKind::Block => {}
}
}
Entry::Vacant(e) => {
e.insert(var_info);
}
}
}
}
}
/// This assumes there are no two variable with same name and same span hygiene.
#[derive(Debug)]
pub(crate) struct UsageAnalyzer {
data: ProgramData,
scope: ScopeData,
ctx: Ctx,
}
impl UsageAnalyzer {
fn with_child<F, Ret>(&mut self, child_ctxt: SyntaxContext, kind: ScopeKind, op: F) -> Ret
where
F: FnOnce(&mut UsageAnalyzer) -> Ret,
{
let mut child = UsageAnalyzer {
data: Default::default(),
ctx: self.ctx,
scope: Default::default(),
};
let ret = op(&mut child);
child
.data
.scopes
.entry(child_ctxt)
.or_default()
.merge(child.scope);
self.data.merge(kind, child.data);
ret
}
fn report(&mut self, i: Id, is_assign: bool) {
let e = self.data.vars.entry(i).or_insert_with(|| VarUsageInfo {
used_above_decl: true,
..Default::default()
});
e.ref_count += 1;
e.reassigned |= is_assign;
e.used_in_loop |= self.ctx.in_loop;
if is_assign {
e.assign_count += 1;
for other in e.infects.clone() {
self.report(other, true)
}
} else {
e.usage_count += 1;
}
}
fn report_usage(&mut self, i: &Ident, is_assign: bool) {
self.report(i.to_id(), is_assign)
}
fn declare_decl(
&mut self,
i: &Ident,
has_init: bool,
kind: Option<VarDeclKind>,
) -> &mut VarUsageInfo {
let v = self
.data
.vars
.entry(i.to_id())
.or_insert_with(|| VarUsageInfo {
is_fn_local: true,
var_kind: kind,
..Default::default()
});
v.declared = true;
if self.ctx.in_cond && has_init {
v.cond_init = true;
}
v.declared_as_catch_param |= self.ctx.in_catch_param;
v
}
}
impl Visit for UsageAnalyzer {
noop_visit_type!();
fn visit_arrow_expr(&mut self, n: &ArrowExpr, _: &dyn Node) {
self.with_child(n.span.ctxt, ScopeKind::Fn, |child| {
{
let ctx = Ctx {
in_pat_of_param: true,
..child.ctx
};
n.params.visit_with(n, &mut *child.with_ctx(ctx));
}
match &n.body {
BlockStmtOrExpr::BlockStmt(body) => {
// We use visit_children_with instead of visit_with to bypass block scope
// handler.
body.visit_children_with(child);
}
BlockStmtOrExpr::Expr(body) => {
body.visit_with(n, child);
}
}
})
}
fn visit_assign_expr(&mut self, n: &AssignExpr, _: &dyn Node) {
let ctx = Ctx {
in_assign_lhs: true,
..self.ctx
};
n.left.visit_with(n, &mut *self.with_ctx(ctx));
n.right.visit_with(n, self);
}
fn visit_block_stmt(&mut self, n: &BlockStmt, _: &dyn Node) {
self.with_child(n.span.ctxt, ScopeKind::Block, |child| {
n.visit_children_with(child);
})
}
fn visit_call_expr(&mut self, n: &CallExpr, _: &dyn Node) {
n.visit_children_with(self);
match &n.callee {
ExprOrSuper::Expr(callee) => match &**callee {
Expr::Ident(Ident { sym, .. }) if *sym == *"eval" => {
self.scope.has_eval_call = true;
}
_ => {}
},
_ => {}
}
}
fn visit_catch_clause(&mut self, n: &CatchClause, _: &dyn Node) {
{
let ctx = Ctx {
in_cond: true,
in_catch_param: true,
..self.ctx
};
n.param.visit_with(n, &mut *self.with_ctx(ctx));
}
{
let ctx = Ctx {
in_cond: true,
..self.ctx
};
n.body.visit_with(n, &mut *self.with_ctx(ctx));
}
}
fn visit_class_decl(&mut self, n: &ClassDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None);
n.visit_children_with(self);
}
fn visit_do_while_stmt(&mut self, n: &DoWhileStmt, _: &dyn Node) {
let ctx = Ctx {
in_loop: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_expr(&mut self, e: &Expr, _: &dyn Node) {
e.visit_children_with(self);
match e {
Expr::Ident(i) => {
self.report_usage(i, self.ctx.in_update_arg || self.ctx.in_assign_lhs);
}
_ => {}
}
}
fn visit_fn_decl(&mut self, n: &FnDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None);
n.visit_children_with(self);
}
fn visit_for_in_stmt(&mut self, n: &ForInStmt, _: &dyn Node) {
n.right.visit_with(n, self);
self.with_child(n.span.ctxt, ScopeKind::Block, |child| {
let ctx = Ctx {
in_left_of_for_loop: true,
..child.ctx
};
n.left.visit_with(n, &mut *child.with_ctx(ctx));
let ctx = Ctx {
in_loop: true,
..child.ctx
};
n.body.visit_with(n, &mut *child.with_ctx(ctx))
});
}
fn visit_for_of_stmt(&mut self, n: &ForOfStmt, _: &dyn Node) {
n.right.visit_with(n, self);
self.with_child(n.span.ctxt, ScopeKind::Block, |child| {
let ctx = Ctx {
in_left_of_for_loop: true,
..child.ctx
};
n.left.visit_with(n, &mut *child.with_ctx(ctx));
let ctx = Ctx {
in_loop: true,
..child.ctx
};
n.body.visit_with(n, &mut *child.with_ctx(ctx))
});
}
fn visit_for_stmt(&mut self, n: &ForStmt, _: &dyn Node) {
n.init.visit_with(n, self);
let ctx = Ctx {
in_loop: true,
..self.ctx
};
n.test.visit_with(n, &mut *self.with_ctx(ctx));
n.update.visit_with(n, &mut *self.with_ctx(ctx));
n.body.visit_with(n, &mut *self.with_ctx(ctx));
}
fn visit_function(&mut self, n: &Function, _: &dyn Node) {
n.decorators.visit_with(n, self);
self.with_child(n.span.ctxt, ScopeKind::Fn, |child| {
n.params.visit_with(n, child);
match &n.body {
Some(body) => {
// We use visit_children_with instead of visit_with to bypass block scope
// handler.
body.visit_children_with(child);
}
None => {}
}
})
}
fn visit_if_stmt(&mut self, n: &IfStmt, _: &dyn Node) {
let ctx = Ctx {
in_cond: true,
..self.ctx
};
n.test.visit_with(n, self);
n.cons.visit_with(n, &mut *self.with_ctx(ctx));
n.alt.visit_with(n, &mut *self.with_ctx(ctx));
}
fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) {
e.obj.visit_with(&Invalid { span: DUMMY_SP }, self);
if e.computed {
e.prop.visit_with(&Invalid { span: DUMMY_SP }, self);
}
match &e.obj {
ExprOrSuper::Super(_) => {}
ExprOrSuper::Expr(obj) => match &**obj {
Expr::Ident(obj) => {
let v = self.data.vars.entry(obj.to_id()).or_default();
v.has_property_access = true;
if !e.computed {
match &*e.prop {
Expr::Ident(prop) => {
v.accessed_props.insert(prop.sym.clone());
}
_ => {}
}
}
}
_ => {}
},
}
}
fn visit_param(&mut self, n: &Param, _: &dyn Node) {
let ctx = Ctx {
in_pat_of_param: false,
..self.ctx
};
n.decorators.visit_with(n, &mut *self.with_ctx(ctx));
let ctx = Ctx {
in_pat_of_param: true,
..self.ctx
};
n.pat.visit_with(n, &mut *self.with_ctx(ctx));
}
fn visit_pat(&mut self, n: &Pat, _: &dyn Node) {
n.visit_children_with(self);
let Ctx {
in_left_of_for_loop,
in_pat_of_param,
..
} = self.ctx;
match n {
Pat::Ident(i) => {
if self.ctx.in_pat_of_var_decl
|| self.ctx.in_pat_of_param
|| self.ctx.in_catch_param
{
let v = self.declare_decl(
&i.id,
self.ctx.in_pat_of_var_decl_with_init,
self.ctx.var_decl_kind_of_pat,
);
if in_pat_of_param {
v.declared_as_fn_param = true;
}
if in_left_of_for_loop {
v.reassigned = true;
}
} else {
self.report_usage(&i.id, true);
}
}
_ => {}
}
}
fn visit_prop(&mut self, n: &Prop, _: &dyn Node) {
let ctx = Ctx {
in_update_arg: false,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
match n {
Prop::Shorthand(i) => {
self.report_usage(i, false);
}
_ => {}
}
}
fn visit_setter_prop(&mut self, n: &SetterProp, _: &dyn Node) {
n.key.visit_with(n, self);
{
let ctx = Ctx {
in_pat_of_param: true,
..self.ctx
};
n.param.visit_with(n, &mut *self.with_ctx(ctx));
}
n.body.visit_with(n, self);
}
fn visit_stmt(&mut self, n: &Stmt, _: &dyn Node) {
let ctx = Ctx {
in_update_arg: false,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_try_stmt(&mut self, n: &TryStmt, _: &dyn Node) {
let ctx = Ctx {
in_cond: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_update_expr(&mut self, n: &UpdateExpr, _: &dyn Node) {
let ctx = Ctx {
in_update_arg: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_var_decl(&mut self, n: &VarDecl, _: &dyn Node) {
let ctx = Ctx {
var_decl_kind_of_pat: Some(n.kind),
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
for decl in &n.decls {
match (&decl.name, decl.init.as_deref()) {
(Pat::Ident(var), Some(Expr::Ident(init))) => {
self.data
.vars
.entry(init.to_id())
.or_default()
.infects
.push(var.to_id());
}
_ => {}
}
}
}
fn visit_var_declarator(&mut self, e: &VarDeclarator, _: &dyn Node) {
let ctx = Ctx {
in_pat_of_var_decl: true,
in_pat_of_var_decl_with_init: e.init.is_some(),
..self.ctx
};
e.name.visit_with(e, &mut *self.with_ctx(ctx));
e.init.visit_with(e, self);
}
fn visit_while_stmt(&mut self, n: &WhileStmt, _: &dyn Node) {
let ctx = Ctx {
in_loop: true,
..self.ctx
};
n.visit_children_with(&mut *self.with_ctx(ctx));
}
fn visit_with_stmt(&mut self, n: &WithStmt, _: &dyn Node) {
self.scope.has_with_stmt = true;
n.visit_children_with(self);
}
}

View File

@ -0,0 +1,89 @@
use std::borrow::Cow;
use std::mem::take;
use swc_common::pass::CompilerPass;
use swc_ecma_ast::*;
use swc_ecma_transforms::pass::JsPass;
use swc_ecma_visit::as_folder;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
pub fn drop_console() -> impl JsPass {
as_folder(DropConsole { done: false })
}
struct DropConsole {
/// Invoking this pass multiple times is simply waste of time.
done: bool,
}
impl CompilerPass for DropConsole {
fn name() -> Cow<'static, str> {
"drop-console".into()
}
}
impl VisitMut for DropConsole {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, n: &mut Expr) {
if self.done {
return;
}
n.visit_mut_children_with(self);
match n {
Expr::Call(CallExpr {
span, callee, args, ..
}) => {
// Find console.log
let callee = match callee {
ExprOrSuper::Expr(callee) => callee,
_ => return,
};
match &**callee {
Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(callee_obj),
prop: callee_prop,
computed: false,
..
}) => {
match (&**callee_obj, &**callee_prop) {
(Expr::Ident(obj), Expr::Ident(prop)) => {
if obj.sym != *"console" {
return;
}
match &*prop.sym {
"log" | "info" => {}
_ => return,
}
}
_ => return,
}
// Sioplifier will remove side-effect-free items.
*n = Expr::Seq(SeqExpr {
span: *span,
exprs: take(args).into_iter().map(|arg| arg.expr).collect(),
})
}
_ => {}
}
}
_ => {}
}
}
fn visit_mut_module(&mut self, n: &mut Module) {
if self.done {
return;
}
n.visit_mut_children_with(self);
self.done = true;
}
}

View File

@ -0,0 +1,271 @@
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::analyzer::UsageAnalyzer;
use crate::util::is_hoisted_var_decl_without_init;
use crate::util::sort::is_sorted_by_key;
use crate::util::IsModuleItem;
use fxhash::FxHashSet;
use swc_common::pass::Repeated;
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::Id;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::VisitWith;
pub(super) struct DeclHoisterConfig {
pub hoist_fns: bool,
pub hoist_vars: bool,
pub top_level: bool,
}
pub(super) fn decl_hoister(config: DeclHoisterConfig) -> Hoister {
Hoister {
config,
changed: false,
data: None,
}
}
pub(super) struct Hoister {
config: DeclHoisterConfig,
changed: bool,
data: Option<ProgramData>,
}
impl Repeated for Hoister {
fn changed(&self) -> bool {
self.changed
}
fn reset(&mut self) {
self.changed = false;
}
}
impl Hoister {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike + IsModuleItem,
Vec<T>: VisitMutWith<Self> + 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);
if ids.iter().any(|id| {
data.vars
.get(id)
.map(|v| !v.used_above_decl)
.unwrap_or(false)
}) {
2
} else {
3
}
} else {
2
}
}
_ => 3,
},
None => 3,
}) || (self.config.hoist_vars
&& stmts.windows(2).any(|stmts| {
is_hoisted_var_decl_without_init(&stmts[0])
&& is_hoisted_var_decl_without_init(&stmts[1])
}));
if !should_hoist {
return;
}
self.changed = true;
let mut var_decls = vec![];
let mut fn_decls = Vec::with_capacity(stmts.len());
let mut new_stmts = Vec::with_capacity(stmts.len());
let mut done = FxHashSet::default();
let mut found_non_var_decl = false;
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => {
// Seaarch for variable declarations.
match stmt {
Stmt::Decl(Decl::Fn(..)) if self.config.hoist_fns => {
// Move functions to top.
fn_decls.push(T::from_stmt(stmt))
}
Stmt::Decl(Decl::Var(
var
@
VarDecl {
kind: VarDeclKind::Var,
..
},
)) if found_non_var_decl => {
let mut exprs = vec![];
for decl in var.decls {
let ids: Vec<Ident> = find_ids(&decl.name);
for id in ids {
if done.insert(id.to_id()) {
// If the enclosing function declares parameter with same
// name, we can drop a varaible.
if decl.init.is_none()
&& self
.data
.as_ref()
.and_then(|v| v.vars.get(&id.to_id()))
.map(|v| v.declared_as_fn_param)
.unwrap_or(false)
{
continue;
}
var_decls.push(VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(id.into()),
init: None,
definite: false,
})
}
}
match decl.init {
Some(init) => {
//
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: decl.span,
left: PatOrExpr::Pat(Box::new(decl.name)),
op: op!("="),
right: init,
})));
}
None => {}
}
}
if exprs.is_empty() {
continue;
}
new_stmts.push(T::from_stmt(Stmt::Expr(ExprStmt {
span: var.span,
expr: if exprs.len() == 1 {
exprs.into_iter().next().unwrap()
} else {
Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
}))
},
})))
}
Stmt::Decl(Decl::Var(VarDecl {
kind: VarDeclKind::Var,
decls,
..
})) => {
// It can be merged because we didn't found normal statement.
//
// Code like
//
// var a = 1;
// var b = 3;
//
// will be merged.
var_decls.extend(decls.into_iter().filter(|decl| {
// We should preserve if init exists because
//
// var a = 2, a = 3;
//
// is valid javascript code.
let preserve = match &decl.name {
Pat::Ident(name) => {
// If the enclosing function declares parameter with same
// name, we can drop a varaible. (If it's side-effect free).
if decl.init.is_none()
&& self
.data
.as_ref()
.and_then(|v| v.vars.get(&name.to_id()))
.map(|v| v.declared_as_fn_param)
.unwrap_or(false)
{
return false;
}
done.insert(name.to_id())
}
_ => true,
};
preserve || decl.init.is_some()
}));
}
Stmt::Decl(Decl::Var(..)) => new_stmts.push(T::from_stmt(stmt)),
_ => {
match stmt {
Stmt::Throw(..) => {
fn_decls.push(T::from_stmt(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: false,
decls: var_decls.take(),
}))));
}
_ => {}
}
found_non_var_decl = true;
new_stmts.push(T::from_stmt(stmt))
}
}
}
Err(stmt) => new_stmts.push(stmt),
}
}
fn_decls.push(T::from_stmt(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: false,
decls: var_decls,
}))));
fn_decls.extend(new_stmts);
*stmts = fn_decls;
}
}
impl VisitMut for Hoister {
noop_visit_mut_type!();
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
self.handle_stmt_likes(stmts);
}
fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
self.handle_stmt_likes(stmts);
}
}

View File

@ -0,0 +1,225 @@
use self::drop_console::drop_console;
use self::hoist_decls::DeclHoisterConfig;
use self::optimize::optimizer;
use crate::compress::hoist_decls::decl_hoister;
use crate::debug::dump;
use crate::option::CompressOptions;
use crate::util::Optional;
use pretty_assertions::assert_eq;
use std::borrow::Cow;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use swc_common::chain;
use swc_common::comments::Comments;
use swc_common::pass::CompilerPass;
use swc_common::pass::Repeat;
use swc_common::pass::Repeated;
use swc_ecma_ast::*;
use swc_ecma_transforms::optimization::simplify::dead_branch_remover;
use swc_ecma_transforms::optimization::simplify::expr_simplifier;
use swc_ecma_transforms::pass::JsPass;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::as_folder;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::FoldWith;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
mod drop_console;
mod hoist_decls;
mod optimize;
pub fn compressor<'a>(
options: &'a CompressOptions,
comments: Option<&'a dyn Comments>,
) -> impl 'a + JsPass {
let console_remover = Optional {
enabled: options.drop_console,
visitor: drop_console(),
};
let compressor = Compressor {
comments,
options,
pass: 0,
changed: false,
};
chain!(
console_remover,
Repeat::new(as_folder(compressor)),
expr_simplifier()
)
}
struct Compressor<'a> {
options: &'a CompressOptions,
comments: Option<&'a dyn Comments>,
changed: bool,
pass: usize,
}
impl CompilerPass for Compressor<'_> {
fn name() -> Cow<'static, str> {
"compressor".into()
}
}
impl Repeated for Compressor<'_> {
fn changed(&self) -> bool {
self.changed
}
fn reset(&mut self) {
self.changed = false;
self.pass += 1;
}
}
impl Compressor<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
Vec<T>: VisitMutWith<Self> + VisitMutWith<hoist_decls::Hoister>,
{
// Skip if `use asm` exists.
if stmts.iter().any(|stmt| match stmt.as_stmt() {
Some(v) => match v {
Stmt::Expr(stmt) => match &*stmt.expr {
Expr::Lit(Lit::Str(Str {
value,
has_escape: false,
..
})) => &**value == "use asm",
_ => false,
},
_ => false,
},
None => false,
}) {
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
}
}
impl VisitMut for Compressor<'_> {
noop_visit_mut_type!();
fn visit_mut_module(&mut self, n: &mut Module) {
if self.options.passes != 0 && self.options.passes + 1 <= self.pass {
let done = dump(&*n);
eprintln!("===== Done =====\n{}", done);
return;
}
dbg!(self.pass);
// Temporary
if self.pass > 10 {
panic!("Infinite loop detected")
}
let start = dump(&*n);
eprintln!("===== Start =====\n{}", start);
{
let mut visitor = expr_simplifier();
n.map_with_mut(|m| m.fold_with(&mut visitor));
self.changed |= visitor.changed();
if visitor.changed() {
log::trace!("compressor: Simplified expressions");
eprintln!("===== Simplified =====\n{}", dump(&*n));
}
if cfg!(debug_assertions) && !visitor.changed() {
let simplified = dump(&*n);
if start != simplified {
assert_eq!(
DebugUsingDisplay(&start),
DebugUsingDisplay(&simplified),
"Invalid state: expr_simplifier: The code is changed but changed is not \
setted to true",
)
}
}
}
{
// TODO: reset_opt_flags
//
// This is swc version of `node.optimize(this);`.
let mut visitor = optimizer(self.options.clone(), self.comments);
n.visit_mut_with(&mut visitor);
self.changed |= visitor.changed();
}
if self.options.conditionals || self.options.dead_code {
let mut v = dead_branch_remover();
n.map_with_mut(|n| n.fold_with(&mut v));
self.changed |= v.changed();
}
n.visit_mut_children_with(self);
}
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>) {
self.handle_stmt_likes(stmts);
stmts.retain(|stmt| match stmt {
ModuleItem::Stmt(Stmt::Empty(..)) => false,
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Var(VarDecl { decls, .. }),
..
}))
| ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { decls, .. })))
if decls.is_empty() =>
{
false
}
_ => true,
});
}
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
self.handle_stmt_likes(stmts);
stmts.retain(|stmt| match stmt {
Stmt::Empty(..) => false,
Stmt::Decl(Decl::Var(VarDecl { decls, .. })) if decls.is_empty() => false,
_ => true,
});
}
}
#[derive(PartialEq, Eq)]
struct DebugUsingDisplay<'a>(&'a str);
impl<'a> Debug for DebugUsingDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}

View File

@ -0,0 +1,212 @@
use std::iter::repeat_with;
use crate::analyzer::analyze;
use super::Optimizer;
use swc_atoms::js_word;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::private_ident;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
/// Methods related to the option `arguments`.
impl Optimizer<'_> {
///
/// - `arguments['foo']` => `arguments.foo`
pub(super) fn optimize_str_access_to_arguments(&mut self, e: &mut Expr) {
if !self.options.arguments {
return;
}
let member = match e {
Expr::Member(member) => member,
_ => return,
};
if !member.computed {
return;
}
match &*member.prop {
Expr::Lit(Lit::Str(prop)) => {
if !prop.value.starts_with(|c: char| c.is_ascii_alphabetic()) {
return;
}
self.changed = true;
log::trace!("arguments: Optimizing computed access to arguments");
member.computed = false;
member.prop = Box::new(Expr::Ident(Ident {
span: prop.span,
sym: prop.value.clone(),
optional: false,
}))
}
_ => {}
}
}
pub(super) fn optimize_usage_of_arguments(&mut self, f: &mut Function) {
if !self.options.arguments {
return;
}
if f.params.iter().any(|param| match param.pat {
Pat::Ident(BindingIdent {
id:
Ident {
sym: js_word!("arguments"),
..
},
..
}) => true,
Pat::Ident(..) => false,
_ => true,
}) {
return;
}
{
// 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 {
return;
}
}
}
let mut v = ArgReplacer {
params: &mut f.params,
changed: false,
keep_fargs: self.options.keep_fargs,
};
// We visit body two time, to use simpler logic in `inject_params_if_required`
f.body.visit_mut_children_with(&mut v);
f.body.visit_mut_children_with(&mut v);
self.changed |= v.changed;
}
}
struct ArgReplacer<'a> {
params: &'a mut Vec<Param>,
changed: bool,
keep_fargs: bool,
}
impl ArgReplacer<'_> {
fn inject_params_if_required(&mut self, idx: usize) {
if idx < self.params.len() || self.keep_fargs {
return;
}
let new_args = idx + 1 - self.params.len();
self.changed = true;
log::trace!("arguments: Injecting {} parameters", new_args);
let mut start = self.params.len();
self.params.extend(
repeat_with(|| {
let p = Param {
span: DUMMY_SP,
decorators: Default::default(),
pat: Pat::Ident(private_ident!(format!("argument_{}", start)).into()),
};
start += 1;
p
})
.take(new_args),
)
}
}
impl VisitMut for ArgReplacer<'_> {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, n: &mut Expr) {
n.visit_mut_children_with(self);
match n {
Expr::Member(member) => {
if member.computed {
match &member.obj {
ExprOrSuper::Expr(obj) => match &**obj {
Expr::Ident(Ident {
sym: js_word!("arguments"),
..
}) => match &*member.prop {
Expr::Lit(Lit::Str(Str { value, .. })) => {
let idx = value.parse::<usize>();
let idx = match idx {
Ok(v) => v,
_ => return,
};
self.inject_params_if_required(idx);
if let Some(param) = self.params.get(idx) {
match &param.pat {
Pat::Ident(i) => {
self.changed = true;
*n = Expr::Ident(i.id.clone());
return;
}
_ => {}
}
}
}
Expr::Lit(Lit::Num(Number { value, .. })) => {
if value.fract() != 0.0 {
// We ignores non-integer values.
return;
}
let idx = value.round() as i64 as usize;
self.inject_params_if_required(idx);
//
if let Some(param) = self.params.get(idx) {
match &param.pat {
Pat::Ident(i) => {
log::trace!(
"arguments: Replacing access to arguments to \
normal reference",
);
self.changed = true;
*n = Expr::Ident(i.id.clone());
return;
}
_ => {}
}
}
}
_ => {}
},
_ => {}
},
_ => {}
}
}
}
_ => {}
}
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
n.obj.visit_mut_with(self);
if n.computed {
n.prop.visit_mut_with(self);
}
}
/// Noop.
fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
/// Noop.
fn visit_mut_function(&mut self, _: &mut Function) {}
}

View File

@ -0,0 +1,104 @@
use super::Optimizer;
use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
/// Methods related to the option `arrows`.
impl Optimizer<'_> {
pub(super) fn optimize_arrow_body(&mut self, b: &mut BlockStmtOrExpr) {
if !self.options.arrows {
return;
}
match b {
BlockStmtOrExpr::BlockStmt(s) => {
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");
*b = BlockStmtOrExpr::Expr(arg.take());
return;
}
}
}
}
BlockStmtOrExpr::Expr(_) => {}
}
}
pub(super) fn optimize_arrow_method_prop(&mut self, p: &mut Prop) {
if self.options.ecma < EsVersion::Es2015 {
return;
}
if !self.options.unsafe_methods && !self.options.arrows {
return;
}
match p {
Prop::KeyValue(kv) => {
//
{
let mut v = ThisVisitor { found: false };
kv.value.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
if v.found {
return;
}
}
match &mut *kv.value {
Expr::Arrow(
m
@
ArrowExpr {
body: BlockStmtOrExpr::BlockStmt(..),
..
},
) => {
*p = Prop::Method(MethodProp {
key: kv.key.take(),
function: Function {
params: m
.params
.take()
.into_iter()
.map(|pat| Param {
span: pat.span(),
decorators: Default::default(),
pat,
})
.collect(),
decorators: Default::default(),
span: m.span,
body: m.body.take().block_stmt(),
is_generator: m.is_generator,
is_async: m.is_async,
type_params: Default::default(),
return_type: Default::default(),
},
});
}
_ => {}
}
}
_ => {}
}
}
}
struct ThisVisitor {
found: bool,
}
impl Visit for ThisVisitor {
noop_visit_type!();
fn visit_this_expr(&mut self, _: &ThisExpr, _: &dyn Node) {
self.found = true;
}
}

View File

@ -0,0 +1,384 @@
use super::Optimizer;
use crate::compress::optimize::is_pure_undefined;
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::Type;
use swc_ecma_utils::Value;
use swc_ecma_utils::Value::Known;
use swc_ecma_utils::Value::Unknown;
/// 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()`
///
/// In this case, if lhs is false, rhs is also false so it's removable.
pub(super) fn remove_useless_pipes(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
}
match e {
Expr::Bin(BinExpr {
left,
op: op @ op!("&&"),
right,
..
})
| Expr::Bin(BinExpr {
left,
op: op @ op!("||"),
right,
..
}) => {
let lt = left.get_type();
let rt = right.get_type();
match (lt, rt) {
(Known(Type::Bool), Known(Type::Bool)) => {
let rb = right.as_pure_bool();
let rb = match rb {
Known(v) => v,
Unknown => return,
};
//
let can_remove = if *op == op!("&&") { rb } else { !rb };
if can_remove {
if *op == op!("&&") {
log::trace!("booleans: Compressing `!foo && true` as `!foo`");
} else {
log::trace!("booleans: Compressing `!foo || false` as `!foo`");
}
self.changed = true;
*e = *left.take();
return;
}
}
_ => {}
}
}
_ => {}
}
}
/// `!(a && b)` => `!a || !b`
pub(super) fn optimize_bools(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
}
match e {
Expr::Unary(UnaryExpr {
span,
op: op!("!"),
arg,
..
}) => match &mut **arg {
Expr::Bin(BinExpr {
op: op!("&&"),
left,
right,
..
}) => {
log::trace!("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(),
})),
});
return;
}
_ => {}
},
_ => {}
}
}
pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
}
let delete = match e {
Expr::Unary(
u @ UnaryExpr {
op: op!("delete"), ..
},
) => u,
_ => return,
};
if delete.arg.may_have_side_effects() {
return;
}
let convert_to_true = match &*delete.arg {
Expr::Seq(..)
| Expr::Cond(..)
| Expr::Bin(BinExpr { op: op!("&&"), .. })
| Expr::Bin(BinExpr { op: op!("||"), .. }) => true,
// V8 and terser test ref have different opinion.
Expr::Ident(Ident {
sym: js_word!("Infinity"),
..
}) => false,
Expr::Ident(Ident {
sym: js_word!("undefined"),
..
}) => false,
Expr::Ident(Ident {
sym: js_word!("NaN"),
..
}) => false,
e if is_pure_undefined(&e) => true,
Expr::Ident(..) => true,
// NaN
Expr::Bin(BinExpr {
op: op!("/"),
right,
..
}) => {
let rn = right.as_number();
let v = if let Known(rn) = rn {
if rn != 0.0 {
true
} else {
false
}
} else {
false
};
if v {
true
} else {
self.changed = true;
let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` as sequence expression");
*e = Expr::Seq(SeqExpr {
span,
exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))],
});
return;
}
}
_ => false,
};
if convert_to_true {
self.changed = true;
let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` => true");
*e = make_bool(span, true);
return;
}
}
/// 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::Unary(UnaryExpr {
span,
op: op!("!"),
arg,
}) => match &**arg {
Expr::Lit(Lit::Num(Number { value, .. })) => {
log::trace!("Optimizing: number => number (in bool context)");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
span: *span,
value: if *value == 0.0 { 1.0 } else { 0.0 },
}))
}
_ => {}
},
Expr::Unary(UnaryExpr {
span,
op: op!("typeof"),
arg,
}) => {
log::trace!("Optimizing: typeof => true (in bool context)");
self.changed = true;
match &**arg {
Expr::Ident(..) => {
*n = Expr::Lit(Lit::Num(Number {
span: *span,
value: 1.0,
}))
}
_ => {
// Return value of typeof is always truthy
let true_expr = Box::new(Expr::Lit(Lit::Num(Number {
span: *span,
value: 1.0,
})));
*n = Expr::Seq(SeqExpr {
span: *span,
exprs: vec![arg.take(), true_expr],
})
}
}
}
Expr::Lit(Lit::Str(s)) => {
log::trace!("Converting string as boolean expressions");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
span: s.span,
value: if s.value.is_empty() { 0.0 } else { 1.0 },
}));
}
Expr::Lit(Lit::Num(num)) => {
if num.value == 1.0 || num.value == 0.0 {
return;
}
if self.options.bools {
log::trace!("booleans: Converting number as boolean expressions");
self.changed = true;
*n = Expr::Lit(Lit::Num(Number {
span: num.span,
value: if num.value == 0.0 { 0.0 } else { 1.0 },
}));
}
}
Expr::Bin(BinExpr {
op: op!("??"),
left,
right,
..
}) => {
// Optimize if (a ?? false); as if (a);
if let Value::Known(false) = right.as_pure_bool() {
log::trace!(
"Dropping right operand of `??` as it's always false (in bool context)"
);
self.changed = true;
*n = *left.take();
}
}
_ => {
let span = n.span();
let v = n.as_pure_bool();
if let Known(v) = v {
log::trace!("Optimizing expr as {} (in bool context)", v);
*n = make_bool(span, v);
}
}
}
}
pub(super) fn compress_if_stmt_as_expr(&mut self, s: &mut Stmt) {
if !self.options.bools {
return;
}
let stmt = match s {
Stmt::If(v) => v,
_ => return,
};
match &stmt.alt {
Some(..) => {}
None => match &mut *stmt.cons {
Stmt::Expr(cons) => {
self.changed = true;
log::trace!("conditionals: `if (foo) bar;` => `foo && bar`");
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr: Box::new(Expr::Bin(BinExpr {
span: stmt.test.span(),
op: op!("&&"),
left: stmt.test.take(),
right: cons.expr.take(),
})),
});
return;
}
_ => {}
},
}
}
}

View File

@ -0,0 +1,313 @@
use super::Optimizer;
use fxhash::FxHashMap;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::VisitWith;
/// Methods related to the option `collapse_vars`.
impl Optimizer<'_> {
pub(super) fn collapse_seq_exprs(&mut self, e: &mut Expr) {
if !self.options.collapse_vars {
return;
}
let seq = match e {
Expr::Seq(seq) => seq,
_ => return,
};
if seq.exprs.len() < 2 {
return;
}
match (
&*seq.exprs[seq.exprs.len() - 2],
&*seq.exprs[seq.exprs.len() - 1],
) {
(Expr::Assign(assign), Expr::Ident(ident)) => {
// Check if lhs is same as `ident`.
match &assign.left {
PatOrExpr::Expr(_) => {}
PatOrExpr::Pat(left) => match &**left {
Pat::Ident(left) => {
if left.id.sym == ident.sym && left.id.span.ctxt == ident.span.ctxt {
seq.exprs.pop();
}
}
_ => {}
},
}
}
_ => {}
}
}
pub(super) fn collapse_assignment_to_vars(&mut self, e: &mut Expr) {
if !self.options.collapse_vars {
return;
}
if self.ctx.in_try_block || self.ctx.executed_multiple_time {
return;
}
match &*e {
Expr::Assign(assign) => {
//
let left = match &assign.left {
PatOrExpr::Expr(_) => return,
PatOrExpr::Pat(left) => match &**left {
Pat::Ident(i) => i,
_ => return,
},
};
if let Some(usage) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&left.to_id()))
{
if !usage.declared
|| !usage.is_fn_local
|| usage.assign_count != 1
|| usage.var_kind == Some(VarDeclKind::Const)
{
return;
}
}
let value = match &*assign.right {
Expr::Lit(..)
| Expr::Member(MemberExpr {
computed: false, ..
}) => assign.right.clone(),
_ => return,
};
self.lits.insert(left.to_id(), value);
}
_ => {}
}
}
/// Collapse single-use non-constant variables, side effects permitting.
pub(super) fn collapse_consequtive_vars<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
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;
}
// We only inline for some subset of statements.
match r {
Stmt::Expr(..) | Stmt::Return(..) => {}
_ => return None,
}
// 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;
}
_ => {
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.reassigned
|| !SimpleUsageFinder::find(&name.id, r)
{
return None;
}
return Some(i);
}
None
}
_ => None,
}
} else {
None
}
}
_ => None,
}
})
.collect::<Vec<_>>();
if indexes.is_empty() {
return;
}
self.changed = true;
log::trace!("collapse_vars: Collapsing {:?}", indexes);
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,
});
new.push(T::from_stmt(stmt));
}
},
Err(item) => {
new.push(item);
}
}
}
*stmts = new;
//
}
}
/// Checks inlinabilty of variable initializer.
struct InlinabiltyChecker {
can_inline: bool,
}
impl Visit for InlinabiltyChecker {
noop_visit_type!();
fn visit_update_expr(&mut self, _: &UpdateExpr, _: &dyn Node) {
self.can_inline = false;
}
}
struct Inliner<'a> {
values: &'a mut FxHashMap<Id, Option<Box<Expr>>>,
}
impl VisitMut for Inliner<'_> {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Ident(i) => {
if let Some(value) = self.values.remove(&i.to_id()) {
*e = *value.expect("should be used only once");
}
}
_ => {}
}
}
}
/// 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

@ -0,0 +1,41 @@
use super::Optimizer;
use swc_common::SyntaxContext;
use swc_ecma_ast::*;
/// Methods related to the option `computed_props`.
impl Optimizer<'_> {
/// If a key of is `'str'` (like `{ 'str': 1 }`) change it to [Ident] like
/// (`{ str: 1, }`)
pub(super) fn optimize_computed_prop_name_as_normal(&mut self, p: &mut PropName) {
if !self.options.computed_props {
return;
}
match p {
PropName::Computed(c) => match &mut *c.expr {
Expr::Lit(Lit::Str(s)) => {
if s.value == *"constructor" || s.value == *"__proto__" {
return;
}
if s.value.is_empty() || s.value.starts_with(|c: char| c.is_numeric()) {
*p = PropName::Str(s.clone());
} else {
*p = PropName::Ident(Ident::new(
s.value.clone(),
s.span.with_ctxt(SyntaxContext::empty()),
));
}
return;
}
Expr::Lit(Lit::Num(n)) => {
*p = PropName::Num(n.clone());
return;
}
_ => {}
},
_ => {}
}
}
}

View File

@ -0,0 +1,699 @@
use super::Optimizer;
use crate::util::make_bool;
use crate::util::SpanExt;
use swc_common::EqIgnoreSpan;
use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::AsOptExpr;
use swc_ecma_transforms_base::ext::ExprRefExt;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::ExprFactory;
use swc_ecma_utils::StmtLike;
use swc_ecma_utils::Type;
use swc_ecma_utils::Value::Known;
/// Methods related to the option `conditionals`. All methods are noop if
/// `conditionals` is false.
impl Optimizer<'_> {
/// Removes useless operands of an logical expressions.
pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) {
if !self.options.conditionals {
return;
}
let bin = match e {
Expr::Bin(b) => b,
_ => return,
};
if bin.op != op!("||") && bin.op != op!("&&") {
return;
}
if bin.left.may_have_side_effects() {
return;
}
let lt = bin.left.get_type();
let rt = bin.right.get_type();
let _lb = bin.left.as_pure_bool();
let rb = bin.right.as_pure_bool();
if let (Known(Type::Bool), Known(Type::Bool)) = (lt, rt) {
// `!!b || true` => true
if let Known(true) = rb {
self.changed = true;
log::trace!("conditionals: `!!foo || true` => `true`");
*e = make_bool(bin.span, true);
return;
}
}
}
pub(super) fn compress_cond_with_logical_as_logical(&mut self, e: &mut Expr) {
if !self.options.conditionals {
return;
}
let cond = match e {
Expr::Cond(v) => v,
_ => return,
};
let cons_span = cond.cons.span();
match (&mut *cond.cons, &mut *cond.alt) {
(Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt)
if (*cons.right).eq_ignore_span(&*alt) =>
{
log::trace!("conditionals: `x ? y || z : z` => `x || y && z`");
self.changed = true;
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: Box::new(Expr::Bin(BinExpr {
span: cons_span,
op: op!("&&"),
left: cond.test.take(),
right: cons.left.take(),
})),
right: cons.right.take(),
});
return;
}
_ => {}
}
}
///
/// - `foo ? 1 : false` => `!!foo && 1`
/// - `!foo ? true : 0` => `!foo || 0`
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,
};
let lt = cond.cons.get_type();
if let Known(Type::Bool) = lt {
let lb = cond.cons.as_pure_bool();
if let Known(true) = lb {
log::trace!("conditionals: `foo ? true : bar` => `!!foo || bar`");
// Negate twice to convert `test` to boolean.
self.negate_twice(&mut cond.test);
self.changed = true;
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
// TODO: Verify this rule.
if false {
if let Known(false) = lb {
log::trace!("conditionals: `foo ? false : bar` => `!foo && bar`");
self.changed = true;
self.negate(&mut cond.test);
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("&&"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
}
}
let rt = cond.alt.get_type();
if let Known(Type::Bool) = rt {
let rb = cond.alt.as_pure_bool();
if let Known(false) = rb {
log::trace!("conditionals: `foo ? 1 : false` => `!!foo && 1`");
self.changed = true;
// Negate twice to convert `test` to boolean.
self.negate_twice(&mut cond.test);
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("&&"),
left: cond.test.take(),
right: cond.cons.take(),
});
return;
}
}
}
///
/// # Example
///
/// ## Input
///
/// ```ts
/// if (foo) return;
/// if (bar) return;
/// if (baz) return;
/// if (baa) return;
/// ```
///
/// ## Output
///
/// ```ts
/// if (foo || bar || baz || baa) return;
/// ```
pub(super) fn merge_simillar_ifs<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
if !self.options.conditionals {
return;
}
let has_work =
stmts
.windows(2)
.any(|stmts| match (&stmts[0].as_stmt(), &stmts[1].as_stmt()) {
(Some(Stmt::If(l)), Some(Stmt::If(r))) => l.cons.eq_ignore_span(&r.cons),
_ => false,
});
if !has_work {
return;
}
self.changed = true;
log::trace!("conditionals: Merging if statements with same `cons`");
let mut cur: Option<IfStmt> = None;
let mut new = Vec::with_capacity(stmts.len());
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => {
match stmt {
Stmt::If(mut stmt) => {
//
match &mut cur {
Some(cur_if) => {
// If cons is same, we merge conditions.
if cur_if.cons.eq_ignore_span(&stmt.cons) {
cur_if.test = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: cur_if.test.take(),
op: op!("||"),
right: stmt.test.take(),
}));
} else {
new.extend(cur.take().map(Stmt::If).map(T::from_stmt));
}
}
None => {
cur = Some(stmt);
}
}
}
_ => {
new.extend(cur.take().map(Stmt::If).map(T::from_stmt));
new.push(T::from_stmt(stmt));
}
}
}
Err(item) => {
new.extend(cur.take().map(Stmt::If).map(T::from_stmt));
new.push(item);
}
}
}
new.extend(cur.map(Stmt::If).map(T::from_stmt));
*stmts = new;
}
///
/// # Examples
///
/// ## Input
///
/// ```ts
/// function foo(do_something, some_condition) {
/// if (some_condition) do_something(x);
/// else do_something(y);
/// if (some_condition) side_effects(x);
/// else side_effects(y);
/// }
/// ```
///
/// ## Output
///
/// ```ts
/// function foo(do_something, some_condition) {
/// do_something(some_condition ? x : y);
/// some_condition ? side_effects(x) : side_effects(y);
/// }
/// ```
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,
};
// If alt does not exist, an if statement is better than a conditional
// expression.
let alt = match &mut stmt.alt {
Some(v) => &mut **v,
None => {
return;
}
};
let alt = match extract_expr_stmt(alt) {
Some(v) => v,
None => return,
};
// if (!foo); else bar();
// =>
// foo && bar()
match &*stmt.cons {
Stmt::Empty(..) => {
match &mut *stmt.test {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
log::trace!("Optimizing `if (!foo); else bar();` as `foo && bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: arg.take(),
op: op!("&&"),
right: Box::new(alt.take()),
}));
self.compress_logical_exprs_as_bang_bang(&mut expr);
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr,
});
}
_ => {
log::trace!("Optimizing `if (foo); else bar();` as `foo || bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: stmt.test.take(),
op: op!("||"),
right: Box::new(alt.take()),
}));
self.compress_logical_exprs_as_bang_bang(&mut expr);
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr,
});
}
}
return;
}
_ => {}
}
let cons = match extract_expr_stmt(&mut *stmt.cons) {
Some(v) => v,
None => return,
};
let new_expr = self.compress_similar_cons_alt(&mut stmt.test, cons, alt, true);
match new_expr {
Some(v) => {
self.changed = true;
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr: Box::new(v),
});
return;
}
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()),
})),
})
}
/// 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,
};
let compressed =
self.compress_similar_cons_alt(&mut cond.test, &mut cond.cons, &mut cond.alt, false);
match compressed {
Some(v) => {
*e = v;
self.changed = true;
return;
}
None => {}
}
// 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`");
self.changed = true;
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
}
fn compress_similar_cons_alt(
&mut self,
test: &mut Box<Expr>,
cons: &mut Expr,
alt: &mut Expr,
is_for_if_stmt: bool,
) -> Option<Expr> {
if cons.eq_ignore_span(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;
}
let side_effect_free = self
.data
.as_ref()
.and_then(|data| data.vars.get(&cons_callee.to_id()))
.map(|v| v.is_fn_local)
.unwrap_or(false);
if side_effect_free
&& cons.args.len() == alt.args.len()
&& cons.args.iter().all(|arg| arg.spread.is_none())
&& alt.args.iter().all(|arg| arg.spread.is_none())
{
let diff_count = cons
.args
.iter()
.zip(alt.args.iter())
.filter(|(cons, alt)| !cons.eq_ignore_span(alt))
.count();
if diff_count == 1 {
log::trace!(
"conditionals: Merging cons and alt as only one argument differs"
);
self.changed = true;
let diff_idx = cons
.args
.iter()
.zip(alt.args.iter())
.position(|(cons, alt)| !cons.eq_ignore_span(alt))
.unwrap();
let mut new_args = vec![];
for (idx, arg) in cons.args.take().into_iter().enumerate() {
if idx == diff_idx {
// Inject conditional.
new_args.push(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Cond(CondExpr {
span: arg.expr.span(),
test: test.take(),
cons: arg.expr,
alt: alt.args[idx].expr.take(),
})),
})
} else {
//
new_args.push(arg)
}
}
return Some(Expr::Call(CallExpr {
span: test.span(),
callee: cons_callee.clone().as_callee(),
args: new_args,
type_args: Default::default(),
}));
}
}
if side_effect_free
&& cons.args.len() == 1
&& alt.args.len() == 1
&& cons.args.iter().all(|arg| arg.spread.is_none())
&& alt.args.iter().all(|arg| arg.spread.is_none())
{
// if (some_condition) do_something(x);
// else do_something(y);
//
// =>
//
// do_something(some_condition ? x : y);
//
let mut args = vec![];
args.push(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP,
test: test.take(),
cons: cons.args[0].expr.take(),
alt: alt.args[0].expr.take(),
})),
});
log::trace!(
"Compreessing if into cond as there's no side effect and the number of \
arguments is 1"
);
return Some(Expr::Call(CallExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
callee: cons.callee.take(),
args,
type_args: Default::default(),
}));
}
if !side_effect_free && is_for_if_stmt {
log::trace!("Compreessing if into cond while preserving side effects");
return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: test.take(),
cons: Box::new(Expr::Call(cons.take())),
alt: Box::new(Expr::Call(alt.take())),
}));
}
None
}
(Expr::New(cons), Expr::New(alt)) => {
// TODO: Handle new expression with no args.
if cons.callee.eq_ignore_span(&alt.callee) {
if cons.args.as_ref().map(|v| v.len() <= 1).unwrap_or(true)
&& alt.args.as_ref().map(|v| v.len() <= 1).unwrap_or(true)
&& (cons.args.is_some()
&& cons
.args
.as_ref()
.unwrap()
.iter()
.all(|arg| arg.spread.is_none()))
&& (alt.args.is_some()
&& alt
.args
.as_ref()
.unwrap()
.iter()
.all(|arg| arg.spread.is_none()))
{
let mut args = vec![];
args.push(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP,
test: test.take(),
cons: cons.args.as_mut().unwrap()[0].expr.take(),
alt: alt.args.as_mut().unwrap()[0].expr.take(),
})),
});
log::trace!(
"Compreessing if statement into a condiotnal expression of `new` as \
there's no side effect and the number of arguments is 1"
);
return Some(Expr::New(NewExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
callee: cons.callee.take(),
args: Some(args),
type_args: Default::default(),
}));
}
}
None
}
(Expr::Assign(cons), Expr::Assign(alt))
if cons.op == op!("=")
&& cons.op == alt.op
&& cons.left.eq_ignore_span(&alt.left)
&& is_simple_lhs(&cons.left) =>
{
log::trace!("Merging assignments in cons and alt of if statement");
Some(Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: cons.op,
left: cons.left.take(),
right: Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP,
test: test.take(),
cons: cons.right.take(),
alt: alt.right.take(),
})),
}))
}
// 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()");
return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: test.take(),
op: op!("&&"),
right: cons.test.take(),
})),
cons: cons.cons.take(),
alt: cons.alt.take(),
}));
}
// 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");
//
let first = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: test.take(),
op: op!("||"),
right: alt.exprs[0].take(),
}));
return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![first, Box::new(cons.take())],
}));
}
_ => None,
}
}
}
fn extract_expr_stmt(s: &mut Stmt) -> Option<&mut Expr> {
match s {
Stmt::Expr(e) => Some(&mut *e.expr),
_ => None,
}
}
fn is_simple_lhs(l: &PatOrExpr) -> bool {
match l {
PatOrExpr::Expr(l) => match &**l {
Expr::Ident(..) => return true,
_ => false,
},
PatOrExpr::Pat(l) => match &**l {
Pat::Ident(_) => return true,
_ => false,
},
}
}

View File

@ -0,0 +1,75 @@
use super::Optimizer;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
/// Methods related to option `dead_code`.
impl Optimizer<'_> {
/// Optimize return value or argument of throw.
///
/// This methods removes some useless assignments.
///
/// # Example
///
/// Note: `a` being declared in the function is important in the example
/// below.
///
/// ```ts
/// function foo(){
/// var a;
/// throw a = x();
/// }
/// ```
///
/// can be optimized as
///
/// ```ts
/// function foo(){
/// var a; // Will be dropped in next pass.
/// throw x();
/// }
/// ```
pub(super) fn optimize_in_fn_termiation(&mut self, e: &mut Expr) {
if !self.options.dead_code {
return;
}
// A return statement in a try block may not terminate function.
if self.ctx.in_try_block {
return;
}
match e {
Expr::Assign(assign) => {
self.optimize_in_fn_termiation(&mut assign.right);
// We only handle identifiers on lhs for now.
match &assign.left {
PatOrExpr::Pat(lhs) => match &**lhs {
Pat::Ident(lhs) => {
//
if self
.data
.as_ref()
.and_then(|data| data.vars.get(&lhs.to_id()))
.map(|var| var.is_fn_local)
.unwrap_or(false)
{
log::trace!(
"dead_code: Dropping an assigment to a varaible declared in \
function because function is being terminated"
);
self.changed = true;
*e = *assign.right.take();
return;
}
}
_ => {}
},
_ => {}
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,238 @@
use std::num::FpCategory;
use super::Optimizer;
use swc_atoms::js_word;
use swc_common::SyntaxContext;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::undefined;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::Value::Known;
/// Methods related to the option `evaludate`.
impl Optimizer<'_> {
/// Evaludate expression if possible.
///
/// This method call apppropriate methods for each ast types.
pub(super) fn evaluate(&mut self, e: &mut Expr) {
self.eval_global_vars(e);
self.eval_numbers(e);
self.eval_str_method_call(e);
}
fn eval_global_vars(&mut self, e: &mut Expr) {
if self.options.ie8 {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
return;
}
// We should not convert used-defined `undefined` to `void 0`.
match e {
Expr::Ident(i) => {
if self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
.map(|var| var.declared)
.unwrap_or(false)
{
return;
}
}
_ => {}
}
match e {
Expr::Ident(Ident {
span,
sym: js_word!("undefined"),
..
}) => {
log::trace!("evaluate: `undefined` -> `void 0`");
self.changed = true;
*e = *undefined(*span);
return;
}
Expr::Ident(Ident {
span,
sym: js_word!("Infinity"),
..
}) => {
log::trace!("evaluate: `Infinity` -> `1 / 0`");
self.changed = true;
*e = Expr::Bin(BinExpr {
span: span.with_ctxt(self.done_ctxt),
op: op!("/"),
left: Box::new(Expr::Lit(Lit::Num(Number {
span: DUMMY_SP,
value: 1.0,
}))),
right: Box::new(Expr::Lit(Lit::Num(Number {
span: DUMMY_SP,
value: 0.0,
}))),
});
return;
}
_ => {}
}
}
fn eval_str_method_call(&mut self, e: &mut Expr) {
if !self.options.evaluate {
return;
}
let call = match e {
Expr::Call(v) => v,
_ => return,
};
let (s, method) = match &call.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Expr(callee) => match &**callee {
Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj),
prop,
computed: false,
..
}) => match (&**obj, &**prop) {
(Expr::Lit(Lit::Str(s)), Expr::Ident(prop)) => (s.clone(), prop.sym.clone()),
_ => return,
},
_ => return,
},
};
let new_val = match &*method {
"toLowerCase" => s.value.to_lowercase(),
"toUpperCase" => s.value.to_uppercase(),
"charCodeAt" => {
if call.args.len() != 1 {
return;
}
if let Expr::Lit(Lit::Num(Number { value, .. })) = &*call.args[0].expr {
if value.fract() != 0.0 {
return;
}
let idx = value.round() as i64 as usize;
let c = s.value.chars().nth(idx);
match c {
Some(v) => {
self.changed = true;
log::trace!("evaluate: Evaluated `charCodeAt` of a string literal",);
*e = Expr::Lit(Lit::Num(Number {
span: call.span,
value: v as usize as f64,
}))
}
None => {}
}
}
return;
}
_ => return,
};
self.changed = true;
log::trace!("evaluate: Evaluated `{}` of a string literal", method);
*e = Expr::Lit(Lit::Str(Str {
value: new_val.into(),
..s
}));
}
fn eval_numbers(&mut self, e: &mut Expr) {
if !self.options.evaluate {
return;
}
if self.ctx.is_delete_arg || self.ctx.is_update_arg || self.ctx.is_lhs_of_assign {
return;
}
match e {
Expr::Bin(bin @ BinExpr { op: op!("/"), .. }) => {
let ln = bin.left.as_number();
let rn = bin.right.as_number();
match (ln, rn) {
(Known(ln), Known(rn)) => {
// Prefer `0/0` over NaN.
if ln == 0.0 && rn == 0.0 {
return;
}
// Prefer `1/0` over Infinity.
if ln == 1.0 && rn == 0.0 {
return;
}
// It's NaN
match (ln.classify(), rn.classify()) {
(FpCategory::Zero, FpCategory::Zero) => {
// If a variable named `NaN` is in scope, don't convert e into NaN.
if self
.data
.as_ref()
.map(|data| {
data.vars.iter().any(|(name, v)| {
v.declared && name.0 == js_word!("NaN")
})
})
.unwrap_or(false)
{
return;
}
self.changed = true;
log::trace!("evaluate: `0 / 0` => `NaN`");
// Sign does not matter for NaN
*e = Expr::Ident(Ident::new(
js_word!("NaN"),
bin.span.with_ctxt(SyntaxContext::empty()),
));
return;
}
(FpCategory::Normal, FpCategory::Zero) => {
self.changed = true;
log::trace!("evaluate: `constant / 0` => `Infinity`");
// Sign does not matter for NaN
*e = if ln.is_sign_positive() == rn.is_sign_positive() {
Expr::Ident(Ident::new(
js_word!("Infinity"),
bin.span.with_ctxt(SyntaxContext::empty()),
))
} else {
Expr::Unary(UnaryExpr {
span: bin.span,
op: op!(unary, "-"),
arg: Box::new(Expr::Ident(Ident::new(
js_word!("NaN"),
bin.span.with_ctxt(SyntaxContext::empty()),
))),
})
};
return;
}
_ => {}
}
return;
}
_ => {}
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,189 @@
use super::Optimizer;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
/// Methods related to the option `hoist_props`.
impl Optimizer<'_> {
/// Store values of properties so we can replace property accesses with the
/// values.
pub(super) fn store_var_for_prop_hoisting(&mut self, n: &mut VarDeclarator) {
if !self.options.hoist_props && !self.options.reduce_vars {
return;
}
match &mut n.name {
Pat::Ident(name) => {
// If a variable is initialized multiple time, we currently don't do anything
// smart.
if !self
.data
.as_ref()
.and_then(|data| data.vars.get(&name.to_id()).map(|v| !v.reassigned))
.unwrap_or(false)
{
return;
}
// We should abort if unknown property is used.
let mut unknown_used_props = self
.data
.as_ref()
.and_then(|data| {
data.vars
.get(&name.to_id())
.map(|v| v.accessed_props.clone())
})
.unwrap_or_default();
match n.init.as_deref() {
Some(Expr::Object(init)) => {
for prop in &init.props {
let prop = match prop {
PropOrSpread::Spread(_) => continue,
PropOrSpread::Prop(prop) => prop,
};
match &**prop {
Prop::KeyValue(p) => {
match &*p.value {
Expr::Lit(..) => {}
_ => continue,
};
match &p.key {
PropName::Str(s) => {
unknown_used_props.remove(&s.value);
}
PropName::Ident(i) => {
unknown_used_props.remove(&i.sym);
}
_ => {}
}
}
_ => {}
}
}
}
_ => {}
}
if !unknown_used_props.is_empty() {
return;
}
match n.init.as_deref() {
Some(Expr::Object(init)) => {
for prop in &init.props {
let prop = match prop {
PropOrSpread::Spread(_) => continue,
PropOrSpread::Prop(prop) => prop,
};
match &**prop {
Prop::KeyValue(p) => {
let value = match &*p.value {
Expr::Lit(..) => p.value.clone(),
_ => continue,
};
match &p.key {
PropName::Str(s) => {
log::trace!(
"hoist_props: Storing a varaible to inline \
properties"
);
self.simple_props
.insert((name.to_id(), s.value.clone()), value);
}
PropName::Ident(i) => {
log::trace!(
"hoist_props: Storing a varaible to inline \
properties"
);
self.simple_props
.insert((name.to_id(), i.sym.clone()), value);
}
_ => {}
}
}
_ => {}
}
}
}
_ => {}
}
// If the variable is used multiple time, just ignore it.
if !self
.data
.as_ref()
.and_then(|data| {
data.vars
.get(&name.to_id())
.map(|v| v.ref_count == 1 && v.has_property_access)
})
.unwrap_or(false)
{
return;
}
let init = match n.init.take() {
Some(v) => v,
None => return,
};
match self.vars_for_prop_hoisting.insert(name.to_id(), init) {
Some(prev) => {
panic!(
"two variable with same name and same span hygiene is \
invalid\nPrevious value: {:?}",
prev
);
}
None => {}
}
}
_ => {}
}
}
/// Replace property accesses to known values.
pub(super) fn replace_props(&mut self, e: &mut Expr) {
let member = match e {
Expr::Member(m) => m,
_ => return,
};
match &member.obj {
ExprOrSuper::Super(_) => {}
ExprOrSuper::Expr(obj) => match &**obj {
Expr::Ident(obj) => {
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");
return;
}
if member.computed {
return;
}
match &*member.prop {
Expr::Ident(prop) => {
if let Some(value) =
self.simple_props.get(&(obj.to_id(), prop.sym.clone()))
{
log::trace!("hoist_props: Inlining `{}.{}`", obj.sym, prop.sym);
self.changed = true;
*e = *value.clone()
}
}
_ => {}
}
}
_ => {}
},
}
}
}

View File

@ -0,0 +1,325 @@
use super::Optimizer;
use crate::compress::optimize::is_pure_undefined;
use crate::util::ExprOptExt;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::undefined;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
/// Methods related to the option `if_return`. All methods are noop if
/// `if_return` is false.
impl Optimizer<'_> {
/// Merge simple return statements in if statements.
///
/// # Example
///
/// ## Input
///
/// ```js
/// function foo() {
/// if (a) return foo();
/// return bar()
/// }
/// ```
///
/// ## Output
///
/// ```js
/// function foo() {
/// 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() {
return;
}
{
let return_count = count_leaping_returns(&*stmts);
let ends_with_if = stmts
.last()
.map(|stmt| match stmt.as_stmt() {
Some(Stmt::If(..)) => true,
_ => false,
})
.unwrap();
if return_count <= 1 && ends_with_if {
return;
}
}
{
let start = stmts
.iter()
.position(|stmt| match stmt.as_stmt() {
Some(v) => self.can_merge_stmt_as_if_return(v),
None => false,
})
.unwrap_or(0);
let ends_with_mergable = stmts
.last()
.map(|stmt| match stmt.as_stmt() {
Some(Stmt::Return(..)) | Some(Stmt::Expr(..)) => true,
_ => false,
})
.unwrap();
if stmts.len() - start == 1 && ends_with_mergable {
return;
}
let can_merge = stmts.iter().skip(start).all(|stmt| match stmt.as_stmt() {
Some(s) => self.can_merge_stmt_as_if_return(s),
_ => false,
});
if !can_merge {
return;
}
}
log::trace!("if_return: Merging returns");
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);
continue;
}
};
let is_nonconditional_return = match stmt {
Stmt::Return(..) => true,
_ => false,
};
let new_expr = self.merge_if_returns_to(stmt, vec![]);
match new_expr {
Expr::Seq(v) => match &mut cur {
Some(cur) => match &mut **cur {
Expr::Cond(cur) => {
let seq = get_rightmost_alt_of_cond(cur).force_seq();
seq.exprs.extend(v.exprs);
}
Expr::Seq(cur) => {
cur.exprs.extend(v.exprs);
}
_ => {
unreachable!(
"if_return: cur must be one of None, Expr::Seq or Expr::Cond(with \
alt Expr::Seq)"
)
}
},
None => cur = Some(Box::new(Expr::Seq(v))),
},
Expr::Cond(v) => match &mut cur {
Some(cur) => match &mut **cur {
Expr::Cond(cur) => {
let alt = get_rightmost_alt_of_cond(cur);
let (span, exprs) = {
let prev_seq = alt.force_seq();
prev_seq.exprs.push(v.test);
let exprs = prev_seq.exprs.take();
(prev_seq.span, exprs)
};
*alt = Expr::Cond(CondExpr {
span: DUMMY_SP,
test: Box::new(Expr::Seq(SeqExpr { span, exprs })),
cons: v.cons,
alt: v.alt,
});
}
Expr::Seq(prev_seq) => {
prev_seq.exprs.push(v.test);
let exprs = prev_seq.exprs.take();
*cur = Box::new(Expr::Cond(CondExpr {
span: DUMMY_SP,
test: Box::new(Expr::Seq(SeqExpr {
span: prev_seq.span,
exprs,
})),
cons: v.cons,
alt: v.alt,
}));
}
_ => {
unreachable!(
"if_return: cur must be one of None, Expr::Seq or Expr::Cond(with \
alt Expr::Seq)"
)
}
},
None => cur = Some(Box::new(Expr::Cond(v))),
},
_ => {
unreachable!(
"if_return: merge_if_returns_to should return one of None, Expr::Seq or \
Expr::Cond"
)
}
}
if is_nonconditional_return {
break;
}
}
if let Some(mut cur) = cur {
match &*cur {
Expr::Seq(seq)
if seq
.exprs
.last()
.map(|v| is_pure_undefined(&v))
.unwrap_or(true) =>
{
let expr = self.ignore_return_value(&mut cur);
if let Some(cur) = expr {
new.push(T::from_stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(cur),
})))
}
}
_ => {
new.push(T::from_stmt(Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(cur),
})));
}
}
}
*stmts = new;
}
/// This method returns [Expr::Seq] or [Expr::Cond].
///
/// `exprs` is a simple optimization.
fn merge_if_returns_to(&mut self, stmt: Stmt, mut exprs: Vec<Box<Expr>>) -> Expr {
//
match stmt {
Stmt::If(IfStmt {
span,
test,
cons,
alt,
..
}) => {
let cons = Box::new(self.merge_if_returns_to(*cons, vec![]));
let alt = match alt {
Some(alt) => Box::new(self.merge_if_returns_to(*alt, vec![])),
None => undefined(DUMMY_SP),
};
exprs.push(test);
Expr::Cond(CondExpr {
span,
test: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
})),
cons,
alt,
})
}
Stmt::Expr(stmt) => {
exprs.push(Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("void"),
arg: stmt.expr,
})));
Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
})
}
Stmt::Return(stmt) => {
let span = stmt.span;
exprs.push(stmt.arg.unwrap_or_else(|| undefined(span)));
Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
})
}
_ => unreachable!(),
}
}
fn can_merge_stmt_as_if_return(&self, s: &Stmt) -> bool {
match s {
Stmt::Expr(..) | Stmt::Return(..) => true,
Stmt::If(stmt) if self.options.conditionals => {
self.can_merge_stmt_as_if_return(&stmt.cons)
&& stmt
.alt
.as_deref()
.map(|s| self.can_merge_stmt_as_if_return(s))
.unwrap_or(true)
}
_ => false,
}
}
}
fn get_rightmost_alt_of_cond(e: &mut CondExpr) -> &mut Expr {
match &mut *e.alt {
Expr::Cond(alt) => get_rightmost_alt_of_cond(alt),
alt => alt,
}
}
fn count_leaping_returns<N>(n: &N) -> usize
where
N: VisitWith<ReturnFinder>,
{
let mut v = ReturnFinder::default();
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.count
}
#[derive(Default)]
pub(super) struct ReturnFinder {
count: usize,
}
impl Visit for ReturnFinder {
noop_visit_type!();
fn visit_return_stmt(&mut self, n: &ReturnStmt, _: &dyn Node) {
n.visit_children_with(self);
self.count += 1;
}
fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
}

View File

@ -0,0 +1,297 @@
use super::Optimizer;
use crate::compress::optimize::Ctx;
use std::mem::swap;
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::undefined;
use swc_ecma_utils::ExprExt;
use swc_ecma_visit::VisitMutWith;
/// 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 {
return;
}
let expr = match e {
Expr::Call(e) => e,
_ => return,
};
let callee = match &mut expr.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Expr(e) => &mut **e,
};
match callee {
Expr::Fn(..) => {
log::trace!("negate_iife: Negating iife");
*e = Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: Box::new(e.take()),
});
return;
}
_ => {}
}
}
///
/// - `iife ? foo : bar` => `!iife ? bar : foo`
pub(super) fn negate_iife_in_cond(&mut self, e: &mut Expr) {
let cond = match e {
Expr::Cond(v) => v,
_ => return,
};
let test_call = match &mut *cond.test {
Expr::Call(e) => e,
_ => return,
};
let callee = match &mut test_call.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Expr(e) => &mut **e,
};
match callee {
Expr::Fn(..) => {
log::trace!("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;
}
_ => {}
}
}
}
/// Methods related to iife.
impl Optimizer<'_> {
/// # Exmaple
///
/// ## Input
///
/// ```ts
/// (function(x) {
/// (function(y) {
/// console.log(7);
/// })(7);
/// })(7);
/// ```
///
///
/// ## Output
///
/// ```ts
/// (function(x) {
/// (function(y) {
/// console.log(y);
/// })(x);
/// })(7);
/// ```
pub(super) fn inline_args_of_iife(&mut self, e: &mut CallExpr) {
if self.options.inline == 0 {
return;
}
let has_spread_arg = e.args.iter().any(|v| v.spread.is_some());
if has_spread_arg {
return;
}
match &mut e.callee {
ExprOrSuper::Super(_) => {}
ExprOrSuper::Expr(callee) => match &mut **callee {
Expr::Fn(callee) => {
// We check for parameter and argument
for (idx, param) in callee.function.params.iter().enumerate() {
let arg = e.args.get(idx).map(|v| &v.expr);
if let Pat::Ident(param) = &param.pat {
if let Some(arg) = arg {
let should_be_inlined = self.can_be_inlined_for_iife(arg);
if should_be_inlined {
self.lits.insert(param.to_id(), arg.clone());
}
}
}
}
let ctx = Ctx {
inline_prevented: false,
..self.ctx
};
callee.function.visit_mut_with(&mut *self.with_ctx(ctx));
// TODO: Drop arguments if all usage is inlined. (We
// should preserve parameters)
}
_ => {}
},
}
}
/// Fully inlines iife.
///
/// # Example
///
/// ## Input
///
/// ```ts
/// (function () {
/// return {};
/// })().x = 10;
/// ```
///
/// ## Oupuy
///
/// ```ts
/// ({
/// }).x = 10;
/// ```
pub(super) fn invoke_iife(&mut self, e: &mut Expr) {
if self.options.inline == 0 {
return;
}
if self.ctx.inline_prevented {
return;
}
let expr = match e {
Expr::Call(v) => v,
_ => return,
};
let callee = match &mut expr.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Expr(e) => &mut **e,
};
if expr.args.iter().any(|arg| arg.expr.may_have_side_effects()) {
return;
}
match callee {
Expr::Arrow(f) => {
// TODO: Improve this.
if !f.params.is_empty() {
return;
}
match &mut f.body {
BlockStmtOrExpr::BlockStmt(_) => {
// TODO
}
BlockStmtOrExpr::Expr(body) => {
match &**body {
Expr::Lit(Lit::Num(..)) => {
if self.ctx.in_obj_of_non_computed_member {
return;
}
}
_ => {}
}
self.changed = true;
log::trace!("inline: Inlining a call to an arrow function");
*e = *body.take();
return;
}
}
}
Expr::Fn(f) => {
if f.function.is_generator {
return;
}
// Abort if a parameter is complex
if f.function.params.iter().any(|param| match param.pat {
Pat::Object(..) | Pat::Array(..) | Pat::Assign(..) | Pat::Rest(..) => true,
_ => false,
}) {
return;
}
let body = f.function.body.as_mut().unwrap();
if body.stmts.is_empty() {
*e = *undefined(f.function.span);
return;
}
if !body.stmts.iter().all(|stmt| match stmt {
Stmt::Expr(..) => true,
Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
Some(Expr::Lit(Lit::Num(..))) => {
if self.ctx.in_obj_of_non_computed_member {
false
} else {
true
}
}
_ => true,
},
_ => false,
}) {
return;
}
self.changed = true;
log::trace!("inline: Inlining a function call");
//
let mut exprs = vec![];
for stmt in body.stmts.take() {
match stmt {
Stmt::Expr(stmt) => {
exprs.push(stmt.expr);
}
Stmt::Return(stmt) => {
let span = stmt.span;
*e = *stmt.arg.unwrap_or_else(|| undefined(span));
return;
}
_ => {}
}
}
if let Some(last) = exprs.last_mut() {
*last = Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("void"),
arg: last.take(),
}));
} else {
*e = *undefined(f.function.span);
return;
}
*e = Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
})
}
_ => {}
}
}
fn can_be_inlined_for_iife(&self, arg: &Expr) -> bool {
match arg {
Expr::Lit(..) => true,
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => self.can_be_inlined_for_iife(&arg),
_ => false,
}
}
}

View File

@ -0,0 +1,383 @@
use super::Optimizer;
use swc_atoms::js_word;
use swc_common::Spanned;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::ExprExt;
/// Methods related to option `inline`.
impl Optimizer<'_> {
/// Stores the value of a variable to inline it.
///
/// This method may remove value of initializer. It mean that the value will
/// be inlined and should be removed from [Vec<VarDeclarator>].
pub(super) fn store_var_for_inining(&mut self, var: &mut VarDeclarator) {
let init = match &mut var.init {
Some(v) => v,
None => return,
};
let should_preserve = (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level();
// TODO: Check for side effect between original decl position and inlined
// position
// We will inline if possible.
match &var.name {
Pat::Ident(i) => {
if i.id.sym == *"arguments" {
return;
}
if self.options.top_retain.contains(&i.id.sym) {
return;
}
// Store variables if it's used only once
if let Some(usage) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
{
if usage.declared_as_catch_param {
return;
}
if should_preserve && usage.var_kind != Some(VarDeclKind::Const) {
return;
}
if usage.cond_init || usage.used_above_decl {
return;
}
if self.options.reduce_vars && self.options.typeofs && !usage.reassigned {
match &**init {
Expr::Fn(..) | Expr::Arrow(..) => {
self.typeofs.insert(i.to_id(), js_word!("function"));
}
Expr::Array(..) | Expr::Object(..) => {
self.typeofs.insert(i.to_id(), js_word!("object"));
}
_ => {}
}
}
// No use => doppred
if usage.ref_count == 0 {
if init.may_have_side_effects() {
// TODO: Inline partially
return;
}
// TODO: Remove
return;
}
let is_inline_enabled = self.options.reduce_vars
|| self.options.collapse_vars
|| self.options.inline != 0;
if is_inline_enabled
&& !usage.reassigned
&& match &**init {
Expr::Lit(lit) => match lit {
Lit::Str(_)
| Lit::Bool(_)
| Lit::Null(_)
| Lit::Num(_)
| Lit::BigInt(_) => true,
Lit::Regex(_) => self.options.unsafe_regexp,
_ => false,
},
_ => false,
}
{
log::trace!(
"inline: Decided to inline '{}{:?}' because it's simple",
i.id.sym,
i.id.span.ctxt
);
if self.options.inline != 0 && !should_preserve {
self.lits.insert(i.to_id(), init.take());
} else {
self.lits.insert(i.to_id(), init.clone());
}
return;
}
// Single use => inlined
if is_inline_enabled
&& !should_preserve
&& !usage.reassigned
&& usage.ref_count == 1
{
match &**init {
Expr::Fn(FnExpr {
function: Function { is_async: true, .. },
..
})
| Expr::Fn(FnExpr {
function:
Function {
is_generator: true, ..
},
..
})
| Expr::Arrow(ArrowExpr { is_async: true, .. })
| Expr::Arrow(ArrowExpr {
is_generator: true, ..
}) => return,
_ => {}
}
match &**init {
Expr::Lit(Lit::Regex(..)) => return,
_ => {}
}
if init.may_have_side_effects() {
// TODO: Inline partially
return;
}
log::trace!(
"inline: Decided to inline '{}{:?}' because it's used only once",
i.id.sym,
i.id.span.ctxt
);
self.changed = true;
self.vars_for_inlining.insert(i.to_id(), init.take());
return;
}
}
}
// TODO
_ => {}
}
}
/// Check if the body of a function is simple enough to inline.
fn is_fn_body_simple_enough_to_inline(&self, body: &BlockStmt) -> bool {
if body.stmts.len() == 1 {
match &body.stmts[0] {
Stmt::Expr(ExprStmt { expr, .. }) => match &**expr {
Expr::Lit(..) => return true,
_ => {}
},
Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
Some(Expr::Lit(Lit::Num(..))) => return true,
_ => {}
},
_ => {}
}
}
false
}
/// Stores `typeof` of [ClassDecl] and [FnDecl].
pub(super) fn store_typeofs(&mut self, decl: &mut Decl) {
if !self.options.reduce_vars || !self.options.typeofs {
return;
}
let i = match &*decl {
Decl::Class(v) => v.ident.clone(),
Decl::Fn(f) => f.ident.clone(),
_ => return,
};
if i.sym == *"arguments" {
return;
}
if let Some(usage) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
{
if !usage.reassigned {
log::trace!("typeofs: Storing typeof `{}{:?}`", i.sym, i.span.ctxt);
match &*decl {
Decl::Fn(..) => {
self.typeofs.insert(i.to_id(), js_word!("function"));
}
Decl::Class(..) => {
self.typeofs.insert(i.to_id(), js_word!("object"));
}
_ => {}
}
}
}
}
/// This method handles only [ClassDecl] and [FnDecl]. [VarDecl] should be
/// handled specially.
pub(super) fn store_decl_for_inlining(&mut self, decl: &mut Decl) {
if self.options.inline == 0 && !self.options.reduce_vars {
return;
}
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
return;
}
if self.has_noinline(decl.span()) {
return;
}
let i = match &*decl {
Decl::Class(v) => v.ident.clone(),
Decl::Fn(f) => {
if f.function.is_async {
return;
}
f.ident.clone()
}
_ => return,
};
// Respect `top_retain`
if self.ctx.in_top_level() && self.options.top_retain.contains(&i.sym) {
return;
}
if let Some(usage) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
{
if usage.declared_as_catch_param {
return;
}
// Inline very simple functions.
match decl {
Decl::Fn(f) if self.options.inline >= 2 && f.ident.sym != *"arguments" => {
match &f.function.body {
Some(body) => {
if self.is_fn_body_simple_enough_to_inline(body) {
log::trace!(
"inline: Decided to inline function '{}{:?}' as it's very \
simple",
f.ident.sym,
f.ident.span.ctxt
);
self.lits.insert(
i.to_id(),
match decl {
Decl::Fn(f) => Box::new(Expr::Fn(FnExpr {
ident: Some(f.ident.clone()),
function: f.function.clone(),
})),
_ => {
unreachable!()
}
},
);
return;
}
}
None => {}
}
}
_ => {}
}
// Single use => inlined
if (self.options.reduce_vars || self.options.collapse_vars || self.options.inline != 0)
&& usage.ref_count == 1
&& usage.is_fn_local
&& !usage.used_in_loop
{
match decl {
Decl::Class(ClassDecl {
class:
Class {
super_class: Some(super_class),
..
},
..
}) => {
if super_class.may_have_side_effects() {
return;
}
}
_ => {}
}
self.changed = true;
match &decl {
Decl::Class(c) => {
log::trace!(
"inline: Decided to inline class '{}{:?}' as it's used only once",
c.ident.sym,
c.ident.span.ctxt
);
}
Decl::Fn(f) => {
log::trace!(
"inline: Decided to inline function '{}{:?}' as it's used only once",
f.ident.sym,
f.ident.span.ctxt
);
}
_ => {}
}
self.vars_for_inlining.insert(
i.to_id(),
match decl.take() {
Decl::Class(c) => Box::new(Expr::Class(ClassExpr {
ident: Some(c.ident),
class: c.class,
})),
Decl::Fn(f) => Box::new(Expr::Fn(FnExpr {
ident: Some(f.ident),
function: f.function,
})),
_ => {
unreachable!()
}
},
);
return;
}
}
}
/// Actually inlines variables.
pub(super) fn inline(&mut self, e: &mut Expr) {
if self.ctx.inline_prevented {
return;
}
match e {
Expr::Ident(i) => {
if self.has_noinline(i.span) {
return;
}
//
if let Some(value) = self.lits.get(&i.to_id()).cloned() {
self.changed = true;
log::trace!("inline: Replacing a variable with cheap expression");
*e = *value;
} else if let Some(value) = self.vars_for_inlining.remove(&i.to_id()) {
self.changed = true;
log::trace!(
"inline: Replacing '{}{:?}' with an expression",
i.sym,
i.span.ctxt
);
*e = *value;
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,114 @@
use super::Optimizer;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::StmtLike;
/// Methods related to option `join_vars`.
impl Optimizer<'_> {
/// Join variables.
///
/// This method may move variables to head of for statements like
///
/// `var a; for(var b;;);` => `for(var a, b;;);`
pub(super) fn join_vars<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
if !self.options.join_vars {
return;
}
{
// Check if we can join variables.
let can_work =
stmts
.windows(2)
.any(|stmts| match (stmts[0].as_stmt(), stmts[1].as_stmt()) {
(Some(Stmt::Decl(Decl::Var(l))), Some(r)) => match r {
Stmt::Decl(Decl::Var(r)) => l.kind == r.kind,
Stmt::For(ForStmt { init: None, .. }) => l.kind == VarDeclKind::Var,
Stmt::For(ForStmt {
init:
Some(VarDeclOrExpr::VarDecl(VarDecl {
kind: VarDeclKind::Var,
..
})),
..
}) => l.kind == VarDeclKind::Var,
_ => false,
},
_ => false,
});
if !can_work {
return;
}
}
log::trace!("join_vars: Joining variables");
self.changed = true;
let mut cur: Option<VarDecl> = None;
let mut new = vec![];
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => match stmt {
Stmt::Decl(Decl::Var(var)) => match &mut cur {
Some(v) if var.kind == v.kind => {
v.decls.extend(var.decls);
}
_ => {
new.extend(cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt));
cur = Some(var)
}
},
Stmt::For(mut stmt) => match &mut stmt.init {
Some(VarDeclOrExpr::VarDecl(var)) => match &mut cur {
Some(cur) if cur.kind == var.kind => {
// Merge
cur.decls.append(&mut var.decls);
var.decls = cur.decls.take();
new.push(T::from_stmt(Stmt::For(stmt)))
}
_ => {
new.extend(
cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt),
);
new.push(T::from_stmt(Stmt::For(stmt)))
}
},
None => {
stmt.init = cur.take().map(VarDeclOrExpr::VarDecl);
new.push(T::from_stmt(Stmt::For(stmt)))
}
_ => {
new.extend(cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt));
new.push(T::from_stmt(Stmt::For(stmt)))
}
},
_ => {
new.extend(cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt));
new.push(T::from_stmt(stmt))
}
},
Err(item) => {
new.extend(cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt));
new.push(item);
}
}
}
new.extend(cur.take().map(Decl::Var).map(Stmt::Decl).map(T::from_stmt));
*stmts = new;
}
}

View File

@ -0,0 +1,188 @@
use crate::compress::optimize::unused::UnreachableHandler;
use crate::compress::optimize::Optimizer;
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::Value::Known;
/// Methods related to the option `loops`.
impl Optimizer<'_> {
///
/// - `while(test);` => `for(;;test);
/// - `do; while(true)` => `for(;;);
pub(super) fn loop_to_for_stmt(&mut self, s: &mut Stmt) {
if !self.options.loops {
return;
}
match s {
Stmt::While(stmt) => {
self.changed = true;
log::trace!("loops: Converting a while loop to a for loop");
*s = Stmt::For(ForStmt {
span: stmt.span,
init: None,
test: Some(stmt.test.take()),
update: None,
body: stmt.body.take(),
});
}
Stmt::DoWhile(stmt) => {
let val = stmt.test.as_pure_bool();
if let Known(true) = val {
self.changed = true;
log::trace!("loops: Converting an always-true do-while loop to a for loop");
*s = Stmt::For(ForStmt {
span: stmt.span,
init: None,
test: Some(stmt.test.take()),
update: None,
body: stmt.body.take(),
});
}
}
_ => {}
}
}
/// `for(a;b;c;) break;` => `a;b;`
pub(super) fn optimize_loops_with_break(&mut self, s: &mut Stmt) {
if !self.options.loops {
return;
}
// As we normalize loops, this is enough.
let f = match s {
Stmt::For(v) => v,
_ => return,
};
// We only care about instant breaks.
if !f.body.is_break_stmt() {
return;
}
self.changed = true;
log::trace!("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)),
VarDeclOrExpr::Expr(expr) => Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr,
}),
}));
self.prepend_stmts.extend(f.test.take().map(|expr| {
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr,
})
}));
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP })
}
///
/// - `while(false) { var a; foo() }` => `var a;`
pub(super) fn optiimze_loops_if_cond_is_false(&mut self, stmt: &mut Stmt) {
if !self.options.loops {
return;
}
match stmt {
Stmt::While(w) => {
let (purity, val) = w.test.as_bool();
if let Known(false) = val {
if purity.is_pure() {
let changed = UnreachableHandler::preserve_vars(stmt);
self.changed |= changed;
if changed {
log::trace!(
"loops: Removing unreachable while statement without side effects"
);
}
} else {
let changed = UnreachableHandler::preserve_vars(&mut w.body);
self.changed |= changed;
if changed {
log::trace!("loops: Removing unreachable body of a while statement");
}
}
}
}
Stmt::For(f) => {
if let Some(test) = &mut f.test {
let (purity, val) = test.as_bool();
if let Known(false) = val {
let changed = UnreachableHandler::preserve_vars(&mut f.body);
self.changed |= changed;
if changed {
log::trace!("loops: Removing unreachable body of a for statement");
}
self.changed |= f.init.is_some() | f.update.is_some();
self.prepend_stmts
.extend(f.init.take().map(|init| match init {
VarDeclOrExpr::VarDecl(var) => Stmt::Decl(Decl::Var(var)),
VarDeclOrExpr::Expr(expr) => Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr,
}),
}));
self.prepend_stmts.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: f.test.take().unwrap(),
}));
f.update = None;
*stmt = *f.body.take();
return;
} else if let Known(true) = val {
if purity.is_pure() {
self.changed = true;
log::trace!(
"loops: Remving `test` part of a for stmt as it's always true"
);
f.test = None;
}
}
}
}
_ => {}
}
}
pub(super) fn drop_if_break(&mut self, _s: &ForStmt) {
if !self.options.loops {
return;
}
}
///
/// - `for (a(), 5; b(); c())` => `for (a(); b(); c())`
pub(super) fn optimize_init_of_for_stmt(&mut self, s: &mut ForStmt) {
if !self.options.side_effects {
return;
}
match &mut s.init {
Some(init) => match init {
VarDeclOrExpr::VarDecl(_) => {}
VarDeclOrExpr::Expr(init) => {
let new = self.ignore_return_value(&mut **init);
if let Some(new) = new {
*init = Box::new(new);
return;
} else {
s.init = None;
self.changed = true;
log::trace!(
"loops: Removed side-effect-free expressions in `init` of a for stmt"
);
}
}
},
None => {}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,462 @@
use super::Optimizer;
use crate::util::ValueExt;
use std::mem::swap;
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;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::Type;
use swc_ecma_utils::Value;
use Value::Known;
impl Optimizer<'_> {
///
/// - `1 == 1` => `true`
pub(super) fn optimize_lit_cmp(&mut self, n: &mut BinExpr) -> Option<Expr> {
match n.op {
op!("==") | op!("!=") => {
// Abort if types differ, or one of them is unknown.
if n.left.get_type().opt()? != n.right.get_type().opt()? {
return None;
}
let l = n.left.as_pure_bool().opt()?;
let r = n.right.as_pure_bool().opt()?;
let value = if n.op == op!("==") { l == r } else { l != r };
log::trace!("Optimizing: literal comparison => bool");
self.changed = true;
return Some(Expr::Lit(Lit::Bool(Bool {
span: n.span,
value,
})));
}
_ => {}
}
None
}
///
/// - `!!(a in b)` => `a in b`
/// - `!!(function() {})()` => `!(function() {})()`
pub(super) fn optimize_bangbang(&mut self, e: &mut Expr) {
match e {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => match &mut **arg {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => match &**arg {
Expr::Unary(UnaryExpr { op: op!("!"), .. })
| Expr::Bin(BinExpr { op: op!("in"), .. })
| Expr::Bin(BinExpr {
op: op!("instanceof"),
..
})
| Expr::Bin(BinExpr { op: op!("=="), .. })
| Expr::Bin(BinExpr { op: op!("!="), .. })
| Expr::Bin(BinExpr { op: op!("==="), .. })
| Expr::Bin(BinExpr { op: op!("!=="), .. })
| Expr::Bin(BinExpr { op: op!("<="), .. })
| Expr::Bin(BinExpr { op: op!("<"), .. })
| Expr::Bin(BinExpr { op: op!(">="), .. })
| Expr::Bin(BinExpr { op: op!(">"), .. }) => {
log::trace!("Optimizing: `!!expr` => `expr`");
*e = *arg.take();
return;
}
_ => {}
},
_ => {}
},
_ => {}
}
}
/// Convert expressions to string literal if possible.
pub(super) fn optimize_expr_in_str_ctx(&mut self, n: &mut Expr) {
match n {
Expr::Lit(Lit::Str(..)) => return,
_ => {}
}
let span = n.span();
let value = n.as_string();
if let Known(value) = value {
*n = Expr::Lit(Lit::Str(Str {
span,
value: value.into(),
has_escape: false,
kind: Default::default(),
}))
}
}
/// TODO: Optimize based on the type.
pub(super) fn negate_twice(&mut self, e: &mut Expr) {
self.negate(e);
self.negate(e);
}
/// Creates `!e` where e is the expression passed as an argument.
///
/// # Note
///
/// This method returns `!e` if `!!e` is given as a argument.
///
/// TODO: Handle special cases like !1 or !0
pub(super) fn negate(&mut self, e: &mut Expr) {
self.changed = true;
let arg = Box::new(e.take());
match e {
Expr::Bin(bin @ BinExpr { op: op!("=="), .. })
| Expr::Bin(bin @ BinExpr { op: op!("!="), .. })
| Expr::Bin(bin @ BinExpr { op: op!("==="), .. })
| Expr::Bin(bin @ BinExpr { op: op!("!=="), .. }) => {
bin.op = match bin.op {
op!("==") => {
op!("!=")
}
op!("!=") => {
op!("==")
}
op!("===") => {
op!("!==")
}
op!("!==") => {
op!("===")
}
_ => {
unreachable!()
}
};
self.changed = true;
log::trace!("negate: binary");
return;
}
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => match &mut **arg {
Expr::Unary(UnaryExpr { op: op!("!"), .. }) => {
log::trace!("negate: !!bool => !bool");
*e = *arg.take();
return;
}
Expr::Bin(BinExpr { op: op!("in"), .. })
| Expr::Bin(BinExpr {
op: op!("instanceof"),
..
}) => {
log::trace!("negate: !bool => bool");
*e = *arg.take();
return;
}
_ => {}
},
_ => {}
}
log::trace!("negate: e => !e");
*e = Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg,
});
}
pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) {
match &mut *n {
Expr::Unary(e @ UnaryExpr { op: op!("!"), .. })
| Expr::Unary(
e @ UnaryExpr {
op: op!("delete"), ..
},
) => {
match &mut *e.arg {
Expr::Seq(SeqExpr { exprs, .. }) => {
if exprs.is_empty() {
return;
}
log::trace!("optimizing negated sequences");
{
let last = exprs.last_mut().unwrap();
self.optimize_expr_in_bool_ctx(last);
// Negate last element.
self.negate(last);
}
*n = *e.arg.take();
}
_ => {}
}
}
_ => {}
}
}
/// This method does
///
/// - `x *= 3` => `x = 3 * x`
/// - `x = 3 | x` `x |= 3`
/// - `x = 3 & x` => `x &= 3;`
/// - `x ^= 3` => `x = 3 ^ x`
pub(super) fn compress_bin_assignment_to_right(&mut self, e: &mut AssignExpr) {
// TODO: Handle pure properties.
let lhs = match &e.left {
PatOrExpr::Expr(e) => match &**e {
Expr::Ident(i) => i,
_ => return,
},
PatOrExpr::Pat(p) => match &**p {
Pat::Ident(i) => &i.id,
_ => return,
},
};
let (op, left) = match &mut *e.right {
Expr::Bin(BinExpr {
left, op, right, ..
}) => match &**right {
Expr::Ident(r) if lhs.sym == r.sym && lhs.span.ctxt == r.span.ctxt => {
// We need this check because a function call like below can change value of
// operand.
//
// x = g() * x;
match &**left {
Expr::This(..) | Expr::Ident(..) | Expr::Lit(..) => {}
_ => return,
}
(op, left)
}
_ => return,
},
_ => return,
};
let op = match op {
BinaryOp::Mul => {
op!("*=")
}
BinaryOp::BitOr => {
op!("|=")
}
BinaryOp::BitXor => {
op!("^=")
}
BinaryOp::BitAnd => {
op!("&=")
}
_ => return,
};
log::trace!("Compressing: `e = 3 & e` => `e &= 3`");
self.changed = true;
e.op = op;
e.right = left.take();
}
pub(super) fn swap_bin_operands(&mut self, expr: &mut Expr) {
// Swap lhs and rhs in certain conditions.
match expr {
Expr::Bin(test) => {
match test.op {
op!("==") | op!("!=") | op!("&") | op!("^") | op!("*") => {}
_ => return,
}
match (&*test.left, &*test.right) {
(&Expr::Ident(..), &Expr::Lit(..)) => {
self.changed = true;
log::trace!("Swapping operands of binary exprssion");
swap(&mut test.left, &mut test.right);
}
_ => {}
}
}
_ => {}
}
}
/// Remove meaningless literals in a binary expressions.
///
///
/// # Examples
///
/// - `x() && true` => `!!x()`
pub(super) fn compress_logical_exprs_as_bang_bang(&mut self, e: &mut Expr) {
if !self.options.conditionals && !self.options.reduce_vars {
return;
}
let bin = match e {
Expr::Bin(bin) => bin,
_ => return,
};
match bin.op {
op!("&&") => {
let rt = bin.right.get_type();
match rt {
Known(Type::Bool) => {}
_ => return,
}
let rb = bin.right.as_pure_bool();
let rb = match rb {
Value::Known(v) => v,
_ => return,
};
log::trace!("Optimizing: e && true => !!e");
if rb {
self.negate(&mut bin.left);
self.negate(&mut bin.left);
*e = *bin.left.take();
}
}
op!("||") => {}
_ => {}
}
}
///
/// - `!(x == y)` => `x != y`
/// - `!(x === y)` => `x !== y`
pub(super) fn compress_negated_bin_eq(&self, e: &mut Expr) {
let unary = match e {
Expr::Unary(e @ UnaryExpr { op: op!("!"), .. }) => e,
_ => return,
};
match &mut *unary.arg {
Expr::Bin(BinExpr {
op: op @ op!("=="),
left,
right,
..
})
| Expr::Bin(BinExpr {
op: op @ op!("==="),
left,
right,
..
}) => {
*e = Expr::Bin(BinExpr {
span: unary.span,
op: if *op == op!("==") {
op!("!=")
} else {
op!("!==")
},
left: left.take(),
right: right.take(),
})
}
_ => {}
}
}
pub(super) fn optimize_nullish_coalescing(&mut self, e: &mut Expr) {
let (l, r) = match e {
Expr::Bin(BinExpr {
op: op!("??"),
left,
right,
..
}) => (&mut **left, &mut **right),
_ => return,
};
match l {
Expr::Lit(Lit::Null(..)) => {
log::trace!("Removing null from lhs of ??");
self.changed = true;
*e = r.take();
return;
}
Expr::Lit(Lit::Num(..))
| Expr::Lit(Lit::Str(..))
| Expr::Lit(Lit::BigInt(..))
| Expr::Lit(Lit::Bool(..))
| Expr::Lit(Lit::Regex(..)) => {
log::trace!("Removing rhs of ?? as lhs cannot be null nor undefined");
self.changed = true;
*e = l.take();
return;
}
_ => {}
}
}
/// `typeof b !== 'undefined'` => `b != void 0`
pub(super) fn compress_typeofs(&mut self, e: &mut Expr) {
if !self.options.typeofs {
return;
}
match e {
Expr::Unary(UnaryExpr {
span,
op: op!("typeof"),
arg,
..
}) => match &**arg {
Expr::Ident(arg) => {
if let Some(value) = self.typeofs.get(&arg.to_id()).cloned() {
log::trace!(
"Converting typeof of variable to literal as we know the value"
);
self.changed = true;
*e = Expr::Lit(Lit::Str(Str {
span: *span,
value,
has_escape: false,
kind: Default::default(),
}));
return;
}
}
Expr::Arrow(..) | Expr::Fn(..) => {
log::trace!("Converting typeof to 'function' as we know the value");
self.changed = true;
*e = Expr::Lit(Lit::Str(Str {
span: *span,
value: js_word!("function"),
has_escape: false,
kind: Default::default(),
}));
return;
}
Expr::Array(..) | Expr::Object(..) => {
log::trace!("Converting typeof to 'object' as we know the value");
self.changed = true;
*e = Expr::Lit(Lit::Str(Str {
span: *span,
value: js_word!("object"),
has_escape: false,
kind: Default::default(),
}));
return;
}
_ => {}
},
_ => {}
}
}
}

View File

@ -0,0 +1,382 @@
use super::Optimizer;
use crate::util::ExprOptExt;
use std::mem::take;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::StmtLike;
/// Methods related to the option `sequences`. All methods are noop if
/// `sequences` is false.
impl Optimizer<'_> {
///
/// # Exmaple
///
///
/// ## Input
///
/// ```ts
/// x = 5;
/// if (y) z();
/// x = 5;
/// for (i = 0; i < 5; i++) console.log(i);
/// x = 5;
/// for (; i < 5; i++) console.log(i);
/// x = 5;
/// switch (y) {
/// }
/// x = 5;
/// with (obj) {
/// }
/// ```
///
/// ## Output
/// ```ts
/// if (x = 5, y) z();
/// for(x = 5, i = 0; i < 5; i++)console.log(i);
/// for(x = 5; i < 5; i++)console.log(i);
/// switch(x = 5, y){
/// }
/// with (x = 5, obj);
/// ```
pub(super) fn make_sequences<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
if !self.options.sequences() {
return;
}
{
let can_work =
stmts
.windows(2)
.any(|stmts| match (stmts[0].as_stmt(), stmts[1].as_stmt()) {
(Some(Stmt::Expr(..)), Some(r)) => {
// If an expression contains `in` and following statement is for loop,
// we should not merge it.
// TODO: Check for `in`
match r {
Stmt::Expr(..)
| Stmt::If(..)
| Stmt::Switch(..)
| Stmt::With(..)
| Stmt::Return(ReturnStmt { arg: Some(..), .. })
| Stmt::Throw(ThrowStmt { .. })
| Stmt::For(ForStmt { init: None, .. })
| Stmt::For(ForStmt {
init: Some(VarDeclOrExpr::Expr(..)),
..
}) => true,
_ => false,
}
}
_ => false,
});
if !can_work {
return;
}
}
log::trace!("sequences: Compressing statements as a sequences");
self.changed = true;
let mut exprs = vec![];
// This is bigger than required.
let mut new_stmts = Vec::with_capacity(stmts.len());
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => {
// If
match stmt {
Stmt::Expr(stmt) => {
exprs.push(stmt.expr);
}
Stmt::If(mut stmt) => {
stmt.test.prepend_exprs(take(&mut exprs));
new_stmts.push(T::from_stmt(Stmt::If(stmt)));
}
Stmt::Switch(mut stmt) => {
stmt.discriminant.prepend_exprs(take(&mut exprs));
new_stmts.push(T::from_stmt(Stmt::Switch(stmt)));
}
Stmt::With(mut stmt) => {
stmt.obj.prepend_exprs(take(&mut exprs));
new_stmts.push(T::from_stmt(Stmt::With(stmt)));
}
Stmt::Return(mut stmt) => {
stmt.arg.as_mut().unwrap().prepend_exprs(take(&mut exprs));
new_stmts.push(T::from_stmt(Stmt::Return(stmt)));
}
Stmt::Throw(mut stmt) => {
stmt.arg.prepend_exprs(take(&mut exprs));
new_stmts.push(T::from_stmt(Stmt::Throw(stmt)));
}
Stmt::For(mut stmt @ ForStmt { init: None, .. })
| Stmt::For(
mut
stmt
@
ForStmt {
init: Some(VarDeclOrExpr::Expr(..)),
..
},
) => {
match &mut stmt.init {
Some(VarDeclOrExpr::Expr(e)) => {
e.prepend_exprs(take(&mut exprs));
}
None => {
stmt.init =
Some(VarDeclOrExpr::Expr(Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: take(&mut exprs),
}))))
}
_ => {
unreachable!()
}
}
new_stmts.push(T::from_stmt(Stmt::For(stmt)));
}
_ => {
if !exprs.is_empty() {
new_stmts.push(T::from_stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: take(&mut exprs),
})),
})))
}
new_stmts.push(T::from_stmt(stmt));
}
}
}
Err(item) => {
if !exprs.is_empty() {
new_stmts.push(T::from_stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: take(&mut exprs),
})),
})))
}
new_stmts.push(item);
}
}
}
if !exprs.is_empty() {
new_stmts.push(T::from_stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: take(&mut exprs),
})),
})))
}
*stmts = new_stmts;
}
///
/// - `x = (foo(), bar(), baz()) ? 10 : 20` => `foo(), bar(), x = baz() ? 10
/// : 20;`
pub(super) fn lift_seqs_of_cond_assign(&mut self, e: &mut Expr) {
if !self.options.sequences() {
return;
}
let assign = match e {
Expr::Assign(v) => v,
_ => return,
};
let cond = match &mut *assign.right {
Expr::Cond(v) => v,
_ => return,
};
match &mut *cond.test {
Expr::Seq(test) => {
//
if test.exprs.len() >= 2 {
let mut new_seq = vec![];
new_seq.extend(test.exprs.drain(..test.exprs.len() - 1));
self.changed = true;
log::trace!("sequences: Lifting sequences in a assignment with cond expr");
let new_cond = CondExpr {
span: cond.span,
test: test.exprs.pop().unwrap(),
cons: cond.cons.take(),
alt: cond.alt.take(),
};
new_seq.push(Box::new(Expr::Assign(AssignExpr {
span: assign.span,
op: assign.op,
left: assign.left.take(),
right: Box::new(Expr::Cond(new_cond)),
})));
*e = Expr::Seq(SeqExpr {
span: assign.span,
exprs: new_seq,
});
return;
}
}
_ => {}
}
}
/// Lift sequence expressions in an assign expression.
///
/// - `(a = (f, 4)) => (f, a = 4)`
pub(super) fn lift_seqs_of_assign(&mut self, e: &mut SeqExpr) {
if !self.options.sequences() {
return;
}
{
let can_work = e.exprs.iter().any(|e| {
match &**e {
Expr::Assign(assign @ AssignExpr { op: op!("="), .. }) => {
match &*assign.right {
Expr::Seq(right) => {
if right.exprs.len() >= 2 {
return true;
}
}
_ => {}
}
}
_ => {}
}
false
});
if !can_work {
return;
}
log::trace!("sequences: Lifting");
self.changed = true;
}
let mut new_exprs = Vec::with_capacity(e.exprs.len() * 12 / 10);
for expr in e.exprs.take() {
match *expr {
Expr::Assign(assign @ AssignExpr { op: op!("="), .. }) => match *assign.right {
Expr::Seq(mut right) => {
new_exprs.extend(right.exprs.drain(..right.exprs.len() - 1));
new_exprs.push(Box::new(Expr::Assign(AssignExpr {
right: right.exprs.pop().unwrap(),
..assign
})));
continue;
}
_ => {
new_exprs.push(Box::new(Expr::Assign(assign)));
continue;
}
},
_ => {}
}
new_exprs.push(expr);
}
e.exprs = new_exprs;
}
/// Hoist varaibles in subscope.
///
/// I don't know why it depends on `sequences`.
pub(super) fn extract_vars_in_subscopes(&mut self, s: &mut Stmt) {
if !self.options.sequences() {
return;
}
match s {
Stmt::If(stmt) if self.options.conditionals => {
self.extract_vars(&mut stmt.cons);
if let Some(alt) = &mut stmt.alt {
self.extract_vars(alt);
}
}
_ => {}
}
}
/// Move `var` in subscope to current scope.
///
/// This mehod acutally `hoist`s [VarDecl]s declared with `var`.
fn extract_vars(&mut self, s: &mut Stmt) {
let mut found_other = false;
match s {
Stmt::Block(bs) => {
// Extract variables without
for stmt in &mut bs.stmts {
match stmt {
Stmt::Decl(Decl::Var(
v
@
VarDecl {
kind: VarDeclKind::Var,
..
},
)) => {
for decl in &mut v.decls {
if decl.init.is_some() {
continue;
}
self.changed = true;
log::trace!("sequences: Hoisting `var` without init");
let s = Stmt::Decl(Decl::Var(VarDecl {
span: v.span,
kind: VarDeclKind::Var,
declare: false,
decls: vec![decl.take()],
}));
if found_other {
self.append_stmts.push(s);
} else {
self.prepend_stmts.push(s);
}
}
v.decls.retain(|v| !v.name.is_invalid());
}
_ => {
found_other = true;
}
}
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,21 @@
use super::Optimizer;
use swc_ecma_ast::*;
impl Optimizer<'_> {
/// Converts template literals to string if `exprs` of [Tpl] is empty.
pub(super) fn convert_tpl_to_str(&mut self, e: &mut Expr) {
match e {
Expr::Tpl(t) if t.quasis.len() == 1 && t.exprs.is_empty() => {
if let Some(c) = &t.quasis[0].cooked {
if c.value.chars().all(|c| match c {
'\u{0020}'..='\u{007e}' => true,
_ => false,
}) {
*e = Expr::Lit(Lit::Str(c.clone()));
}
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,327 @@
use super::Optimizer;
use crate::util::ExprOptExt;
use swc_common::EqIgnoreSpan;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::Type;
use swc_ecma_utils::Value::Known;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
/// Methods related to option `switches`.
impl Optimizer<'_> {
/// Handle switches in the case where we can know which branch will be
/// taken.
pub(super) fn optimize_const_switches(&mut self, s: &mut Stmt) {
if !self.options.switches || self.ctx.stmt_lablled {
return;
}
let (label, stmt) = match s {
Stmt::Switch(s) => (None, s),
Stmt::Labeled(l) => match &mut *l.body {
Stmt::Switch(s) => (Some(l.label.clone()), s),
_ => return,
},
_ => return,
};
let discriminant = &mut stmt.discriminant;
match &**discriminant {
Expr::Update(..) => return,
_ => {}
}
let matching_case = stmt.cases.iter_mut().position(|case| {
case.test
.as_ref()
.map(|test| discriminant.value_mut().eq_ignore_span(&test))
.unwrap_or(false)
});
if let Some(case_idx) = matching_case {
let mut stmts = vec![];
let should_preserve_switch = stmt.cases.iter().skip(case_idx).any(|case| {
let mut v = BreakFinder {
found_unlabelled_break_for_stmt: false,
};
case.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.found_unlabelled_break_for_stmt
});
if should_preserve_switch {
// Prevent infinite loop.
if stmt.cases.len() == 1 {
return;
}
log::trace!("switches: Removing unreachable cases from a constant switch");
} else {
log::trace!("switches: Removing a constant switch");
}
self.changed = true;
let mut preserved = vec![];
if !should_preserve_switch && !discriminant.is_lit() {
preserved.push(Stmt::Expr(ExprStmt {
span: stmt.span,
expr: discriminant.take(),
}));
if let Some(expr) = stmt.cases[case_idx].test.take() {
preserved.push(Stmt::Expr(ExprStmt {
span: stmt.cases[case_idx].span,
expr,
}));
}
}
for case in stmt.cases.iter_mut().skip(case_idx) {
let mut found_break = false;
case.cons.retain(|stmt| match stmt {
Stmt::Break(BreakStmt { label: None, .. }) => {
found_break = true;
false
}
// TODO: Search recursively.
Stmt::Break(BreakStmt {
label: Some(break_label),
..
}) => {
if Some(break_label.to_id()) == label.as_ref().map(|label| label.to_id()) {
found_break = true;
false
} else {
!found_break
}
}
_ => !found_break,
});
stmts.append(&mut case.cons);
if found_break {
break;
}
}
let inner = if should_preserve_switch {
let mut cases = stmt.cases.take();
let case = SwitchCase {
span: cases[case_idx].span,
test: cases[case_idx].test.take(),
cons: stmts,
};
Stmt::Switch(SwitchStmt {
span: stmt.span,
discriminant: stmt.discriminant.take(),
cases: vec![case],
})
} else {
preserved.extend(stmts);
Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: preserved,
})
};
*s = match label {
Some(label) => Stmt::Labeled(LabeledStmt {
span: DUMMY_SP,
label,
body: Box::new(inner),
}),
None => inner,
};
return;
}
}
/// Drops useless switch cases and statements in it.
///
/// This method will
///
/// - drop the empty cases at the end.
pub(super) fn optimize_switch_cases(&mut self, cases: &mut Vec<SwitchCase>) {
if !self.options.switches {
return;
}
// If default is not last, we can't remove empty cases.
let has_default = cases.iter().any(|case| case.test.is_none());
let all_ends_with_break = cases
.iter()
.all(|case| case.cons.is_empty() || case.cons.last().unwrap().is_break_stmt());
let mut preserve_cases = false;
if !all_ends_with_break && has_default {
if let Some(last) = cases.last() {
if last.test.is_some() {
preserve_cases = true;
}
}
}
self.merge_cases_with_same_cons(cases);
let last_non_empty = cases.iter().rposition(|case| {
// We should preserve test cases if the test is not a literal.
match case.test.as_deref() {
Some(Expr::Lit(..)) | None => {}
_ => return true,
}
if case.cons.is_empty() {
return false;
}
if case.cons.len() == 1 {
match case.cons[0] {
Stmt::Break(BreakStmt { label: None, .. }) => return false,
_ => {}
}
}
true
});
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");
self.changed = true;
cases.drain(last_non_empty + 1..);
}
}
}
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");
self.changed = true;
last.cons.pop();
}
_ => {}
}
}
}
/// 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 mut found = None;
'l: for (li, l) in cases.iter().enumerate().rev() {
if l.cons.is_empty() {
continue;
}
if let Some(l_last) = l.cons.last() {
match l_last {
Stmt::Break(BreakStmt { label: None, .. }) => {}
_ => continue,
}
}
for r in cases.iter().skip(li + 1) {
if r.cons.is_empty() {
continue;
}
let mut r_cons_slice = r.cons.len();
if let Some(last) = r.cons.last() {
match last {
Stmt::Break(BreakStmt { label: None, .. }) => {
r_cons_slice -= 1;
}
_ => {}
}
}
if l.cons[..l.cons.len() - 1].eq_ignore_span(&r.cons[..r_cons_slice]) {
found = Some(li);
break 'l;
}
}
}
if let Some(idx) = found {
self.changed = true;
log::trace!("switches: Merging cases with same cons");
cases[idx].cons.clear();
}
}
/// Remove unreachable cases using discriminant.
pub(super) fn drop_unreachable_cases(&mut self, s: &mut SwitchStmt) {
if !self.options.switches {
return;
}
let dt = s.discriminant.get_type();
if let Known(Type::Bool) = dt {
let db = s.discriminant.as_pure_bool();
if let Known(db) = db {
s.cases.retain(|case| match case.test.as_deref() {
Some(test) => {
let tb = test.as_pure_bool();
match tb {
Known(tb) if db != tb => false,
_ => true,
}
}
None => false,
})
}
}
}
pub(super) fn optimize_switches(&mut self, _s: &mut Stmt) {
if !self.options.switches || self.ctx.stmt_lablled {
return;
}
//
}
}
#[derive(Default)]
struct BreakFinder {
found_unlabelled_break_for_stmt: bool,
}
impl Visit for BreakFinder {
noop_visit_type!();
fn visit_break_stmt(&mut self, s: &BreakStmt, _: &dyn Node) {
if s.label.is_none() {
self.found_unlabelled_break_for_stmt = true;
}
}
/// We don't care about breaks in a lop[
fn visit_for_stmt(&mut self, _: &ForStmt, _: &dyn Node) {}
/// We don't care about breaks in a lop[
fn visit_for_in_stmt(&mut self, _: &ForInStmt, _: &dyn Node) {}
/// We don't care about breaks in a lop[
fn visit_for_of_stmt(&mut self, _: &ForOfStmt, _: &dyn Node) {}
/// We don't care about breaks in a lop[
fn visit_do_while_stmt(&mut self, _: &DoWhileStmt, _: &dyn Node) {}
/// We don't care about breaks in a lop[
fn visit_while_stmt(&mut self, _: &WhileStmt, _: &dyn Node) {}
fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
}

View File

@ -0,0 +1,26 @@
use super::Optimizer;
use swc_atoms::js_word;
use swc_ecma_ast::*;
use swc_ecma_utils::ExprExt;
impl Optimizer<'_> {
/// Drop arguments of `Symbol()` call.
pub(super) fn optimize_symbol_call_unsafely(&mut self, e: &mut CallExpr) {
if !self.options.unsafe_symbols {
return;
}
match &e.callee {
ExprOrSuper::Super(_) => return,
ExprOrSuper::Expr(callee) => match &**callee {
Expr::Ident(Ident {
sym: js_word!("Symbol"),
..
}) => {}
_ => return,
},
}
e.args.retain(|arg| arg.expr.may_have_side_effects());
}
}

View File

@ -0,0 +1,356 @@
use super::Optimizer;
use swc_atoms::js_word;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike;
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_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) {
match s {
Stmt::Return(r) => match r.arg.as_deref_mut() {
Some(Expr::Unary(UnaryExpr {
span,
op: op!("void"),
arg,
})) => {
log::trace!("unused: Removing `return void` in end of a function");
self.changed = true;
*s = Stmt::Expr(ExprStmt {
span: *span,
expr: arg.take(),
});
return;
}
_ => {}
},
_ => {}
}
}
pub(super) fn drop_unused_var_declarator(&mut self, var: &mut VarDeclarator) {
match &mut var.init {
Some(init) => match &**init {
Expr::Invalid(..) => {
var.init = None;
}
// I don't know why, but terser preserves this
Expr::Fn(FnExpr {
function: Function { is_async: true, .. },
..
}) => {}
_ => {
self.drop_unused_vars(&mut var.name);
if var.name.is_invalid() {
let side_effects = self.ignore_return_value(init);
if let Some(e) = side_effects {
self.append_stmts.push(Stmt::Expr(ExprStmt {
span: var.span,
expr: Box::new(e),
}))
}
}
}
},
None => {
self.drop_unused_vars(&mut var.name);
}
}
}
pub(super) fn drop_unused_vars(&mut self, name: &mut Pat) {
if !self.options.unused || self.ctx.in_var_decl_of_for_in_or_of_loop || self.ctx.is_exported
{
return;
}
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
return;
}
if let Some(scope) = self
.data
.as_ref()
.and_then(|data| data.scopes.get(&self.ctx.scope))
{
if scope.has_eval_call || scope.has_with_stmt {
return;
}
}
match name {
Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) {
return;
}
if self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
.map(|v| v.ref_count == 0)
.unwrap_or(false)
{
self.changed = true;
log::trace!(
"unused: Dropping a variable '{}{:?}' because it is not used",
i.id.sym,
i.id.span.ctxt
);
// This will remove variable.
name.take();
return;
}
}
_ => {}
}
}
/// Creates an empty [VarDecl] if `decl` should be removed.
pub(super) fn drop_unused_decl(&mut self, decl: &mut Decl) {
if self.ctx.is_exported {
return;
}
if !self.options.top_level() && (self.ctx.top_level || !self.ctx.in_fn_like) {
return;
}
if !self.options.unused {
return;
}
if let Some(scope) = self
.data
.as_ref()
.and_then(|data| data.scopes.get(&self.ctx.scope))
{
if scope.has_eval_call || scope.has_with_stmt {
return;
}
}
match decl {
Decl::Class(ClassDecl { ident, .. }) | Decl::Fn(FnDecl { ident, .. }) => {
// We should skip if the name of decl is arguments.
if ident.sym == js_word!("arguments") {
return;
}
// If it is not used, drop it.
if self
.data
.as_ref()
.and_then(|data| data.vars.get(&ident.to_id()))
.map(|v| v.ref_count == 0)
.unwrap_or(false)
{
self.changed = true;
log::trace!(
"unused: Dropping a decl '{}{:?}' because it is not used",
ident.sym,
ident.span.ctxt
);
// This will remove the declaration.
decl.take();
return;
}
}
Decl::Var(_) => {
// Variable declarations are handled by other functions.
return;
}
Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
// Nothing to do. We might change this to unreachable!()
return;
}
}
}
pub(super) fn drop_unused_assignments(&mut self, e: &mut Expr) {
if !self.options.unused {
return;
}
if self.ctx.is_delete_arg {
return;
}
if self
.data
.as_ref()
.map(|v| v.top.has_eval_call || v.top.has_with_stmt)
.unwrap_or(false)
{
return;
}
if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level()
{
return;
}
let assign = match e {
Expr::Assign(e) => e,
_ => return,
};
match &mut assign.left {
PatOrExpr::Expr(_) => return,
PatOrExpr::Pat(left) => match &**left {
Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) {
return;
}
if let Some(var) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&i.to_id()))
{
if var.is_fn_local && var.usage_count == 0 {
log::trace!(
"unused: Dropping assignment to var '{}{:?}', which is never used",
i.id.sym,
i.id.span.ctxt
);
self.changed = true;
*e = *assign.right.take();
return;
}
}
}
_ => return,
},
}
}
/// Make `name` [None] if the name is not used.
pub(super) fn remove_name_if_not_used(&mut self, name: &mut Option<Ident>) {
if !self.options.unused {
return;
}
if self.ctx.is_exported {
return;
}
if let Some(i) = &name {
if let Some(data) = &self.data {
let can_remove_ident = data
.vars
.get(&i.to_id())
.map(|v| v.ref_count == 0)
.unwrap_or(true);
if can_remove_ident {
self.changed = true;
log::trace!("Removing ident of an class / function expression");
*name = None;
}
}
}
}
}
#[derive(Debug, Default)]
pub(super) struct UnreachableHandler {
vars: Vec<Ident>,
in_var_name: bool,
in_hoisted_var: bool,
}
impl UnreachableHandler {
/// Asssumes `s` is not reachable, and preserves variable declarations and
/// function declarations in `s`.
///
/// Returns true if statement is chnged.
pub fn preserve_vars(s: &mut Stmt) -> bool {
if s.is_empty() {
return false;
}
match s {
Stmt::Decl(Decl::Var(v)) => {
let mut changed = false;
for decl in &mut v.decls {
if decl.init.is_some() {
decl.init = None;
changed = true;
}
}
return changed;
}
_ => {}
}
let mut v = Self::default();
s.visit_mut_with(&mut v);
if v.vars.is_empty() {
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
} else {
*s = Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: false,
decls: v
.vars
.into_iter()
.map(BindingIdent::from)
.map(Pat::Ident)
.map(|name| VarDeclarator {
span: DUMMY_SP,
name,
init: None,
definite: false,
})
.collect(),
}))
}
true
}
}
impl VisitMut for UnreachableHandler {
noop_visit_mut_type!();
fn visit_mut_pat(&mut self, n: &mut Pat) {
n.visit_mut_children_with(self);
if self.in_var_name && self.in_hoisted_var {
match n {
Pat::Ident(i) => {
self.vars.push(i.id.clone());
}
_ => {}
}
}
}
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
self.in_hoisted_var = n.kind == VarDeclKind::Var;
n.visit_mut_children_with(self);
}
fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
self.in_var_name = true;
n.name.visit_mut_with(self);
self.in_var_name = false;
n.init.visit_mut_with(self);
}
fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
fn visit_mut_function(&mut self, _: &mut Function) {}
}

View File

@ -0,0 +1,125 @@
use super::Ctx;
use super::Optimizer;
use std::ops::Deref;
use std::ops::DerefMut;
use swc_common::comments::Comment;
use swc_common::comments::CommentKind;
use swc_common::Mark;
use swc_common::Span;
impl<'b> Optimizer<'b> {
/// Check for `/** @const */`.
pub(super) fn has_const_ann(&self, span: Span) -> bool {
self.find_comment(span, |c| {
if c.kind == CommentKind::Block {
if !c.text.starts_with('*') {
return false;
}
let t = c.text[1..].trim();
//
if t.starts_with("@const") {
return true;
}
}
false
})
}
/// Check for `/*#__NOINLINE__*/`
pub(super) fn has_noinline(&self, span: Span) -> bool {
self.has_flag(span, "NOINLINE")
}
fn find_comment<F>(&self, span: Span, mut op: F) -> bool
where
F: FnMut(&Comment) -> bool,
{
let mut found = false;
if let Some(comments) = self.comments {
let cs = comments.take_leading(span.lo);
if let Some(cs) = cs {
for c in &cs {
found |= op(&c);
if found {
break;
}
}
comments.add_leading_comments(span.lo, cs);
}
}
found
}
fn has_flag(&self, span: Span, text: &'static str) -> bool {
self.find_comment(span, |c| {
if c.kind == CommentKind::Block {
//
if c.text.len() == (text.len() + 5)
&& c.text.starts_with("#__")
&& c.text.ends_with("__")
&& text == &c.text[3..c.text.len() - 2]
{
return true;
}
}
false
})
}
#[allow(unused)]
pub(super) fn is_done(&mut self, span: Span) -> bool {
let mut ctxt = span.ctxt;
if ctxt == self.done_ctxt {
return true;
}
loop {
let mark = ctxt.remove_mark();
if mark == Mark::root() {
return false;
}
if mark == self.done {
return true;
}
}
}
/// RAII guard to change context temporarically
#[inline]
pub(super) fn with_ctx(&mut self, ctx: Ctx) -> WithCtx<'_, 'b> {
let orig_ctx = self.ctx;
self.ctx = ctx;
WithCtx {
reducer: self,
orig_ctx,
}
}
}
pub(super) struct WithCtx<'a, 'b> {
reducer: &'a mut Optimizer<'b>,
orig_ctx: Ctx,
}
impl<'b> Deref for WithCtx<'_, 'b> {
type Target = Optimizer<'b>;
fn deref(&self) -> &Self::Target {
&self.reducer
}
}
impl DerefMut for WithCtx<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.reducer
}
}
impl Drop for WithCtx<'_, '_> {
fn drop(&mut self) {
self.reducer.ctx = self.orig_ctx;
}
}

View File

@ -0,0 +1,55 @@
use swc_common::sync::Lrc;
use swc_common::SourceMap;
use swc_common::SyntaxContext;
use swc_ecma_ast::Ident;
use swc_ecma_ast::StrKind;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_codegen::Emitter;
use swc_ecma_utils::drop_span;
use swc_ecma_utils::DropSpan;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
pub(crate) struct Debugger;
impl VisitMut for Debugger {
noop_visit_mut_type!();
fn visit_mut_ident(&mut self, n: &mut Ident) {
if n.span.ctxt == SyntaxContext::empty() {
return;
}
n.sym = format!("{}{:?}", n.sym, n.span.ctxt).into();
n.span.ctxt = SyntaxContext::empty();
}
fn visit_mut_str_kind(&mut self, n: &mut StrKind) {
*n = Default::default();
}
}
pub(crate) fn dump<N>(node: &N) -> String
where
N: swc_ecma_codegen::Node + Clone + VisitMutWith<DropSpan> + VisitMutWith<Debugger>,
{
let mut node = node.clone();
node.visit_mut_with(&mut Debugger);
node = drop_span(node);
let mut buf = vec![];
let cm = Lrc::new(SourceMap::default());
{
let mut emitter = Emitter {
cfg: Default::default(),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
node.emit_with(&mut emitter).unwrap();
}
String::from_utf8(buf).unwrap()
}

View File

@ -0,0 +1,29 @@
use swc_common::Mark;
use swc_common::Span;
use swc_common::SyntaxContext;
use swc_ecma_ast::*;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
/// Makes all nodes except identifiers unique in aspect of span hygiene.
pub(crate) fn unique_marker() -> impl VisitMut {
UniqueMarker
}
struct UniqueMarker;
impl VisitMut for UniqueMarker {
noop_visit_mut_type!();
fn visit_mut_ident(&mut self, _: &mut Ident) {}
fn visit_mut_span(&mut self, span: &mut Span) {
debug_assert_eq!(
span.ctxt,
SyntaxContext::empty(),
"unique_marker: Expected empty syntax context"
);
span.ctxt = span.ctxt.apply_mark(Mark::fresh(Mark::root()));
}
}

View File

@ -0,0 +1,108 @@
//! Javascript minifier implemented in rust.
//!
//! Note: Passes should be visited only with [Module] and it's an error to feed
//! them something other. Don't call methods like `visit_mut_script` nor
//! `visit_mut_module_items`.
use crate::compress::compressor;
use crate::hygiene::unique_marker;
use crate::option::MinifyOptions;
use crate::pass::compute_char_freq::compute_char_freq;
use crate::pass::expand_names::name_expander;
use crate::pass::hygiene::hygiene_optimizer;
use crate::pass::mangle_names::name_mangler;
use crate::pass::mangle_props::mangle_properties;
use analyzer::analyze;
use swc_common::comments::Comments;
use swc_ecma_ast::Module;
use swc_ecma_visit::FoldWith;
use swc_ecma_visit::VisitMutWith;
use timing::Timings;
mod analyzer;
mod compress;
mod debug;
mod hygiene;
pub mod option;
mod pass;
pub mod timing;
mod util;
#[inline]
pub fn optimize(
mut m: Module,
comments: Option<&dyn Comments>,
mut timings: Option<&mut Timings>,
options: &MinifyOptions,
) -> Module {
m.visit_mut_with(&mut unique_marker());
if options.wrap {
// TODO: wrap_common_js
// toplevel = toplevel.wrap_commonjs(options.wrap);
}
if options.enclose {
// TODO: enclose
// toplevel = toplevel.wrap_enclose(options.enclose);
}
// We don't need validation.
if let Some(ref mut _t) = timings {
// TODO: store `rename`
}
// Noop.
// https://github.com/mishoo/UglifyJS2/issues/2794
if options.rename && false {
// toplevel.figure_out_scope(options.mangle);
// TODO: Pass `options.mangle` to name expander.
m.visit_mut_with(&mut name_expander());
}
if let Some(ref mut t) = timings {
t.section("compress");
}
if let Some(options) = &options.compress {
m = m.fold_with(&mut compressor(&options, comments));
// Again, we don't need to validate ast
}
if let Some(ref mut _t) = timings {
// TODO: store `scope`
}
if options.mangle.is_some() {
// toplevel.figure_out_scope(options.mangle);
}
if let Some(ref mut t) = timings {
t.section("mangle");
}
if let Some(mangle) = &options.mangle {
// TODO: base54.reset();
let char_freq_info = compute_char_freq(&m);
m.visit_mut_with(&mut name_mangler(mangle.clone(), char_freq_info));
}
if let Some(property_mangle_options) = options.mangle.as_ref().and_then(|o| o.props.as_ref()) {
mangle_properties(&mut m, property_mangle_options.clone());
}
if let Some(ref mut t) = timings {
t.section("hygiene");
}
{
let data = analyze(&m);
m.visit_mut_with(&mut hygiene_optimizer(data));
}
if let Some(ref mut t) = timings {
t.end_section();
}
m
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,317 @@
[
"$&",
"$'",
"$+",
"$1",
"$2",
"$3",
"$4",
"$5",
"$6",
"$7",
"$8",
"$9",
"$_",
"$`",
"-Infinity",
"BYTES_PER_ELEMENT",
"E",
"EPSILON",
"Infinity",
"LN10",
"LN2",
"LOG10E",
"LOG2E",
"MAX_SAFE_INTEGER",
"MAX_VALUE",
"MIN_SAFE_INTEGER",
"MIN_VALUE",
"NEGATIVE_INFINITY",
"NaN",
"PI",
"POSITIVE_INFINITY",
"SQRT1_2",
"SQRT2",
"UTC",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"__proto__",
"abs",
"acos",
"acosh",
"add",
"all",
"allSettled",
"anchor",
"any",
"apply",
"arguments",
"asin",
"asinh",
"assign",
"asyncIterator",
"atan",
"atan2",
"atanh",
"big",
"bind",
"blink",
"bold",
"buffer",
"byteLength",
"byteOffset",
"call",
"caller",
"captureStackTrace",
"catch",
"cbrt",
"ceil",
"charAt",
"charCodeAt",
"clear",
"clz32",
"codePointAt",
"compile",
"concat",
"construct",
"constructor",
"copyWithin",
"cos",
"cosh",
"create",
"defineProperties",
"defineProperty",
"delete",
"deleteProperty",
"description",
"dotAll",
"endsWith",
"entries",
"every",
"exec",
"exp",
"expm1",
"false",
"fill",
"filter",
"finally",
"find",
"findIndex",
"fixed",
"flags",
"flat",
"flatMap",
"floor",
"fontcolor",
"fontsize",
"for",
"forEach",
"freeze",
"from",
"fromCharCode",
"fromCodePoint",
"fromEntries",
"fround",
"get",
"getBigInt64",
"getBigUint64",
"getDate",
"getDay",
"getFloat32",
"getFloat64",
"getFullYear",
"getHours",
"getInt16",
"getInt32",
"getInt8",
"getMilliseconds",
"getMinutes",
"getMonth",
"getOwnPropertyDescriptor",
"getOwnPropertyDescriptors",
"getOwnPropertyNames",
"getOwnPropertySymbols",
"getPrototypeOf",
"getSeconds",
"getTime",
"getTimezoneOffset",
"getUTCDate",
"getUTCDay",
"getUTCFullYear",
"getUTCHours",
"getUTCMilliseconds",
"getUTCMinutes",
"getUTCMonth",
"getUTCSeconds",
"getUint16",
"getUint32",
"getUint8",
"getYear",
"global",
"has",
"hasInstance",
"hasOwnProperty",
"hypot",
"ignoreCase",
"imul",
"includes",
"indexOf",
"input",
"is",
"isArray",
"isConcatSpreadable",
"isExtensible",
"isFinite",
"isFrozen",
"isInteger",
"isNaN",
"isPrototypeOf",
"isSafeInteger",
"isSealed",
"isView",
"italics",
"iterator",
"join",
"keyFor",
"keys",
"lastIndexOf",
"lastMatch",
"lastParen",
"leftContext",
"length",
"link",
"localeCompare",
"log",
"log10",
"log1p",
"log2",
"map",
"match",
"matchAll",
"max",
"message",
"min",
"multiline",
"name",
"normalize",
"now",
"null",
"of",
"ownKeys",
"padEnd",
"padStart",
"parse",
"parseFloat",
"parseInt",
"pop",
"pow",
"preventExtensions",
"propertyIsEnumerable",
"prototype",
"push",
"race",
"random",
"raw",
"reduce",
"reduceRight",
"reject",
"repeat",
"replace",
"replaceAll",
"resolve",
"reverse",
"revocable",
"rightContext",
"round",
"seal",
"search",
"set",
"setBigInt64",
"setBigUint64",
"setDate",
"setFloat32",
"setFloat64",
"setFullYear",
"setHours",
"setInt16",
"setInt32",
"setInt8",
"setMilliseconds",
"setMinutes",
"setMonth",
"setPrototypeOf",
"setSeconds",
"setTime",
"setUTCDate",
"setUTCFullYear",
"setUTCHours",
"setUTCMilliseconds",
"setUTCMinutes",
"setUTCMonth",
"setUTCSeconds",
"setUint16",
"setUint32",
"setUint8",
"setYear",
"shift",
"sign",
"sin",
"sinh",
"size",
"slice",
"small",
"some",
"sort",
"source",
"species",
"splice",
"split",
"sqrt",
"stackTraceLimit",
"startsWith",
"sticky",
"strike",
"stringify",
"sub",
"substr",
"substring",
"sup",
"tan",
"tanh",
"test",
"then",
"toDateString",
"toExponential",
"toFixed",
"toGMTString",
"toISOString",
"toJSON",
"toLocaleDateString",
"toLocaleLowerCase",
"toLocaleString",
"toLocaleTimeString",
"toLocaleUpperCase",
"toLowerCase",
"toPrecision",
"toPrimitive",
"toString",
"toStringTag",
"toTimeString",
"toUTCString",
"toUpperCase",
"trim",
"trimEnd",
"trimLeft",
"trimRight",
"trimStart",
"true",
"trunc",
"undefined",
"unicode",
"unscopables",
"unshift",
"valueOf",
"values"
]

View File

@ -0,0 +1,313 @@
use fxhash::FxHashMap;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use swc_atoms::JsWord;
use swc_ecma_ast::EsVersion;
use swc_ecma_ast::Lit;
pub mod terser;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct MinifyOptions {
#[serde(default)]
pub rename: bool,
#[serde(default)]
pub compress: Option<CompressOptions>,
#[serde(default)]
pub mangle: Option<MangleOptions>,
#[serde(default)]
pub wrap: bool,
#[serde(default)]
pub enclose: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct TopLevelOptions {
pub functions: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct MangleOptions {
#[serde(default, alias = "properties")]
pub props: Option<ManglePropertiesOptions>,
#[serde(default, alias = "toplevel")]
pub top_level: bool,
#[serde(default, alias = "keep_classnames")]
pub keep_class_names: bool,
#[serde(default, alias = "keep_fnames")]
pub keep_fn_names: bool,
#[serde(default, alias = "ie8")]
pub ie8: bool,
#[serde(default, alias = "safari10")]
pub safari10: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManglePropertiesOptions {
#[serde(default, alias = "reserved")]
pub reserved: Vec<String>,
#[serde(default, alias = "undeclared")]
pub undeclared: bool,
#[serde(default, with = "serde_regex")]
pub regex: Option<Regex>,
}
/// https://terser.org/docs/api-reference.html#compress-options
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct CompressOptions {
#[serde(default)]
#[serde(alias = "arguments")]
pub arguments: bool,
#[serde(default = "true_by_default")]
pub arrows: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "booleans")]
pub bools: bool,
#[serde(default)]
#[serde(alias = "booleans_as_integers")]
pub bools_as_ints: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "collapse_vars")]
pub collapse_vars: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "comparisons")]
pub comparisons: bool,
#[serde(default)]
#[serde(alias = "computed_props")]
pub computed_props: bool,
#[serde(default)]
#[serde(alias = "conditionals")]
pub conditionals: bool,
#[serde(default)]
#[serde(alias = "dead_code")]
pub dead_code: bool,
#[serde(default)]
#[serde(alias = "directives")]
pub directives: bool,
#[serde(default)]
#[serde(alias = "drop_console")]
pub drop_console: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "drop_debugger")]
pub drop_debugger: bool,
#[serde(default = "default_ecma")]
pub ecma: EsVersion,
#[serde(default = "true_by_default")]
#[serde(alias = "evaluate")]
pub evaluate: bool,
/// Should we simplify expressions?
#[serde(default)]
#[serde(alias = "expression")]
pub expr: bool,
#[serde(default)]
#[serde(alias = "global_defs")]
pub global_defs: FxHashMap<JsWord, Lit>,
#[serde(default)]
#[serde(alias = "hoist_funs")]
pub hoist_fns: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "hoist_props")]
pub hoist_props: bool,
#[serde(default)]
#[serde(alias = "hoist_vars")]
pub hoist_vars: bool,
#[serde(default)]
#[serde(alias = "ie8")]
pub ie8: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "if_return")]
pub if_return: bool,
///
/// - `0`: disabled inlining
/// - `1`: inline simple functions
/// - `2`: inline functions with arguments
/// - `3`: inline functions with arguments and variables
#[serde(default = "three_by_default")]
#[serde(alias = "inline")]
pub inline: u8,
#[serde(default = "true_by_default")]
#[serde(alias = "join_vars")]
pub join_vars: bool,
#[serde(default)]
#[serde(alias = "keep_classnames")]
pub keep_classnames: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "keep_fargs")]
pub keep_fargs: bool,
#[serde(default)]
#[serde(alias = "keep_fnames")]
pub keep_fnames: bool,
#[serde(default)]
#[serde(alias = "keep_infinity")]
pub keep_infinity: bool,
#[serde(default = "true_by_default")]
#[serde(alias = "loops")]
pub loops: bool,
// module : false,
#[serde(default = "true_by_default")]
#[serde(alias = "negate_iife")]
pub negate_iife: bool,
/// If this value is zero, the minifier will repeat work until the ast node
/// is settled.
#[serde(default = "one_by_default")]
#[serde(alias = "passes")]
pub passes: usize,
#[serde(default = "true_by_default")]
#[serde(alias = "properties")]
pub props: bool,
// pure_getters : !false_by_default && "strict",
// pure_funcs : null,
#[serde(default)]
#[serde(alias = "reduce_funcs")]
pub reduce_fns: bool,
#[serde(default)]
#[serde(alias = "reduce_vars")]
pub reduce_vars: bool,
#[serde(default = "three_by_default")]
#[serde(alias = "sequences")]
pub sequences: u8,
#[serde(default = "true_by_default")]
#[serde(alias = "side_effects")]
pub side_effects: bool,
#[serde(default)]
#[serde(alias = "switches")]
pub switches: bool,
/// Top level symbols to retain.
#[serde(default)]
#[serde(alias = "top_retain")]
pub top_retain: Vec<JsWord>,
#[serde(default)]
#[serde(alias = "toplevel")]
pub top_level: Option<TopLevelOptions>,
#[serde(default = "true_by_default")]
#[serde(alias = "typeofs")]
pub typeofs: bool,
#[serde(default)]
#[serde(rename = "unsafe")]
pub unsafe_passes: bool,
#[serde(default)]
pub unsafe_arrows: bool,
#[serde(default)]
pub unsafe_comps: bool,
#[serde(default)]
#[serde(alias = "unsafe_Function")]
pub unsafe_function: bool,
#[serde(default)]
pub unsafe_math: bool,
#[serde(default)]
pub unsafe_symbols: bool,
#[serde(default)]
pub unsafe_methods: bool,
#[serde(default)]
pub unsafe_proto: bool,
#[serde(default)]
pub unsafe_regexp: bool,
#[serde(default)]
pub unsafe_undefined: bool,
#[serde(default = "true_by_default")]
pub unused: bool,
}
impl CompressOptions {
pub(crate) fn sequences(&self) -> bool {
self.sequences != 0
}
/// Returns `true` if any of toplevel optimizer is enabled.
pub(crate) fn top_level(&self) -> bool {
self.top_level.map(|v| v.functions).unwrap_or(false)
}
}
const fn true_by_default() -> bool {
true
}
const fn one_by_default() -> usize {
1
}
const fn three_by_default() -> u8 {
3
}
const fn default_ecma() -> EsVersion {
EsVersion::Es5
}
/// Implement default using serde.
macro_rules! impl_default {
($T:ty) => {
impl Default for $T {
fn default() -> Self {
serde_json::from_str("{}").unwrap()
}
}
};
}
impl_default!(MinifyOptions);
impl_default!(MangleOptions);
impl_default!(CompressOptions);

View File

@ -0,0 +1,396 @@
//! Compatibility for terser config.
use super::CompressOptions;
use super::TopLevelOptions;
use fxhash::FxHashMap;
use serde::Deserialize;
use serde_json::Value;
use swc_atoms::JsWord;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserEcmaVersion {
Num(usize),
Str(String),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserPureGetterOption {
Bool(bool),
#[serde(rename = "strict")]
Strict,
Str(String),
}
impl Default for TerserPureGetterOption {
fn default() -> Self {
TerserPureGetterOption::Strict
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserInlineOption {
Bool(bool),
Num(u8),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserTopLevelOptions {
Bool(bool),
Str(String),
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserSequenceOptions {
Bool(bool),
Num(u8),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TerserTopRetainOption {
Str(String),
Seq(Vec<JsWord>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TerserCompressorOptions {
#[serde(default)]
pub arguments: bool,
#[serde(default)]
pub arrows: Option<bool>,
#[serde(default)]
pub booleans: Option<bool>,
#[serde(default)]
pub booleans_as_integers: bool,
#[serde(default)]
pub collapse_vars: Option<bool>,
#[serde(default)]
pub comparisons: Option<bool>,
#[serde(default)]
pub computed_props: bool,
#[serde(default)]
pub conditionals: bool,
#[serde(default)]
pub dead_code: bool,
#[serde(default)]
pub defaults: bool,
#[serde(default)]
pub directives: bool,
#[serde(default)]
pub drop_console: bool,
#[serde(default)]
pub drop_debugger: Option<bool>,
#[serde(default = "ecma_default")]
pub ecma: TerserEcmaVersion,
#[serde(default)]
pub evaluate: Option<bool>,
#[serde(default)]
pub expression: bool,
#[serde(default)]
pub global_defs: FxHashMap<JsWord, Value>,
#[serde(default)]
pub hoist_funs: bool,
#[serde(default)]
pub hoist_props: Option<bool>,
#[serde(default)]
pub hoist_vars: bool,
#[serde(default)]
pub ie8: bool,
#[serde(default)]
pub if_return: Option<bool>,
#[serde(default)]
pub inline: Option<TerserInlineOption>,
#[serde(default)]
pub join_vars: Option<bool>,
#[serde(default)]
pub keep_classnames: bool,
#[serde(default)]
pub keep_fargs: Option<bool>,
#[serde(default)]
pub keep_fnames: bool,
#[serde(default)]
pub keep_infinity: bool,
#[serde(default)]
pub loops: Option<bool>,
// module : false,
#[serde(default)]
pub negate_iife: Option<bool>,
#[serde(default)]
pub passes: usize,
#[serde(default)]
pub properties: Option<bool>,
#[serde(default)]
pub pure_getters: TerserPureGetterOption,
#[serde(default)]
pub pure_funcs: Vec<String>,
#[serde(default)]
pub reduce_funcs: bool,
#[serde(default)]
pub reduce_vars: bool,
#[serde(default)]
pub sequences: Option<TerserSequenceOptions>,
#[serde(default)]
pub side_effects: Option<bool>,
#[serde(default)]
pub switches: bool,
#[serde(default)]
pub top_retain: Option<TerserTopRetainOption>,
#[serde(default)]
pub toplevel: Option<TerserTopLevelOptions>,
#[serde(default)]
pub typeofs: Option<bool>,
#[serde(default)]
#[serde(rename = "unsafe")]
pub unsafe_passes: bool,
#[serde(default)]
pub unsafe_arrows: bool,
#[serde(default)]
pub unsafe_comps: bool,
#[serde(default)]
#[serde(rename = "unsafe_Function")]
pub unsafe_function: bool,
#[serde(default)]
pub unsafe_math: bool,
#[serde(default)]
pub unsafe_symbols: bool,
#[serde(default)]
pub unsafe_methods: bool,
#[serde(default)]
pub unsafe_proto: bool,
#[serde(default)]
pub unsafe_regexp: bool,
#[serde(default)]
pub unsafe_undefined: bool,
#[serde(default)]
pub unused: Option<bool>,
#[serde(default)]
pub module: bool,
}
fn ecma_default() -> TerserEcmaVersion {
TerserEcmaVersion::Num(5)
}
impl From<TerserCompressorOptions> for CompressOptions {
fn from(c: TerserCompressorOptions) -> Self {
CompressOptions {
arguments: c.arguments,
arrows: c.arrows.unwrap_or(c.defaults),
bools: c.booleans.unwrap_or(c.defaults),
bools_as_ints: c.booleans_as_integers,
collapse_vars: c.collapse_vars.unwrap_or(c.defaults),
comparisons: c.comparisons.unwrap_or(c.defaults),
computed_props: c.computed_props,
conditionals: c.conditionals,
dead_code: c.dead_code,
directives: c.directives,
drop_console: c.drop_console,
drop_debugger: c.drop_debugger.unwrap_or(c.defaults),
ecma: c.ecma.into(),
evaluate: c.evaluate.unwrap_or(c.defaults),
expr: c.expression,
global_defs: c
.global_defs
.into_iter()
.map(|(k, v)| {
(
k,
match v {
Value::Null => Lit::Null(Null { span: DUMMY_SP }),
Value::Bool(value) => Lit::Bool(Bool {
span: DUMMY_SP,
value,
}),
Value::Number(v) => Lit::Num(Number {
span: DUMMY_SP,
value: v.as_f64().unwrap(),
}),
Value::String(v) => Lit::Str(Str {
span: DUMMY_SP,
value: v.into(),
has_escape: false,
kind: Default::default(),
}),
Value::Object(_) | Value::Array(_) => {
unreachable!()
}
},
)
})
.collect(),
hoist_fns: c.hoist_funs,
hoist_props: c.hoist_props.unwrap_or(c.defaults),
hoist_vars: c.hoist_vars,
ie8: c.ie8,
if_return: c.if_return.unwrap_or(c.defaults),
inline: c
.inline
.map(|v| match v {
TerserInlineOption::Bool(v) => {
if v {
3
} else {
0
}
}
TerserInlineOption::Num(n) => n,
})
.unwrap_or(if c.defaults { 3 } else { 0 }),
join_vars: c.join_vars.unwrap_or(c.defaults),
keep_classnames: c.keep_classnames,
keep_fargs: c.keep_fargs.unwrap_or(c.defaults),
keep_fnames: c.keep_fnames,
keep_infinity: c.keep_infinity,
loops: c.loops.unwrap_or(c.defaults),
negate_iife: c.negate_iife.unwrap_or(c.defaults),
passes: c.passes,
props: c.properties.unwrap_or(c.defaults),
reduce_fns: c.reduce_funcs,
reduce_vars: c.reduce_vars,
sequences: c
.sequences
.map(|v| match v {
TerserSequenceOptions::Bool(v) => {
if v {
3
} else {
0
}
}
TerserSequenceOptions::Num(v) => v,
})
.unwrap_or(if c.defaults { 3 } else { 0 }),
side_effects: c.side_effects.unwrap_or(c.defaults),
switches: c.switches,
top_retain: c.top_retain.map(From::from).unwrap_or_default(),
top_level: c.toplevel.map(From::from),
typeofs: c.typeofs.unwrap_or(c.defaults),
unsafe_passes: c.unsafe_passes,
unsafe_arrows: c.unsafe_arrows,
unsafe_comps: c.unsafe_comps,
unsafe_function: c.unsafe_function,
unsafe_math: c.unsafe_math,
unsafe_symbols: c.unsafe_symbols,
unsafe_methods: c.unsafe_methods,
unsafe_proto: c.unsafe_proto,
unsafe_regexp: c.unsafe_regexp,
unsafe_undefined: c.unsafe_undefined,
unused: c.unused.unwrap_or(c.defaults),
}
}
}
impl From<TerserTopLevelOptions> for TopLevelOptions {
fn from(c: TerserTopLevelOptions) -> Self {
match c {
TerserTopLevelOptions::Bool(v) => TopLevelOptions { functions: v },
TerserTopLevelOptions::Str(..) => {
// TODO
TopLevelOptions { functions: false }
}
}
}
}
impl From<TerserEcmaVersion> for EsVersion {
fn from(v: TerserEcmaVersion) -> Self {
match v {
TerserEcmaVersion::Num(v) => match v {
3 => EsVersion::Es3,
5 => EsVersion::Es5,
6 | 2015 => EsVersion::Es2015,
2016 => EsVersion::Es2016,
2017 => EsVersion::Es2017,
2018 => EsVersion::Es2018,
2019 => EsVersion::Es2019,
2020 => EsVersion::Es2020,
_ => {
panic!("`{}` is not a valid ecmascript version", v)
}
},
TerserEcmaVersion::Str(v) => {
TerserEcmaVersion::Num(v.parse().expect("failed to parse version of ecmascript"))
.into()
}
}
}
}
impl From<TerserTopRetainOption> for Vec<JsWord> {
fn from(v: TerserTopRetainOption) -> Self {
match v {
TerserTopRetainOption::Str(s) => s
.split(",")
.filter(|s| s.trim() != "")
.map(|v| v.into())
.collect(),
TerserTopRetainOption::Seq(v) => v,
}
}
}

View File

@ -0,0 +1,8 @@
use swc_ecma_ast::Module;
#[derive(Debug)]
pub struct CharFreqInfo {}
pub fn compute_char_freq(_: &Module) -> CharFreqInfo {
CharFreqInfo {}
}

View File

@ -0,0 +1,16 @@
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
/// Currently noop.
///
/// See: https://github.com/terser/terser/blob/0c5fde1f6951c70b2dfc91f6960dfedbf0e84fef/lib/minify.js#L177-L182
pub fn name_expander() -> impl VisitMut {
Expander {}
}
#[derive(Debug)]
struct Expander {}
impl VisitMut for Expander {
noop_visit_mut_type!();
}

View File

@ -0,0 +1,60 @@
use crate::analyzer::ProgramData;
use swc_common::Span;
use swc_common::SyntaxContext;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
/// Create a hygiene optimizer.
///
/// Hygiene optimizer removes span hygiene without renaming if it's ok to do so.
pub(crate) fn hygiene_optimizer(data: ProgramData) -> impl 'static + VisitMut {
Optimizer { data }
}
struct Optimizer {
data: ProgramData,
}
impl Optimizer {
/// Registers a binding ident. This is treated as [PatMode::OtherDecl].
///
/// If it conflicts
#[allow(unused)]
fn register_binding_ident(&mut self, i: &mut Ident) {}
}
impl VisitMut for Optimizer {
noop_visit_mut_type!();
fn visit_mut_span(&mut self, span: &mut Span) {
span.ctxt = SyntaxContext::empty();
}
fn visit_mut_ident(&mut self, i: &mut Ident) {
if i.span.ctxt == SyntaxContext::empty() {
return;
}
let info = self.data.vars.get(&i.to_id());
// Ignore labels.
let info = match info {
Some(v) => v,
None => return,
};
if info.is_fn_local {
i.span.ctxt = SyntaxContext::empty();
}
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
n.obj.visit_mut_with(self);
if n.computed {
n.prop.visit_mut_with(self);
}
}
}

View File

@ -0,0 +1,132 @@
use self::preserver::idents_to_preserve;
use super::compute_char_freq::CharFreqInfo;
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::option::MangleOptions;
use crate::util::base54::base54;
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use swc_atoms::JsWord;
use swc_common::SyntaxContext;
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id;
use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
mod preserver;
pub fn name_mangler(options: MangleOptions, _char_freq_info: CharFreqInfo) -> impl VisitMut {
Mangler {
options,
..Default::default()
}
}
#[derive(Debug, Default)]
struct Mangler {
options: MangleOptions,
n: usize,
preserved: FxHashSet<Id>,
preserved_symbols: FxHashSet<JsWord>,
renamed: FxHashMap<Id, JsWord>,
data: Option<ProgramData>,
}
impl Mangler {
fn rename(&mut self, i: &mut Ident) {
if self.preserved.contains(&i.to_id()) {
return;
}
if let Some(var) = self.data.as_ref().unwrap().vars.get(&i.to_id()) {
if !var.declared {
return;
}
}
i.span.ctxt = SyntaxContext::empty();
if let Some(v) = self.renamed.get(&i.to_id()) {
i.sym = v.clone();
return;
}
loop {
let sym: JsWord = base54(self.n).into();
self.n += 1;
if self.preserved_symbols.contains(&sym) {
continue;
}
self.renamed.insert(i.to_id(), sym.clone());
i.sym = sym.clone();
break;
}
}
}
impl VisitMut for Mangler {
noop_visit_mut_type!();
fn visit_mut_module(&mut self, n: &mut Module) {
let data = analyze(&*n);
self.data = Some(data);
self.preserved = idents_to_preserve(self.options.clone(), n);
self.preserved_symbols = self.preserved.iter().map(|v| v.0.clone()).collect();
n.visit_mut_children_with(self);
}
fn visit_mut_script(&mut self, n: &mut Script) {
let data = analyze(&*n);
self.data = Some(data);
self.preserved = idents_to_preserve(self.options.clone(), n);
self.preserved_symbols = self.preserved.iter().map(|v| v.0.clone()).collect();
n.visit_mut_children_with(self);
}
fn visit_mut_class_decl(&mut self, n: &mut ClassDecl) {
self.rename(&mut n.ident);
n.class.visit_mut_with(self);
}
fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) {
self.rename(&mut n.ident);
n.function.visit_mut_with(self);
}
fn visit_mut_labeled_stmt(&mut self, n: &mut LabeledStmt) {
n.body.visit_mut_with(self);
}
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Ident(i) => {
self.rename(i);
}
_ => {}
}
}
fn visit_mut_pat(&mut self, n: &mut Pat) {
n.visit_mut_children_with(self);
match n {
Pat::Ident(i) => {
self.rename(&mut i.id);
}
_ => {}
}
}
fn visit_mut_member_expr(&mut self, n: &mut MemberExpr) {
n.obj.visit_mut_with(self);
if n.computed {
n.prop.visit_mut_with(self);
}
}
}

View File

@ -0,0 +1,151 @@
use crate::option::MangleOptions;
use fxhash::FxHashSet;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::find_ids;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
pub(super) fn idents_to_preserve<N>(options: MangleOptions, n: &N) -> FxHashSet<Id>
where
N: VisitWith<Preserver>,
{
let mut v = Preserver {
options,
preserved: Default::default(),
should_preserve: false,
in_top_level: false,
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.preserved
}
pub(super) struct Preserver {
options: MangleOptions,
preserved: FxHashSet<Id>,
should_preserve: bool,
in_top_level: bool,
}
impl Visit for Preserver {
noop_visit_type!();
fn visit_class_decl(&mut self, n: &ClassDecl, _: &dyn Node) {
n.visit_children_with(self);
if (self.in_top_level && !self.options.top_level) || self.options.keep_class_names {
self.preserved.insert(n.ident.to_id());
}
}
fn visit_catch_clause(&mut self, n: &CatchClause, _: &dyn Node) {
let old = self.should_preserve;
self.should_preserve = true;
n.param.visit_with(&Invalid { span: DUMMY_SP }, self);
self.should_preserve = old;
n.body.visit_with(&Invalid { span: DUMMY_SP }, self);
}
fn visit_export_decl(&mut self, n: &ExportDecl, _: &dyn Node) {
n.visit_children_with(self);
match &n.decl {
Decl::Class(c) => {
self.preserved.insert(c.ident.to_id());
}
Decl::Fn(f) => {
self.preserved.insert(f.ident.to_id());
}
Decl::Var(v) => {
let ids: Vec<Id> = find_ids(&v.decls);
self.preserved.extend(ids);
}
_ => {}
}
}
fn visit_expr(&mut self, n: &Expr, _: &dyn Node) {
n.visit_children_with(self);
match n {
Expr::Ident(i) => {
if self.should_preserve {
self.preserved.insert(i.to_id());
}
}
_ => {}
}
}
fn visit_fn_decl(&mut self, n: &FnDecl, _: &dyn Node) {
n.visit_children_with(self);
if (self.in_top_level && !self.options.top_level) || self.options.keep_fn_names {
self.preserved.insert(n.ident.to_id());
}
}
fn visit_member_expr(&mut self, n: &MemberExpr, _: &dyn Node) {
n.obj.visit_with(n, self);
if n.computed {
n.prop.visit_with(n, self);
}
}
fn visit_module_items(&mut self, n: &[ModuleItem], _: &dyn Node) {
for n in n {
self.in_top_level = true;
n.visit_with(&Invalid { span: DUMMY_SP }, self);
}
}
fn visit_pat(&mut self, n: &Pat, _: &dyn Node) {
n.visit_children_with(self);
match n {
Pat::Ident(i) => {
if self.should_preserve {
self.preserved.insert(i.to_id());
}
}
_ => {}
}
}
fn visit_stmts(&mut self, n: &[Stmt], _: &dyn Node) {
let old_top_level = self.in_top_level;
for n in n {
self.in_top_level = false;
n.visit_with(&Invalid { span: DUMMY_SP }, self);
}
self.in_top_level = old_top_level;
}
fn visit_var_declarator(&mut self, n: &VarDeclarator, _: &dyn Node) {
n.visit_children_with(self);
if self.in_top_level && !self.options.top_level {
let old = self.should_preserve;
self.should_preserve = true;
n.name.visit_with(n, self);
self.should_preserve = old;
return;
}
if self.options.keep_fn_names {
match n.init.as_deref() {
Some(Expr::Fn(..)) | Some(Expr::Arrow(..)) => {
let old = self.should_preserve;
self.should_preserve = true;
n.name.visit_with(n, self);
self.should_preserve = old;
}
_ => {}
}
}
}
}

View File

@ -0,0 +1,316 @@
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::option::ManglePropertiesOptions;
use crate::util::base54::base54;
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,
};
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{VisitMut, VisitMutWith};
pub static JS_ENVIRONMENT_PROPS: Lazy<HashSet<JsWord>> = Lazy::new(|| {
let domprops: Vec<JsWord> = serde_json::from_str(include_str!("../lists/domprops.json"))
.expect("failed to parse domprops.json for property mangler");
let jsprops: Vec<JsWord> = serde_json::from_str(include_str!("../lists/jsprops.json"))
.expect("Failed to parse jsprops.json for property mangler");
let mut word_set: HashSet<JsWord> = HashSet::new();
for name in domprops.iter().chain(jsprops.iter()) {
word_set.insert(name.clone());
}
word_set
});
#[derive(Debug, Default)]
struct ManglePropertiesState {
options: ManglePropertiesOptions,
names_to_mangle: HashSet<JsWord>,
unmangleable: HashSet<JsWord>,
// 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 {
fn add(&mut self, name: &JsWord) {
if self.can_mangle(&name) {
self.names_to_mangle.insert(name.clone());
}
if !self.should_mangle(&name) {
self.unmangleable.insert(name.clone());
}
}
fn can_mangle(&self, name: &JsWord) -> bool {
if self.unmangleable.contains(name) {
false
} else if self.is_reserved(name) {
false
} else {
// TODO only_cache, check if it's a name that doesn't need quotes
true
}
}
fn matches_regex_option(&self, name: &JsWord) -> bool {
if let Some(regex) = &self.options.regex {
regex.is_match(name)
} else {
true
}
}
fn should_mangle(&self, name: &JsWord) -> bool {
if !self.matches_regex_option(name) {
false
} else if self.is_reserved(name) {
false
} else {
self.cache.contains_key(name) || self.names_to_mangle.contains(name)
}
}
fn is_reserved(&self, name: &JsWord) -> bool {
JS_ENVIRONMENT_PROPS.contains(name) || self.options.reserved.contains(&name.to_string())
}
fn gen_name(&mut self, name: &JsWord) -> Option<JsWord> {
if self.should_mangle(name) {
if let Some(cached) = self.cache.get(name) {
Some(cached.clone())
} else {
let n = self.n;
self.n += 1;
let mangled_name: JsWord = base54(n).into();
self.cache.insert(name.clone(), mangled_name.clone());
Some(mangled_name)
}
} else {
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) {
let mut state = ManglePropertiesState {
options,
..Default::default()
};
let data = analyze(&*m);
m.visit_mut_with(&mut PropertyCollector {
state: &mut state,
data,
});
m.visit_mut_with(&mut Mangler { state: &mut state });
}
// Step 1 -- collect candidates to mangle
#[derive(Debug)]
pub struct PropertyCollector<'a> {
data: ProgramData,
state: &'a mut ManglePropertiesState,
}
impl VisitMut for PropertyCollector<'_> {
fn visit_mut_prop_name(&mut self, name: &mut PropName) {
name.visit_mut_children_with(self);
match name {
PropName::Ident(ident) => {
self.state.add(&ident.sym);
}
PropName::Str(s) => {
self.state.add(&s.value);
}
_ => {}
};
}
fn visit_mut_prop(&mut self, prop: &mut Prop) {
prop.visit_mut_children_with(self);
if let Prop::Shorthand(ident) = prop {
self.state.add(&ident.sym);
}
}
fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
call.visit_mut_children_with(self);
if let Some(prop_name) = get_object_define_property_name_arg(call) {
self.state.add(&prop_name.value);
}
}
fn visit_mut_member_expr(&mut self, member_expr: &mut MemberExpr) {
member_expr.visit_mut_children_with(self);
let is_root_declared = is_root_of_member_expr_declared(member_expr, &self.data);
if is_root_declared && !member_expr.computed {
if let Expr::Ident(ident) = &mut *member_expr.prop {
self.state.add(&ident.sym);
}
}
}
}
fn is_root_of_member_expr_declared(member_expr: &MemberExpr, data: &ProgramData) -> bool {
match &member_expr.obj {
ExprOrSuper::Expr(boxed_exp) => match &**boxed_exp {
Expr::Member(member_expr) => is_root_of_member_expr_declared(member_expr, data),
Expr::Ident(expr) => data
.vars
.get(&expr.to_id())
.and_then(|var| Some(var.declared))
.unwrap_or(false),
_ => false,
},
ExprOrSuper::Super(_) => true,
}
}
fn is_object_property_call(call: &CallExpr) -> bool {
// Find Object.defineProperty
if let ExprOrSuper::Expr(callee) = &call.callee {
if let Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj),
prop,
..
}) = &**callee
{
match (&**obj, &**prop) {
(Expr::Ident(Ident { sym: ident, .. }), Expr::Ident(Ident { sym: prop, .. })) => {
if ident.as_ref() == "Object" && prop.as_ref() == "defineProperty" {
return true;
}
}
_ => {}
}
}
}
return false;
}
fn get_object_define_property_name_arg<'a>(call: &'a mut CallExpr) -> Option<&'a mut Str> {
if is_object_property_call(&call) {
let second_arg: &mut Expr = call.args.get_mut(1).map(|arg| &mut arg.expr)?;
if let Expr::Lit(Lit::Str(s)) = second_arg {
Some(s)
} else {
None
}
} else {
None
}
}
// Step 2 -- mangle those properties
#[derive(Debug)]
struct Mangler<'a> {
state: &'a mut ManglePropertiesState,
}
impl Mangler<'_> {
fn mangle_ident(&mut self, ident: &mut Ident) {
if let Some(mangled) = self.state.gen_name(&ident.sym) {
ident.sym = mangled.into();
}
}
fn mangle_str(&mut self, string: &mut Str) {
if let Some(mangled) = self.state.gen_name(&string.value) {
string.value = mangled.into();
string.kind = StrKind::Synthesized;
}
}
}
impl VisitMut for Mangler<'_> {
fn visit_mut_prop_name(&mut self, name: &mut PropName) {
name.visit_mut_children_with(self);
match name {
PropName::Ident(ident) => {
self.mangle_ident(ident);
}
PropName::Str(string) => {
self.mangle_str(string);
}
_ => {}
}
}
fn visit_mut_prop(&mut self, prop: &mut Prop) {
prop.visit_mut_children_with(self);
if let Prop::Shorthand(ident) = prop {
let mut new_ident = ident.clone();
self.mangle_ident(&mut new_ident);
*prop = Prop::KeyValue(KeyValueProp {
key: PropName::Ident(new_ident),
value: Box::new(Expr::Ident(ident.clone())),
});
}
}
fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
call.visit_mut_children_with(self);
if let Some(mut prop_name_str) = get_object_define_property_name_arg(call) {
self.mangle_str(&mut prop_name_str);
}
}
fn visit_mut_member_expr(&mut self, member_expr: &mut MemberExpr) {
member_expr.visit_mut_children_with(self);
if !member_expr.computed {
if let Expr::Ident(ident) = &mut *member_expr.prop {
self.mangle_ident(ident);
}
}
}
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

@ -0,0 +1,5 @@
pub mod compute_char_freq;
pub mod expand_names;
pub mod hygiene;
pub mod mangle_names;
pub mod mangle_props;

View File

@ -0,0 +1,52 @@
use std::io::{self, Write};
use std::time::{Duration, Instant};
/// TOOD: Add timings.
#[derive(Default, Debug)]
pub struct Timings {
current_section: Option<(String, Instant)>,
entries: Vec<(String, Duration)>,
}
impl Timings {
pub fn new() -> Timings {
Timings {
..Default::default()
}
}
pub fn section(&mut self, name: &str) {
self.end_section();
self.current_section = Some((String::from(name), Instant::now()));
}
pub fn end_section(&mut self) {
if let Some((prev_name, prev_start)) = &self.current_section {
self.entries.push((
prev_name.clone(),
Instant::now().duration_since(*prev_start),
));
}
}
pub fn log(&mut self) {
self.end_section();
let entries_printout: Vec<u8> = self
.entries
.iter()
.flat_map(|(name, duration)| {
let sec_duration = duration.as_micros() / 1_000_000;
format!("- {}: {:.6}s\n", name, sec_duration).into_bytes()
})
.collect();
io::stderr()
.write_all(&entries_printout)
.expect("Could not write timings");
*self = Timings::new();
}
}

View File

@ -0,0 +1,19 @@
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/// Note: This returns `a` for 0.
pub(crate) fn base54(mut n: usize) -> String {
let mut ret = String::new();
let mut base = 54;
n += 1;
while n > 0 {
n -= 1;
ret.push(CHARS[n % base] as char);
n = n / base;
base = 64;
}
ret
}

View File

@ -0,0 +1,247 @@
use swc_common::pass::CompilerPass;
use swc_common::pass::Repeated;
use swc_common::Mark;
use swc_common::Span;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::StmtLike;
use swc_ecma_utils::Value;
use swc_ecma_visit::noop_visit_type;
use swc_ecma_visit::Fold;
use swc_ecma_visit::FoldWith;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;
use swc_ecma_visit::VisitWith;
pub(crate) mod base54;
pub(crate) mod sort;
///
/// - `!0` for true
/// - `!1` for false
pub(crate) fn make_bool(span: Span, value: bool) -> Expr {
Expr::Unary(UnaryExpr {
span,
op: op!("!"),
arg: Box::new(Expr::Lit(Lit::Num(Number {
span: DUMMY_SP,
value: if value { 0.0 } else { 1.0 },
}))),
})
}
/// Addditional methods for optimizing expressions.
pub(crate) trait ExprOptExt: Sized {
fn as_expr(&self) -> &Expr;
fn as_mut(&mut self) -> &mut Expr;
/// This returns itself for normal expressions and returns last exprssions
/// for sequence expressions.
fn value_mut(&mut self) -> &mut Expr {
let expr = self.as_mut();
match expr {
Expr::Seq(seq) => seq
.exprs
.last_mut()
.expect("Sequence expressions should have at least one element")
.value_mut(),
expr => expr,
}
}
fn force_seq(&mut self) -> &mut SeqExpr {
let expr = self.as_mut();
match expr {
Expr::Seq(seq) => seq,
_ => {
let inner = expr.take();
*expr = Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![Box::new(inner)],
});
expr.force_seq()
}
}
}
#[inline]
fn prepend_exprs(&mut self, mut exprs: Vec<Box<Expr>>) {
if exprs.is_empty() {
return;
}
let to = self.as_mut();
match to {
Expr::Seq(to) => {
exprs.append(&mut to.exprs);
to.exprs = exprs;
}
_ => {
let v = to.take();
exprs.push(Box::new(v));
*to = Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
});
}
}
}
}
impl ExprOptExt for Box<Expr> {
#[inline]
fn as_expr(&self) -> &Expr {
&self
}
#[inline]
fn as_mut(&mut self) -> &mut Expr {
self
}
}
impl ExprOptExt for Expr {
#[inline]
fn as_expr(&self) -> &Expr {
self
}
#[inline]
fn as_mut(&mut self) -> &mut Expr {
self
}
}
pub(crate) trait SpanExt: Into<Span> {
fn with_mark(self, mark: Mark) -> Span {
let span = self.into();
span.apply_mark(mark)
}
}
impl SpanExt for Span {}
pub(crate) fn contains_leaping_yield<N>(n: &N) -> bool
where
N: VisitWith<LeapFinder>,
{
let mut v = LeapFinder::default();
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.found_yield
}
#[derive(Default)]
pub(crate) struct LeapFinder {
found_yield: bool,
}
impl Visit for LeapFinder {
noop_visit_type!();
fn visit_yield_expr(&mut self, _: &YieldExpr, _: &dyn Node) {
self.found_yield = true;
}
fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
}
/// This method returns true only if `T` is `var`. (Not `const` or `let`)
pub(crate) fn is_hoisted_var_decl_without_init<T>(t: &T) -> bool
where
T: StmtLike,
{
let var = match t.as_stmt() {
Some(Stmt::Decl(Decl::Var(
v @ VarDecl {
kind: VarDeclKind::Var,
..
},
))) => v,
_ => return false,
};
var.decls.iter().all(|decl| decl.init.is_none())
}
pub(crate) trait IsModuleItem {
fn is_module_item() -> bool;
}
impl IsModuleItem for Stmt {
#[inline]
fn is_module_item() -> bool {
false
}
}
impl IsModuleItem for ModuleItem {
#[inline]
fn is_module_item() -> bool {
true
}
}
pub trait ValueExt<T>: Into<Value<T>> {
fn opt(self) -> Option<T> {
match self.into() {
Value::Known(v) => Some(v),
_ => None,
}
}
}
impl<T> ValueExt<T> for Value<T> {}
/// TODO(kdy1): Modify swc_visit.
/// Actually we should implement `swc_visit::Repeated` for
/// `swc_visit::Optional`. But I'm too lazy to bump versions.
pub(crate) struct Optional<V> {
pub enabled: bool,
pub visitor: V,
}
impl<V> Repeated for Optional<V>
where
V: Repeated,
{
#[inline]
fn changed(&self) -> bool {
if self.enabled {
return false;
}
self.visitor.changed()
}
#[inline]
fn reset(&mut self) {
if self.enabled {
return;
}
self.visitor.reset()
}
}
impl<V> CompilerPass for Optional<V>
where
V: CompilerPass,
{
fn name() -> std::borrow::Cow<'static, str> {
V::name()
}
}
impl<V> Fold for Optional<V>
where
V: Fold,
{
#[inline(always)]
fn fold_module(&mut self, module: Module) -> Module {
if !self.enabled {
return module;
}
module.fold_with(&mut self.visitor)
}
}

View File

@ -0,0 +1,30 @@
use std::cmp::Ordering;
pub(crate) fn is_sorted_by<T, F>(mut items: impl Iterator<Item = T>, mut compare: F) -> bool
where
T: Copy,
F: FnMut(&T, &T) -> Option<Ordering>,
{
let mut last = match items.next() {
Some(e) => e,
None => return true,
};
while let Some(curr) = items.next() {
if let Some(Ordering::Greater) | None = compare(&last, &curr) {
return false;
}
last = curr;
}
true
}
pub(crate) fn is_sorted_by_key<T, F, K>(items: impl Iterator<Item = T>, key: F) -> bool
where
T: Copy,
F: FnMut(T) -> K,
K: Copy + PartialOrd,
{
is_sorted_by(items.map(key), PartialOrd::partial_cmp)
}

View File

@ -0,0 +1,266 @@
use ansi_term::Color;
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::env;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fs::read_to_string;
use std::panic::catch_unwind;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use swc_common::comments::SingleThreadedComments;
use swc_common::sync::Lrc;
use swc_common::FileName;
use swc_common::SourceMap;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_codegen::Emitter;
use swc_ecma_minifier::optimize;
use swc_ecma_minifier::option::terser::TerserCompressorOptions;
use swc_ecma_minifier::option::CompressOptions;
use swc_ecma_minifier::option::MangleOptions;
use swc_ecma_minifier::option::MinifyOptions;
use swc_ecma_parser::lexer::input::SourceFileInput;
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::Parser;
use swc_ecma_transforms::fixer;
use swc_ecma_transforms::hygiene;
use swc_ecma_transforms::resolver;
use swc_ecma_visit::FoldWith;
use testing::assert_eq;
use testing::NormalizedOutput;
fn is_ignored(path: &Path) -> bool {
static IGNORED: Lazy<Vec<String>> = Lazy::new(|| {
let lines = read_to_string("tests/ignored.txt").unwrap();
lines
.lines()
.filter(|v| !v.trim().is_empty())
.map(|v| v.to_string())
.collect()
});
static GOLDEN: Lazy<Vec<String>> = Lazy::new(|| {
let lines = read_to_string("tests/golden.txt").unwrap();
lines
.lines()
.filter(|v| !v.trim().is_empty())
.map(|v| v.to_string())
.collect()
});
let s = path.to_string_lossy().replace("-", "_");
if IGNORED.iter().any(|ignored| s.contains(&**ignored)) {
return true;
}
if let Ok(one) = env::var("GOLDEN_ONLY").or_else(|_| env::var("CI")) {
if one == "1" {
if GOLDEN.iter().all(|golden| !s.contains(&**golden)) {
return true;
}
}
}
false
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
enum TestMangleOptions {
Bool(bool),
Normal(MangleOptions),
}
fn parse_compressor_config(s: &str) -> (bool, CompressOptions) {
let c: TerserCompressorOptions =
serde_json::from_str(s).expect("failed to deserialize value into a compressor config");
(c.module, c.into())
}
/// Tests ported from terser.
#[testing::fixture("terser/compress/**/input.js")]
fn fixture(input: PathBuf) {
if is_ignored(&input) {
return;
}
let dir = input.parent().unwrap();
let config = dir.join("config.json");
let config = read_to_string(&config).expect("failed to read config.json");
eprintln!("---- {} -----\n{}", Color::Green.paint("Config"), config);
let (_module, config) = parse_compressor_config(&config);
let mangle = dir.join("mangle.json");
let mangle = read_to_string(&mangle).ok();
if let Some(mangle) = &mangle {
eprintln!(
"---- {} -----\n{}",
Color::Green.paint("Mangle config"),
mangle
);
}
let mangle: Option<TestMangleOptions> =
mangle.map(|s| serde_json::from_str(&s).expect("failed to deserialize mangle.json"));
testing::run_test2(false, |cm, handler| {
let fm = cm.load_file(&input).expect("failed to load input.js");
let comments = SingleThreadedComments::default();
eprintln!("---- {} -----\n{}", Color::Green.paint("Input"), fm.src);
let lexer = Lexer::new(
Default::default(),
Default::default(),
SourceFileInput::from(&*fm),
Some(&comments),
);
let mut parser = Parser::new_from(lexer);
let program = parser
.parse_module()
.map_err(|err| {
err.into_diagnostic(&handler).emit();
})
.map(|module| module.fold_with(&mut resolver()));
// Ignore parser errors.
//
// This is typically related to strict mode caused by module context.
let program = match program {
Ok(v) => v,
_ => return Ok(()),
};
let output = optimize(
program,
Some(&comments),
None,
&MinifyOptions {
compress: Some(config),
mangle: mangle.and_then(|v| match v {
TestMangleOptions::Bool(v) => {
if v {
Some(Default::default())
} else {
None
}
}
TestMangleOptions::Normal(v) => Some(v),
}),
..Default::default()
},
)
.fold_with(&mut hygiene())
.fold_with(&mut fixer(None));
let output = print(cm.clone(), &[output]);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
let expected = {
let expected = read_to_string(&dir.join("output.js")).unwrap();
let fm = cm.new_source_file(FileName::Anon, expected);
let lexer = Lexer::new(
Default::default(),
Default::default(),
SourceFileInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let expected = parser.parse_module().map_err(|err| {
err.into_diagnostic(&handler).emit();
})?;
let expected = expected.fold_with(&mut fixer(None));
print(cm.clone(), &[expected])
};
if output == expected {
return Ok(());
}
eprintln!(
"---- {} -----\n{}",
Color::Green.paint("Expected"),
expected
);
if let Ok(expected_stdout) = read_to_string(dir.join("expected.stdout")) {
eprintln!(
"---- {} -----\n{}",
Color::Green.paint("Expected stdout"),
expected_stdout
);
// We should compare stdout instead.
let output = Command::new("node")
.arg("-e")
.arg(&output)
.output()
.expect("failed to execute output of minifier");
if !output.status.success() {
panic!(
"failed to execute output of minifier:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
let actual = String::from_utf8_lossy(&output.stdout);
assert_eq!(
DebugUsingDisplay(&actual),
DebugUsingDisplay(&expected_stdout)
);
if expected.trim().is_empty() {
return Ok(());
}
}
if env::var("UPDATE").map(|s| s == "1").unwrap_or(false) {
let output = output.clone();
let _ = catch_unwind(|| {
NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js"))
.unwrap();
});
}
assert_eq!(DebugUsingDisplay(&output), DebugUsingDisplay(&expected));
Ok(())
})
.unwrap()
}
fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N]) -> String {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Default::default(),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
for n in nodes {
n.emit_with(&mut emitter).unwrap();
}
}
String::from_utf8(buf).unwrap()
}
#[derive(PartialEq, Eq)]
struct DebugUsingDisplay<'a>(&'a str);
impl<'a> Debug for DebugUsingDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
{
"arguments": true,
"evaluate": true,
"keep_fargs": false,
"properties": true
}

View File

@ -0,0 +1,15 @@
(function (a, b) {
console.log(arguments[0], a, arguments[1], arguments[3], b, arguments[2]);
})("bar", 42, false);
(function (a, b) {
(() => {
console.log(
arguments[0],
a,
arguments[1],
arguments[3],
b,
arguments[2]
);
})(10, 20, 30, 40);
})("bar", 42, false);

View File

@ -0,0 +1,15 @@
(function (a, b, argument_2, argument_3) {
console.log(a, a, b, argument_3, b, argument_2);
})("bar", 42, false);
(function (a, b) {
(() => {
console.log(
arguments[0],
a,
arguments[1],
arguments[3],
b,
arguments[2]
);
})(10, 20, 30, 40);
})("bar", 42, false);

View File

@ -0,0 +1,6 @@
{
"arguments": true,
"evaluate": true,
"keep_fargs": true,
"properties": true
}

View File

@ -0,0 +1,15 @@
(function (a, b) {
console.log(arguments[0], a, arguments[1], arguments[3], b, arguments[2]);
})("bar", 42, false);
(function (a, b) {
(() => {
console.log(
arguments[0],
a,
arguments[1],
arguments[3],
b,
arguments[2]
);
})(10, 20, 30, 40);
})("bar", 42, false);

View File

@ -0,0 +1,15 @@
(function (a, b) {
console.log(a, a, b, arguments[3], b, arguments[2]);
})("bar", 42, false);
(function (a, b) {
(() => {
console.log(
arguments[0],
a,
arguments[1],
arguments[3],
b,
arguments[2]
);
})(10, 20, 30, 40);
})("bar", 42, false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
{
"arguments": true,
"evaluate": true,
"properties": true
}

View File

@ -0,0 +1,15 @@
var arguments = [];
console.log(arguments[0]);
(function () {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (a, b) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (arguments) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function () {
var arguments;
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);

View File

@ -0,0 +1,15 @@
var arguments = [];
console.log(arguments[0]);
(function () {
console.log(arguments[1], arguments[1], arguments.foo);
})("bar", 42);
(function (a, b) {
console.log(b, b, arguments.foo);
})("bar", 42);
(function (arguments) {
console.log(arguments[1], arguments[1], arguments.foo);
})("bar", 42);
(function () {
var arguments;
console.log(arguments[1], arguments[1], arguments.foo);
})("bar", 42);

View File

@ -0,0 +1,6 @@
{
"arguments": true,
"evaluate": true,
"keep_fargs": false,
"properties": true
}

View File

@ -0,0 +1,15 @@
var arguments = [];
console.log(arguments[0]);
(function () {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (a, b) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (arguments) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function () {
var arguments;
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);

View File

@ -0,0 +1,15 @@
var arguments = [];
console.log(arguments[0]);
(function (argument_0, argument_1) {
console.log(argument_1, argument_1, arguments.foo);
})("bar", 42);
(function (a, b) {
console.log(b, b, arguments.foo);
})("bar", 42);
(function (arguments) {
console.log(arguments[1], arguments[1], arguments.foo);
})("bar", 42);
(function () {
var arguments;
console.log(arguments[1], arguments[1], arguments.foo);
})("bar", 42);

View File

@ -0,0 +1,7 @@
{
"arguments": true,
"evaluate": true,
"keep_fargs": false,
"properties": true,
"reduce_vars": true
}

View File

@ -0,0 +1,7 @@
"use strict";
(function () {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (a, b) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);

View File

@ -0,0 +1,7 @@
"use strict";
(function (argument_0, argument_1) {
console.log(argument_1, argument_1, arguments.foo);
})("bar", 42);
(function (a, b) {
console.log(b, b, arguments.foo);
})("bar", 42);

View File

@ -0,0 +1,6 @@
{
"arguments": true,
"evaluate": true,
"properties": true,
"reduce_vars": true
}

View File

@ -0,0 +1,7 @@
"use strict";
(function () {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);
(function (a, b) {
console.log(arguments[1], arguments["1"], arguments["foo"]);
})("bar", 42);

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