mirror of
https://github.com/swc-project/swc.git
synced 2024-11-22 15:25:01 +03:00
feat(es/minifier): Implement minifier partially (#1302)
Co-authored-by: Fábio Santos <fabiosantosart@gmail.com>
This commit is contained in:
parent
b6589af92b
commit
c6b22c57f8
4
.github/.prettierrc
vendored
Normal file
4
.github/.prettierrc
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
1
.github/workflows/cargo.yml
vendored
1
.github/workflows/cargo.yml
vendored
@ -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
4
.gitmodules
vendored
@ -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
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false
|
||||
}
|
@ -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,
|
||||
|
@ -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}
|
||||
|
35
ecmascript/minifier/Cargo.toml
Normal file
35
ecmascript/minifier/Cargo.toml
Normal 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"
|
7
ecmascript/minifier/README.md
Normal file
7
ecmascript/minifier/README.md
Normal 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`
|
15
ecmascript/minifier/scripts/add-golden.sh
Executable file
15
ecmascript/minifier/scripts/add-golden.sh
Executable 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
|
12
ecmascript/minifier/scripts/ignore.sh
Executable file
12
ecmascript/minifier/scripts/ignore.sh
Executable 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
|
47
ecmascript/minifier/scripts/jsprops.js
Normal file
47
ecmascript/minifier/scripts/jsprops.js
Normal 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)
|
22
ecmascript/minifier/scripts/run.sh
Executable file
22
ecmascript/minifier/scripts/run.sh
Executable 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 $@
|
11
ecmascript/minifier/scripts/sort.sh
Executable file
11
ecmascript/minifier/scripts/sort.sh
Executable 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
|
517
ecmascript/minifier/scripts/terser/compress.js
Normal file
517
ecmascript/minifier/scripts/terser/compress.js
Normal 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;
|
||||
}
|
58
ecmascript/minifier/src/analyzer/ctx.rs
Normal file
58
ecmascript/minifier/src/analyzer/ctx.rs
Normal 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;
|
||||
}
|
||||
}
|
600
ecmascript/minifier/src/analyzer/mod.rs
Normal file
600
ecmascript/minifier/src/analyzer/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
89
ecmascript/minifier/src/compress/drop_console.rs
Normal file
89
ecmascript/minifier/src/compress/drop_console.rs
Normal 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;
|
||||
}
|
||||
}
|
271
ecmascript/minifier/src/compress/hoist_decls.rs
Normal file
271
ecmascript/minifier/src/compress/hoist_decls.rs
Normal 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);
|
||||
}
|
||||
}
|
225
ecmascript/minifier/src/compress/mod.rs
Normal file
225
ecmascript/minifier/src/compress/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
212
ecmascript/minifier/src/compress/optimize/arguments.rs
Normal file
212
ecmascript/minifier/src/compress/optimize/arguments.rs
Normal 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 ¶m.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 ¶m.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) {}
|
||||
}
|
104
ecmascript/minifier/src/compress/optimize/arrows.rs
Normal file
104
ecmascript/minifier/src/compress/optimize/arrows.rs
Normal 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;
|
||||
}
|
||||
}
|
384
ecmascript/minifier/src/compress/optimize/bools.rs
Normal file
384
ecmascript/minifier/src/compress/optimize/bools.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
313
ecmascript/minifier/src/compress/optimize/collapse_vars.rs
Normal file
313
ecmascript/minifier/src/compress/optimize/collapse_vars.rs
Normal 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
|
||||
}
|
||||
}
|
41
ecmascript/minifier/src/compress/optimize/computed_props.rs
Normal file
41
ecmascript/minifier/src/compress/optimize/computed_props.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
699
ecmascript/minifier/src/compress/optimize/conditionals.rs
Normal file
699
ecmascript/minifier/src/compress/optimize/conditionals.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
75
ecmascript/minifier/src/compress/optimize/dead_code.rs
Normal file
75
ecmascript/minifier/src/compress/optimize/dead_code.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
238
ecmascript/minifier/src/compress/optimize/evaluate.rs
Normal file
238
ecmascript/minifier/src/compress/optimize/evaluate.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
189
ecmascript/minifier/src/compress/optimize/hoist_props.rs
Normal file
189
ecmascript/minifier/src/compress/optimize/hoist_props.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
325
ecmascript/minifier/src/compress/optimize/if_return.rs
Normal file
325
ecmascript/minifier/src/compress/optimize/if_return.rs
Normal 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) {}
|
||||
}
|
297
ecmascript/minifier/src/compress/optimize/iife.rs
Normal file
297
ecmascript/minifier/src/compress/optimize/iife.rs
Normal 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) = ¶m.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,
|
||||
}
|
||||
}
|
||||
}
|
383
ecmascript/minifier/src/compress/optimize/inline.rs
Normal file
383
ecmascript/minifier/src/compress/optimize/inline.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
114
ecmascript/minifier/src/compress/optimize/join_vars.rs
Normal file
114
ecmascript/minifier/src/compress/optimize/join_vars.rs
Normal 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;
|
||||
}
|
||||
}
|
188
ecmascript/minifier/src/compress/optimize/loops.rs
Normal file
188
ecmascript/minifier/src/compress/optimize/loops.rs
Normal 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 => {}
|
||||
}
|
||||
}
|
||||
}
|
1894
ecmascript/minifier/src/compress/optimize/mod.rs
Normal file
1894
ecmascript/minifier/src/compress/optimize/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
462
ecmascript/minifier/src/compress/optimize/ops.rs
Normal file
462
ecmascript/minifier/src/compress/optimize/ops.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
382
ecmascript/minifier/src/compress/optimize/sequences.rs
Normal file
382
ecmascript/minifier/src/compress/optimize/sequences.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
21
ecmascript/minifier/src/compress/optimize/strings.rs
Normal file
21
ecmascript/minifier/src/compress/optimize/strings.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
327
ecmascript/minifier/src/compress/optimize/switches.rs
Normal file
327
ecmascript/minifier/src/compress/optimize/switches.rs
Normal 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) {}
|
||||
}
|
26
ecmascript/minifier/src/compress/optimize/unsafes.rs
Normal file
26
ecmascript/minifier/src/compress/optimize/unsafes.rs
Normal 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());
|
||||
}
|
||||
}
|
356
ecmascript/minifier/src/compress/optimize/unused.rs
Normal file
356
ecmascript/minifier/src/compress/optimize/unused.rs
Normal 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) {}
|
||||
}
|
125
ecmascript/minifier/src/compress/optimize/util.rs
Normal file
125
ecmascript/minifier/src/compress/optimize/util.rs
Normal 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;
|
||||
}
|
||||
}
|
55
ecmascript/minifier/src/debug.rs
Normal file
55
ecmascript/minifier/src/debug.rs
Normal 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()
|
||||
}
|
29
ecmascript/minifier/src/hygiene.rs
Normal file
29
ecmascript/minifier/src/hygiene.rs
Normal 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()));
|
||||
}
|
||||
}
|
108
ecmascript/minifier/src/lib.rs
Normal file
108
ecmascript/minifier/src/lib.rs
Normal 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
|
||||
}
|
7771
ecmascript/minifier/src/lists/domprops.json
Normal file
7771
ecmascript/minifier/src/lists/domprops.json
Normal file
File diff suppressed because it is too large
Load Diff
317
ecmascript/minifier/src/lists/jsprops.json
Normal file
317
ecmascript/minifier/src/lists/jsprops.json
Normal 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"
|
||||
]
|
313
ecmascript/minifier/src/option/mod.rs
Normal file
313
ecmascript/minifier/src/option/mod.rs
Normal 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);
|
396
ecmascript/minifier/src/option/terser.rs
Normal file
396
ecmascript/minifier/src/option/terser.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
8
ecmascript/minifier/src/pass/compute_char_freq.rs
Normal file
8
ecmascript/minifier/src/pass/compute_char_freq.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use swc_ecma_ast::Module;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CharFreqInfo {}
|
||||
|
||||
pub fn compute_char_freq(_: &Module) -> CharFreqInfo {
|
||||
CharFreqInfo {}
|
||||
}
|
16
ecmascript/minifier/src/pass/expand_names.rs
Normal file
16
ecmascript/minifier/src/pass/expand_names.rs
Normal 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!();
|
||||
}
|
60
ecmascript/minifier/src/pass/hygiene/mod.rs
Normal file
60
ecmascript/minifier/src/pass/hygiene/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
132
ecmascript/minifier/src/pass/mangle_names/mod.rs
Normal file
132
ecmascript/minifier/src/pass/mangle_names/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
151
ecmascript/minifier/src/pass/mangle_names/preserver.rs
Normal file
151
ecmascript/minifier/src/pass/mangle_names/preserver.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
316
ecmascript/minifier/src/pass/mangle_props.rs
Normal file
316
ecmascript/minifier/src/pass/mangle_props.rs
Normal 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);
|
||||
}
|
||||
}
|
5
ecmascript/minifier/src/pass/mod.rs
Normal file
5
ecmascript/minifier/src/pass/mod.rs
Normal 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;
|
52
ecmascript/minifier/src/timing.rs
Normal file
52
ecmascript/minifier/src/timing.rs
Normal 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();
|
||||
}
|
||||
}
|
19
ecmascript/minifier/src/util/base54.rs
Normal file
19
ecmascript/minifier/src/util/base54.rs
Normal 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
|
||||
}
|
247
ecmascript/minifier/src/util/mod.rs
Normal file
247
ecmascript/minifier/src/util/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
30
ecmascript/minifier/src/util/sort.rs
Normal file
30
ecmascript/minifier/src/util/sort.rs
Normal 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)
|
||||
}
|
266
ecmascript/minifier/tests/compress.rs
Normal file
266
ecmascript/minifier/tests/compress.rs
Normal 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)
|
||||
}
|
||||
}
|
1152
ecmascript/minifier/tests/golden.txt
Normal file
1152
ecmascript/minifier/tests/golden.txt
Normal file
File diff suppressed because it is too large
Load Diff
1117
ecmascript/minifier/tests/ignored.txt
Normal file
1117
ecmascript/minifier/tests/ignored.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,4 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"defaults": true
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
(function ({ d: d }) {
|
||||
console.log((a = "foo"), arguments[0].d);
|
||||
})({ d: "Bar" });
|
@ -0,0 +1,3 @@
|
||||
!(function ({ d: d }) {
|
||||
console.log((a = "foo"), arguments[0].d);
|
||||
})({ d: "Bar" });
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"defaults": true
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
(function (a, { d: d }) {
|
||||
console.log((a = "foo"), arguments[0]);
|
||||
})("baz", { d: "Bar" });
|
@ -0,0 +1,3 @@
|
||||
!(function (a, { d: d }) {
|
||||
console.log((a = "foo"), arguments[0]);
|
||||
})("baz", { d: "Bar" });
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"defaults": true
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
(function ({ d: d }, a) {
|
||||
console.log((a = "foo"), arguments[0].d);
|
||||
})({ d: "Bar" }, "baz");
|
@ -0,0 +1,3 @@
|
||||
!(function ({ d: d }, a) {
|
||||
console.log((a = "foo"), arguments[0].d);
|
||||
})({ d: "Bar" }, "baz");
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"keep_fargs": false,
|
||||
"properties": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"keep_fargs": true,
|
||||
"properties": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"defaults": true
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
(function (a, a) {
|
||||
console.log((a = "foo"), arguments[0]);
|
||||
})("baz", "Bar");
|
@ -0,0 +1,3 @@
|
||||
!(function (a, a) {
|
||||
console.log((a = "foo"), arguments[0]);
|
||||
})("baz", "Bar");
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"defaults": true,
|
||||
"toplevel": true
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
function shouldBePure() {
|
||||
return arguments.length;
|
||||
}
|
||||
shouldBePure();
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"arguments": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"reduce_vars": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"properties": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"keep_fargs": false,
|
||||
"properties": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"keep_fargs": false,
|
||||
"properties": true,
|
||||
"reduce_vars": true
|
||||
}
|
@ -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);
|
@ -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);
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"arguments": true,
|
||||
"evaluate": true,
|
||||
"properties": true,
|
||||
"reduce_vars": true
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user