mirror of
https://github.com/swc-project/swc.git
synced 2024-12-30 09:03:37 +03:00
ef6a745599
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.
521 lines
21 KiB
JavaScript
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;
|
|
}
|