support limited `ufuzz` testing for `export` (#4693)
authorAlex Lam S.L <alexlamsl@gmail.com>
Fri, 26 Feb 2021 20:56:34 +0000 (20:56 +0000)
committerGitHub <noreply@github.com>
Fri, 26 Feb 2021 20:56:34 +0000 (04:56 +0800)
fixes #4692

lib/ast.js
lib/compress.js
lib/output.js
test/compress/exports.js
test/reduce.js
test/sandbox.js
test/ufuzz/index.js

index 3c6672c..5fe6efc 100644 (file)
@@ -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");
         }
     },
index 0e69880..101fdb4 100644 (file)
@@ -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
index 4916435..bebcd7f 100644 (file)
@@ -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) {
index 425b024..c573223 100644 (file)
@@ -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";
+    }
+}
index 25dc14a..dd0cb6f 100644 (file)
@@ -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,
         };
     }
index 1aee775..1585c98 100644 (file)
@@ -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";
index 3d0824b..e756b74 100644 (file)
@@ -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 {