From: Alex Lam S.L Date: Fri, 26 Feb 2021 20:56:34 +0000 (+0000) Subject: support limited `ufuzz` testing for `export` (#4693) X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=ba4a771bbccb0b6026588a56b0c31f5bac466775;p=UglifyJS.git support limited `ufuzz` testing for `export` (#4693) fixes #4692 --- diff --git a/lib/ast.js b/lib/ast.js index 3c6672c2..5fe6efc1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1176,15 +1176,7 @@ var AST_ExportDefault = DEFNODE("ExportDefault", "body", { }); }, _validate: function() { - if (this.body instanceof AST_Class && this.body.name) { - if (!(this.body instanceof AST_DefClass)) { - throw new Error("body must be AST_DefClass when named"); - } - } else if (this.body instanceof AST_Lambda && this.body.name) { - if (!(this.body instanceof AST_LambdaDefinition)) { - throw new Error("body must be AST_LambdaDefinition when named"); - } - } else { + if (!(this.body instanceof AST_DefClass || this.body instanceof AST_LambdaDefinition)) { must_be_expression(this, "body"); } }, diff --git a/lib/compress.js b/lib/compress.js index 0e698804..101fdb4e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -940,6 +940,8 @@ merge(Compressor.prototype, { }); if (!node.name) return; var d = node.name.definition(); + var parent = tw.parent(); + if (parent instanceof AST_ExportDeclaration || parent instanceof AST_ExportDefault) d.single_use = false; if (safe_to_assign(tw, d, true)) { mark(tw, d); tw.loop_ids[d.id] = tw.in_loop; @@ -6709,6 +6711,7 @@ merge(Compressor.prototype, { var var_decl = 0; self.walk(new TreeWalker(function(node) { if (var_decl > 1) return true; + if (node instanceof AST_ExportDeclaration) return true; if (node instanceof AST_Scope && node !== self) return true; if (node instanceof AST_Var) { var_decl++; @@ -6728,12 +6731,15 @@ merge(Compressor.prototype, { dirs.push(node); return make_node(AST_EmptyStatement, node); } - if (hoist_funs && node instanceof AST_Defun - && (tt.parent() === self || !compressor.has_directive("use strict"))) { + if (node instanceof AST_Defun) { + if (!hoist_funs) return node; + if (tt.parent() !== self && compressor.has_directive("use strict")) return node; hoisted.push(node); return make_node(AST_EmptyStatement, node); } - if (hoist_vars && node instanceof AST_Var) { + if (node instanceof AST_Var) { + if (!hoist_vars) return node; + if (tt.parent() instanceof AST_ExportDeclaration) return node; if (!all(node.definitions, function(defn) { var sym = defn.name; return sym instanceof AST_SymbolVar diff --git a/lib/output.js b/lib/output.js index 49164359..bebcd7fd 100644 --- a/lib/output.js +++ b/lib/output.js @@ -664,7 +664,9 @@ function OutputStream(options) { function needs_parens_function(output) { if (!output.has_parens() && first_in_statement(output)) return true; var p = output.parent(); - // export default (function() {})() + // export default (function foo() {}); + if (this.name && p instanceof AST_ExportDefault) return true; + // export default (function() {})(foo); if (p && p.TYPE == "Call" && output.parent(1) instanceof AST_ExportDefault) return true; if (output.option("webkit") && p instanceof AST_PropAccess && p.expression === this) return true; if (output.option("wrap_iife") && p instanceof AST_Call && p.expression === this) return true; @@ -725,6 +727,8 @@ function OutputStream(options) { // { [(1, 2)]: foo } = bar // { 1: (2, foo) } = bar || p instanceof AST_DestructuredKeyVal + // export default (foo, bar) + || p instanceof AST_ExportDefault // for (foo of (bar, baz)); || p instanceof AST_ForOf // { [(1, 2)]: 3 }[2] ---> 3 @@ -1032,9 +1036,16 @@ function OutputStream(options) { output.space(); output.print("default"); output.space(); - this.body.print(output); - if (this.body instanceof AST_Class) return; - if (this.body instanceof AST_Lambda && !is_arrow(this.body)) return; + var body = this.body; + body.print(output); + if (body instanceof AST_ClassExpression) { + if (!body.name) return; + } + if (body instanceof AST_DefClass) return; + if (body instanceof AST_LambdaDefinition) return; + if (body instanceof AST_LambdaExpression) { + if (!body.name && !is_arrow(body)) return; + } output.semicolon(); }); DEFPRINT(AST_ExportForeign, function(output) { diff --git a/test/compress/exports.js b/test/compress/exports.js index 425b0245..c573223d 100644 --- a/test/compress/exports.js +++ b/test/compress/exports.js @@ -56,6 +56,20 @@ defaults_parentheses_2: { expect_exact: 'export default(async function(){console.log("PASS")})();' } +defaults_parentheses_3: { + input: { + export default (42, "PASS"); + } + expect_exact: 'export default(42,"PASS");' +} + +defaults_parentheses_4: { + input: { + export default (function f() {}); + } + expect_exact: "export default(function f(){});" +} + foreign: { input: { export * from "foo"; @@ -203,6 +217,20 @@ hoist_exports: { } } +hoist_vars: { + options = { + hoist_vars: true, + } + input: { + var a; + export var b = 42; + } + expect: { + var a; + export var b = 42; + } +} + keep_return_values: { options = { booleans: true, @@ -301,3 +329,35 @@ single_use_default: { f(); } } + +single_use_class: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + export class A {} + A.prototype.p = "PASS"; + } + expect: { + export class A {} + A.prototype.p = "PASS"; + } +} + +single_use_class_default: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + export default class A {} + A.prototype.p = "PASS"; + } + expect: { + export default class A {} + A.prototype.p = "PASS"; + } +} diff --git a/test/reduce.js b/test/reduce.js index 25dc14ae..dd0cb6f3 100644 --- a/test/reduce.js +++ b/test/reduce.js @@ -132,6 +132,11 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) return; } if (parent instanceof U.AST_VarDef && parent.name === node) return; + // preserve exports + if (parent instanceof U.AST_ExportDeclaration) return; + if (parent instanceof U.AST_ExportDefault) return; + if (parent instanceof U.AST_ExportForeign) return; + if (parent instanceof U.AST_ExportReferences) return; // preserve for (var xxx; ...) if (parent instanceof U.AST_For && parent.init === node && node instanceof U.AST_Definitions) return node; // preserve for (xxx in/of ...) @@ -455,6 +460,13 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) return List.skip; } + // preserve sole definition of an export statement + if (node instanceof U.AST_VarDef + && parent.definitions.length == 1 + && tt.parent(1) instanceof U.AST_ExportDeclaration) { + return node; + } + // remove this node unless its the sole element of a (transient) sequence if (!(parent instanceof U.AST_Sequence) || parent.expressions.length > 1) { node.start._permute++; @@ -720,7 +732,7 @@ function run_code(code, toplevel, result_cache, timeout) { if (!value) { var start = Date.now(); result_cache[key] = value = { - result: sandbox.run_code(code, toplevel, timeout), + result: sandbox.run_code(sandbox.strip_exports(code), toplevel, timeout), elapsed: Date.now() - start, }; } diff --git a/test/sandbox.js b/test/sandbox.js index 1aee7753..1585c988 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -49,6 +49,14 @@ exports.same_stdout = semver.satisfies(process.version, "0.12") ? function(expec } : function(expected, actual) { return typeof expected == typeof actual && strip_func_ids(expected) == strip_func_ids(actual); }; +exports.strip_exports = function(code) { + var count = 0; + return code.replace(/\bexport(?:\s*\{[^}]*};|\s+default\b(?:\s*(\(|\{|class\s*\{|class\s+(?=extends\b)|(?:async\s+)?function\s*(?:\*\s*)?\())?|\b)/g, function(match, header) { + if (!header) return ""; + if (header.length == 1) return "~" + header; + return header.slice(0, -1) + " _" + ++count + header.slice(-1); + }); +}; function is_error(result) { return result && typeof result.name == "string" && typeof result.message == "string"; diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 3d0824b6..e756b747 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -363,6 +363,7 @@ var lambda_vars = []; var unique_vars = []; var classes = []; var async = false; +var export_default = false; var generator = false; var loops = 0; var funcs = 0; @@ -380,6 +381,17 @@ function strictMode() { return use_strict && rng(4) == 0 ? '"use strict";' : ""; } +function appendExport(stmtDepth, allowDefault) { + if (stmtDepth == 1 && rng(20) == 0) { + if (allowDefault && !export_default && rng(5) == 0) { + export_default = true; + return "export default "; + } + return "export "; + } + return ""; +} + function createTopLevelCode() { VAR_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list block_vars.length = 0; @@ -387,21 +399,28 @@ function createTopLevelCode() { unique_vars.length = 0; classes.length = 0; async = false; + export_default = false; generator = false; loops = 0; funcs = 0; clazz = 0; in_class = 0; called = Object.create(null); - return [ + var s = [ strictMode(), - "var _calls_ = 10, a = 100, b = 10, c = 0;", - rng(2) - ? createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0) - : createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, DEFUN_OK, CANNOT_THROW, 0), - // preceding `null` makes for a cleaner output (empty string still shows up etc) - "console.log(null, a, b, c, Infinity, NaN, undefined);" - ].join("\n"); + appendExport(1) + "var _calls_ = 10, a = 100, b = 10, c = 0;", + ]; + createBlockVariables(MAX_GENERATION_RECURSION_DEPTH, 0, CANNOT_THROW, function(defns) { + s.push(defns()); + if (rng(2)) { + s.push(createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0)); + } else { + s.push(createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, DEFUN_OK, CANNOT_THROW, 0)); + } + }); + // preceding `null` makes for a cleaner output (empty string still shows up etc) + s.push("console.log(null, a, b, c, Infinity, NaN, undefined);"); + return s.join("\n"); } function createFunctions(n, recurmax, allowDefun, canThrow, stmtDepth) { @@ -668,6 +687,7 @@ function filterDirective(s) { } function createBlockVariables(recurmax, stmtDepth, canThrow, fn) { + ++stmtDepth; var block_len = block_vars.length; var class_len = classes.length; var nameLenBefore = VAR_NAMES.length; @@ -691,7 +711,7 @@ function createBlockVariables(recurmax, stmtDepth, canThrow, fn) { if (SUPPORT.class) while (rng(100) == 0) { var name = "C" + clazz++; classes.push(name); - s.push(createClassLiteral(recurmax,stmtDepth, canThrow, name)); + s.push(appendExport(stmtDepth, true) + createClassLiteral(recurmax, stmtDepth, canThrow, name)); } if (rng(2)) { s.push(createDefinitions("const", consts), createDefinitions("let", lets)); @@ -707,7 +727,7 @@ function createBlockVariables(recurmax, stmtDepth, canThrow, fn) { function createDefinitions(type, names) { if (!names.length) return ""; - var s = type + " "; + var s = appendExport(stmtDepth) + type + " "; switch (SUPPORT.destructuring ? rng(10) : 2) { case 0: while (!rng(10)) names.splice(rng(names.length + 1), 0, ""); @@ -780,6 +800,7 @@ function invokeGenerator(was_generator) { function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { if (--recurmax < 0) { return ";"; } if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; + ++stmtDepth; var s = []; var name, args; var nameLenBefore = VAR_NAMES.length; @@ -831,13 +852,14 @@ function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { lambda_vars.length = lambda_len; VAR_NAMES.length = nameLenBefore; + if (allowDefun) s = appendExport(stmtDepth, true) + s; if (!allowDefun) { // avoid "function statements" (decl inside statements) - s = "var " + createVarName(MANDATORY) + " = " + s; + s = appendExport(stmtDepth) + "var " + createVarName(MANDATORY) + " = " + s; s += args || createArgs(recurmax, stmtDepth, canThrow); s += call_next; } else if (!(name in called) || args || rng(3)) { - s += "var " + createVarName(MANDATORY) + " = " + name; + s += appendExport(stmtDepth) + "var " + createVarName(MANDATORY) + " = " + name; s += args || createArgs(recurmax, stmtDepth, canThrow); s += call_next; } @@ -987,6 +1009,10 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn case STMT_SEMI: return use_strict && rng(20) === 0 ? '"use strict";' : ";"; case STMT_EXPR: + if (stmtDepth == 1 && !export_default && rng(20) == 0) { + export_default = true; + return "export default " + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ";"; + } return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ";"; case STMT_SWITCH: // note: case args are actual expressions @@ -995,7 +1021,7 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn case STMT_VAR: if (SUPPORT.destructuring && rng(20) == 0) { var pairs = createAssignmentPairs(recurmax, stmtDepth, canThrow); - return "var " + pairs.names.map(function(name, index) { + return appendExport(stmtDepth) + "var " + pairs.names.map(function(name, index) { return index in pairs.values ? name + " = " + pairs.values[index] : name; }).join(", ") + ";"; } else switch (rng(3)) { @@ -1003,20 +1029,20 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn unique_vars.push("c"); var name = createVarName(MANDATORY); unique_vars.pop(); - return "var " + name + ";"; + return appendExport(stmtDepth) + "var " + name + ";"; case 1: // initializer can only have one expression unique_vars.push("c"); var name = createVarName(MANDATORY); unique_vars.pop(); - return "var " + name + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";"; + return appendExport(stmtDepth) + "var " + name + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";"; default: // initializer can only have one expression unique_vars.push("c"); var n1 = createVarName(MANDATORY); var n2 = createVarName(MANDATORY); unique_vars.pop(); - return "var " + n1 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ", " + n2 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";"; + return appendExport(stmtDepth) + "var " + n1 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ", " + n2 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";"; } case STMT_RETURN_ETC: switch (rng(8)) { @@ -1939,6 +1965,10 @@ if (require.main !== module) { return; } +function run_code(code, toplevel) { + return sandbox.run_code(sandbox.strip_exports(code), toplevel); +} + function writeln(stream, msg) { if (typeof msg != "undefined") { stream.write(typeof msg == "string" ? msg : msg.stack || "" + msg); @@ -1960,7 +1990,7 @@ function try_beautify(code, toplevel, result, printfn, options) { printfn("// !!! beautify failed !!!"); printfn(beautified.error); beautified = null; - } else if (!sandbox.same_stdout(sandbox.run_code(beautified.code, toplevel), result)) { + } else if (!sandbox.same_stdout(run_code(beautified.code, toplevel), result)) { beautified = null; } else if (options) { var uglified = UglifyJS.minify(beautified.code, JSON.parse(options)); @@ -1970,7 +2000,7 @@ function try_beautify(code, toplevel, result, printfn, options) { actual = uglified.error; } else { expected = uglify_result; - actual = sandbox.run_code(uglified.code, toplevel); + actual = run_code(uglified.code, toplevel); } if (!sandbox.same_stdout(expected, actual)) { beautified = null; @@ -2008,7 +2038,7 @@ function log_suspects(minify_options, component) { errorln("Error testing options." + component + "." + name); errorln(result.error); } else { - var r = sandbox.run_code(result.code, toplevel); + var r = run_code(result.code, toplevel); return !sandbox.same_stdout(uglify_result, r); } } @@ -2036,7 +2066,7 @@ function log_suspects_global(options, toplevel) { errorln("Error testing options." + component); errorln(result.error); } else { - var r = sandbox.run_code(result.code, toplevel); + var r = run_code(result.code, toplevel); return !sandbox.same_stdout(uglify_result, r); } }); @@ -2108,7 +2138,7 @@ function log(options) { } function sort_globals(code) { - var globals = sandbox.run_code("throw Object.keys(this).sort(" + function(global) { + var globals = run_code("throw Object.keys(this).sort(" + function(global) { return function(m, n) { return (n == "toString") - (m == "toString") || (typeof global[n] == "function") - (typeof global[m] == "function") @@ -2221,7 +2251,7 @@ function patch_try_catch(orig, toplevel) { ].join("\n"); } var new_code = code.slice(0, index) + insert + code.slice(index) + tail_throw; - var result = sandbox.run_code(new_code, toplevel); + var result = run_code(new_code, toplevel); if (!sandbox.is_error(result)) { if (!stack.filled && match[1]) stack.push({ code: code, @@ -2292,7 +2322,7 @@ for (var round = 1; round <= num_iterations; round++) { process.stdout.write(round + " of " + num_iterations + "\r"); original_code = createTopLevelCode(); - var orig_result = [ sandbox.run_code(original_code), sandbox.run_code(original_code, true) ]; + var orig_result = [ run_code(original_code), run_code(original_code, true) ]; if (orig_result.some(function(result, toplevel) { if (typeof result == "string") return; println(); @@ -2317,7 +2347,7 @@ for (var round = 1; round <= num_iterations; round++) { errored = typeof original_result != "string"; if (!uglify_code.error) { uglify_code = uglify_code.code; - uglify_result = sandbox.run_code(uglify_code, toplevel); + uglify_result = run_code(uglify_code, toplevel); ok = sandbox.same_stdout(original_result, uglify_result); // ignore v8 parser bug if (!ok && bug_async_arrow_rest(uglify_result)) ok = true; @@ -2328,13 +2358,13 @@ for (var round = 1; round <= num_iterations; round++) { ok = true; } else { // ignore spurious time-outs - if (!orig_result[toplevel ? 3 : 2]) orig_result[toplevel ? 3 : 2] = sandbox.run_code(original_code, toplevel, 10000); + if (!orig_result[toplevel ? 3 : 2]) orig_result[toplevel ? 3 : 2] = run_code(original_code, toplevel, 10000); ok = sandbox.same_stdout(orig_result[toplevel ? 3 : 2], uglify_result); } } // ignore declaration order of global variables if (!ok && !toplevel) { - ok = sandbox.same_stdout(sandbox.run_code(sort_globals(original_code)), sandbox.run_code(sort_globals(uglify_code))); + ok = sandbox.same_stdout(run_code(sort_globals(original_code)), run_code(sort_globals(uglify_code))); } // ignore numerical imprecision caused by `unsafe_math` if (!ok && o.compress && o.compress.unsafe_math && typeof original_result == typeof uglify_result) { @@ -2344,7 +2374,7 @@ for (var round = 1; round <= num_iterations; round++) { ok = original_result.name == uglify_result.name && fuzzy_match(original_result.message, uglify_result.message); } if (!ok) { - var fuzzy_result = sandbox.run_code(original_code.replace(/( - 0\.1){3}/g, " - 0.3"), toplevel); + var fuzzy_result = run_code(original_code.replace(/( - 0\.1){3}/g, " - 0.3"), toplevel); ok = sandbox.same_stdout(fuzzy_result, uglify_result); } } @@ -2352,8 +2382,8 @@ for (var round = 1; round <= num_iterations; round++) { if (!ok && errored && uglify_result.name == "ReferenceError" && original_result.name == "ReferenceError") ok = true; // ignore difference due to implicit strict-mode in `class` if (!ok && /\bclass\b/.test(original_code)) { - var original_strict = sandbox.run_code('"use strict";' + original_code, toplevel); - var uglify_strict = sandbox.run_code('"use strict";' + uglify_code, toplevel); + var original_strict = run_code('"use strict";' + original_code, toplevel); + var uglify_strict = run_code('"use strict";' + uglify_code, toplevel); if (typeof original_strict != "string" && /strict/.test(original_strict.message)) { ok = typeof uglify_strict != "string" && /strict/.test(uglify_strict.message); } else { @@ -2385,7 +2415,7 @@ for (var round = 1; round <= num_iterations; round++) { var orig_skipped = patch_try_catch(original_code, toplevel); var uglify_skipped = patch_try_catch(uglify_code, toplevel); if (orig_skipped && uglify_skipped) { - ok = sandbox.same_stdout(sandbox.run_code(orig_skipped, toplevel), sandbox.run_code(uglify_skipped, toplevel)); + ok = sandbox.same_stdout(run_code(orig_skipped, toplevel), run_code(uglify_skipped, toplevel)); } } } else {