swc/ecmascript/minifier/scripts/terser/compress.js
강동윤 ef6a745599
feat(es/minifier): Implement more rules (#1735)
swc_ecma_minifier:
 - Implement `evaluate` partially.
 - Store 'expected output' from terser along with our result.

swc_ecma_transforms_base:
 - `fixer`: Don't wrap `+ (a % b)`.

swc_ecma_transforms_optimization:
 - `expr_simplifier`: Preserve this in calls.
2021-05-25 16:39:07 +09:00

521 lines
21 KiB
JavaScript

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 || '');
if (expect) {
fs.writeFileSync(path.join(dir, 'output.terser.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;
}