From e13d1e996909f68ee643df17fd7d87773c3e82a5 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 8 Feb 2021 20:28:23 +0000 Subject: [PATCH] support `for [await]...of` statements (#4627) --- README.md | 9 ++++++ lib/ast.js | 21 +++++++++--- lib/compress.js | 30 ++++++++--------- lib/output.js | 39 +++++++++++++--------- lib/parse.js | 27 +++++++++++----- lib/transform.js | 2 +- test/compress/loops.js | 15 +++++++++ test/compress/yields.js | 42 ++++++++++++++++++++++++ test/mocha/cli.js | 4 +-- test/reduce.js | 6 ++-- test/ufuzz/index.js | 72 ++++++++++++++++++++++++++++++++--------- 11 files changed, 203 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 6884d8c8..e3d1d677 100644 --- a/README.md +++ b/README.md @@ -1272,3 +1272,12 @@ To allow for better optimizations, the compiler makes various assumptions: // SyntaxError: Invalid Unicode escape sequence ``` UglifyJS may modify the input which in turn may suppress those errors. +- Some versions of JavaScript will throw `SyntaxError` with the + following: + ```javascript + try {} catch (e) { + for (var e of []); + } + // SyntaxError: Identifier 'e' has already been declared + ``` + UglifyJS may modify the input which in turn may suppress those errors. diff --git a/lib/ast.js b/lib/ast.js index ea823b28..7c6fcaf0 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,11 +422,11 @@ var AST_For = DEFNODE("For", "init condition step", { }, }, AST_IterationStatement); -var AST_ForIn = DEFNODE("ForIn", "init object", { - $documentation: "A `for ... in` statement", +var AST_ForEnumeration = DEFNODE("ForEnumeration", "init object", { + $documentation: "Base class for enumeration loops, i.e. `for ... in`, `for ... of` & `for await ... of`", $propdoc: { - init: "[AST_Node] the `for/in` initialization code", - object: "[AST_Node] the object that we're looping through" + init: "[AST_Node] the assignment target during iteration", + object: "[AST_Node] the object to iterate over" }, walk: function(visitor) { var node = this; @@ -437,6 +437,7 @@ var AST_ForIn = DEFNODE("ForIn", "init object", { }); }, _validate: function() { + if (this.TYPE == "ForEnumeration") throw new Error("should not instantiate AST_ForEnumeration"); if (this.init instanceof AST_Definitions) { if (this.init.definitions.length != 1) throw new Error("init must have single declaration"); } else { @@ -450,6 +451,18 @@ var AST_ForIn = DEFNODE("ForIn", "init object", { }, }, AST_IterationStatement); +var AST_ForIn = DEFNODE("ForIn", null, { + $documentation: "A `for ... in` statement", +}, AST_ForEnumeration); + +var AST_ForOf = DEFNODE("ForOf", null, { + $documentation: "A `for ... of` statement", +}, AST_ForEnumeration); + +var AST_ForAwaitOf = DEFNODE("ForAwaitOf", null, { + $documentation: "A `for await ... of` statement", +}, AST_ForOf); + var AST_With = DEFNODE("With", "expression", { $documentation: "A `with` statement", $propdoc: { diff --git a/lib/compress.js b/lib/compress.js index 591f28a8..1d8c71c7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -347,7 +347,7 @@ merge(Compressor.prototype, { && !parent.is_expr_pure(compressor) && (!(value instanceof AST_LambdaExpression) || !(parent instanceof AST_New) && value.contains_this()); } - if (parent instanceof AST_ForIn) return parent.init === node; + if (parent instanceof AST_ForEnumeration) return parent.init === node; if (parent instanceof AST_ObjectKeyVal) { if (parent.value !== node) return; var obj = tw.parent(level + 1); @@ -930,7 +930,7 @@ merge(Compressor.prototype, { tw.in_loop = saved_loop; return true; }); - def(AST_ForIn, function(tw, descend, compressor) { + def(AST_ForEnumeration, function(tw, descend, compressor) { this.variables.each(function(def) { reset_def(tw, compressor, def); }); @@ -1802,8 +1802,8 @@ merge(Compressor.prototype, { abort = true; return node; } - // Scan object only in a for-in statement - if (node instanceof AST_ForIn) { + // Scan object only in a for-in/of statement + if (node instanceof AST_ForEnumeration) { node.object = node.object.transform(tt); abort = true; return node; @@ -2117,7 +2117,7 @@ merge(Compressor.prototype, { if (!(expr.body instanceof AST_Block)) { extract_candidates(expr.body); } - } else if (expr instanceof AST_ForIn) { + } else if (expr instanceof AST_ForEnumeration) { extract_candidates(expr.object); if (!(expr.body instanceof AST_Block)) { extract_candidates(expr.body); @@ -2232,7 +2232,7 @@ merge(Compressor.prototype, { if (parent.init !== node && parent.condition !== node) return node; return find_stop_value(parent, level + 1); } - if (parent instanceof AST_ForIn) { + if (parent instanceof AST_ForEnumeration) { if (parent.init !== node) return node; return find_stop_value(parent, level + 1); } @@ -3190,7 +3190,7 @@ merge(Compressor.prototype, { } else if (stat.init instanceof AST_Var) { defs = stat.init; } - } else if (stat instanceof AST_ForIn) { + } else if (stat instanceof AST_ForEnumeration) { if (defs && defs.TYPE == stat.init.TYPE) { var defns = defs.definitions.slice(); stat.init = stat.init.definitions[0].name.convert_symbol(AST_SymbolRef, function(ref, name) { @@ -3237,7 +3237,7 @@ merge(Compressor.prototype, { if (defs === node) return node; if (defs.TYPE != node.TYPE) return node; var parent = this.parent(); - if (parent instanceof AST_ForIn && parent.init === node) return node; + if (parent instanceof AST_ForEnumeration && parent.init === node) return node; if (!declarations_only(node)) return node; defs.definitions = defs.definitions.concat(node.definitions); CHANGED = true; @@ -5155,7 +5155,7 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_ForIn) { + if (node instanceof AST_ForEnumeration) { node.object.walk(tw); push(); segment.block = node; @@ -5788,7 +5788,7 @@ merge(Compressor.prototype, { if (node instanceof AST_Catch && node.argname instanceof AST_Destructured) { node.argname.transform(trimmer); } - if (node instanceof AST_Definitions && !(parent instanceof AST_ForIn && parent.init === node)) { + if (node instanceof AST_Definitions && !(parent instanceof AST_ForEnumeration && parent.init === node)) { // place uninitialized names at the start var body = [], head = [], tail = []; // for unused names whose initialization has @@ -6456,7 +6456,7 @@ merge(Compressor.prototype, { }); var seq = node.to_assignments(); var p = tt.parent(); - if (p instanceof AST_ForIn && p.init === node) { + if (p instanceof AST_ForEnumeration && p.init === node) { if (seq) return seq; var def = node.definitions[0].name; return make_node(AST_SymbolRef, def, def); @@ -7330,7 +7330,7 @@ merge(Compressor.prototype, { return if_break_in_loop(self, compressor); }); - OPT(AST_ForIn, function(self, compressor) { + OPT(AST_ForEnumeration, function(self, compressor) { if (compressor.option("varify") && (self.init instanceof AST_Const || self.init instanceof AST_Let)) { var name = self.init.definitions[0].name; if ((name instanceof AST_Destructured || name instanceof AST_SymbolLet) @@ -8661,7 +8661,7 @@ merge(Compressor.prototype, { } else if (scope instanceof AST_For) { if (scope.init === child) continue; in_loop = []; - } else if (scope instanceof AST_ForIn) { + } else if (scope instanceof AST_ForEnumeration) { if (scope.init === child) continue; if (scope.object === child) continue; in_loop = []; @@ -10734,7 +10734,7 @@ merge(Compressor.prototype, { if (assigned) return self; if (compressor.option("sequences") && parent.TYPE != "Call" - && !(parent instanceof AST_ForIn && parent.init === self)) { + && !(parent instanceof AST_ForEnumeration && parent.init === self)) { var seq = lift_sequence_in_expression(self, compressor); if (seq !== self) return seq.optimize(compressor); } @@ -10857,7 +10857,7 @@ merge(Compressor.prototype, { if (is_lhs(compressor.self(), parent)) return self; if (compressor.option("sequences") && parent.TYPE != "Call" - && !(parent instanceof AST_ForIn && parent.init === self)) { + && !(parent instanceof AST_ForEnumeration && parent.init === self)) { var seq = lift_sequence_in_expression(self, compressor); if (seq !== self) return seq.optimize(compressor); } diff --git a/lib/output.js b/lib/output.js index b2213f74..3406daa1 100644 --- a/lib/output.js +++ b/lib/output.js @@ -715,9 +715,13 @@ function OutputStream(options) { || p instanceof AST_Conditional // [ a = (1, 2) ] = [] ---> a == 2 || p instanceof AST_DefaultValue + // { [(1, 2)]: foo } = bar + // { 1: (2, foo) } = bar + || p instanceof AST_DestructuredKeyVal + // for (foo of (bar, baz)); + || p instanceof AST_ForOf // { [(1, 2)]: 3 }[2] ---> 3 // { foo: (1, 2) }.foo ---> 2 - || p instanceof AST_DestructuredKeyVal || p instanceof AST_ObjectProperty // (1, {foo:2}).foo or (1, {foo:2})["foo"] ---> 2 || p instanceof AST_PropAccess && p.expression === this @@ -978,20 +982,25 @@ function OutputStream(options) { output.space(); force_statement(self.body, output); }); - DEFPRINT(AST_ForIn, function(output) { - var self = this; - output.print("for"); - output.space(); - output.with_parens(function() { - self.init.print(output); + function print_for_enum(prefix, infix) { + return function(output) { + var self = this; + output.print(prefix); output.space(); - output.print("in"); + output.with_parens(function() { + self.init.print(output); + output.space(); + output.print(infix); + output.space(); + self.object.print(output); + }); output.space(); - self.object.print(output); - }); - output.space(); - force_statement(self.body, output); - }); + force_statement(self.body, output); + }; + } + DEFPRINT(AST_ForAwaitOf, print_for_enum("for await", "of")); + DEFPRINT(AST_ForIn, print_for_enum("for", "in")); + DEFPRINT(AST_ForOf, print_for_enum("for", "of")); DEFPRINT(AST_With, function(output) { var self = this; output.print("with"); @@ -1226,7 +1235,7 @@ function OutputStream(options) { def.print(output); }); var p = output.parent(); - if (p && p.init !== self || !(p instanceof AST_For || p instanceof AST_ForIn)) output.semicolon(); + if (!(p instanceof AST_IterationStatement && p.init === self)) output.semicolon(); }; } DEFPRINT(AST_Const, print_definitinos("const")); @@ -1253,7 +1262,7 @@ function OutputStream(options) { output.print("="); output.space(); var p = output.parent(1); - var noin = p instanceof AST_For || p instanceof AST_ForIn; + var noin = p instanceof AST_For || p instanceof AST_ForEnumeration; parenthesize_for_noin(self.value, output, noin); } }); diff --git a/lib/parse.js b/lib/parse.js index d5098ff5..c8f63d80 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1039,6 +1039,7 @@ function parse($TEXT, options) { } function for_() { + var await = is("name", "await") && next(); expect("("); var init = null; if (!is("punc", ";")) { @@ -1049,16 +1050,26 @@ function parse($TEXT, options) { : is("keyword", "var") ? (next(), var_(true)) : expression(true); - if (is("operator", "in")) { + var ctor; + if (await) { + expect_token("name", "of"); + ctor = AST_ForAwaitOf; + } else if (is("operator", "in")) { + next(); + ctor = AST_ForIn; + } else if (is("name", "of")) { + next(); + ctor = AST_ForOf; + } + if (ctor) { if (init instanceof AST_Definitions) { if (init.definitions.length > 1) { - token_error(init.start, "Only one variable declaration allowed in for..in loop"); + token_error(init.start, "Only one variable declaration allowed in for..in/of loop"); } } else if (!(is_assignable(init) || (init = to_destructured(init)) instanceof AST_Destructured)) { - token_error(init.start, "Invalid left-hand side in for..in loop"); + token_error(init.start, "Invalid left-hand side in for..in/of loop"); } - next(); - return for_in(init); + return for_enum(ctor, init); } } return regular_for(init); @@ -1078,10 +1089,10 @@ function parse($TEXT, options) { }); } - function for_in(init) { + function for_enum(ctor, init) { var obj = expression(); expect(")"); - return new AST_ForIn({ + return new ctor({ init : init, object : obj, body : in_loop(statement) @@ -1523,7 +1534,7 @@ function parse($TEXT, options) { func.end = prev(); return subscripts(func, allow_calls); } - if (is("name")) { + if (is("name") && is_token(peek(), "punc", "=>")) { start = S.token; sym = _make_symbol(AST_SymbolRef, start); next(); diff --git a/lib/transform.js b/lib/transform.js index ede6e254..716fb148 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -82,7 +82,7 @@ TreeTransformer.prototype = new TreeWalker; if (self.step) self.step = self.step.transform(tw); self.body = self.body.transform(tw); }); - DEF(AST_ForIn, function(self, tw) { + DEF(AST_ForEnumeration, function(self, tw) { self.init = self.init.transform(tw); self.object = self.object.transform(tw); self.body = self.body.transform(tw); diff --git a/test/compress/loops.js b/test/compress/loops.js index a6d0277b..3217afd4 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -828,6 +828,21 @@ empty_for_in_prop_init: { ] } +for_of: { + input: { + var async = [ "PASS", 42 ]; + async.p = "FAIL"; + for (async of (null, async)) + console.log(async); + } + expect_exact: 'var async=["PASS",42];async.p="FAIL";for(async of(null,async))console.log(async);' + expect_stdout: [ + "PASS", + "42", + ] + node_version: ">=0.12" +} + issue_3631_1: { options = { dead_code: true, diff --git a/test/compress/yields.js b/test/compress/yields.js index 2d29c9b6..d799c194 100644 --- a/test/compress/yields.js +++ b/test/compress/yields.js @@ -96,6 +96,48 @@ pause_resume: { node_version: ">=4" } +for_of: { + input: { + function* f() { + if (yield "PASS") yield "FAIL 1"; + yield 42; + return "FAIL 2"; + } + for (var a of f()) + console.log(a); + } + expect_exact: 'function*f(){if(yield"PASS")yield"FAIL 1";yield 42;return"FAIL 2"}for(var a of f())console.log(a);' + expect_stdout: [ + "PASS", + "42", + ] + node_version: ">=4" +} + +for_await_of: { + input: { + async function* f() { + if (yield "PASS") yield "FAIL 1"; + yield { + then: function(r) { + r(42); + }, + }; + return "FAIL 2"; + } + (async function(a) { + for await (a of f()) + console.log(a); + })(); + } + expect_exact: 'async function*f(){if(yield"PASS")yield"FAIL 1";yield{then:function(r){r(42)}};return"FAIL 2"}(async function(a){for await(a of f())console.log(a)})();' + expect_stdout: [ + "PASS", + "42", + ] + node_version: ">=10" +} + collapse_vars_1: { options = { collapse_vars: true, diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 475eae65..af1ed1f8 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -624,7 +624,7 @@ describe("bin/uglifyjs", function() { "Parse error at test/input/invalid/for-in_1.js:2,5", "for (1, 2, a in b) {", " ^", - "ERROR: Invalid left-hand side in for..in loop" + "ERROR: Invalid left-hand side in for..in/of loop" ].join("\n")); done(); }); @@ -638,7 +638,7 @@ describe("bin/uglifyjs", function() { "Parse error at test/input/invalid/for-in_2.js:2,5", "for (var a, b in c) {", " ^", - "ERROR: Only one variable declaration allowed in for..in loop" + "ERROR: Only one variable declaration allowed in for..in/of loop" ].join("\n")); done(); }); diff --git a/test/reduce.js b/test/reduce.js index bfc0c173..d5758ccc 100644 --- a/test/reduce.js +++ b/test/reduce.js @@ -134,8 +134,8 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) if (parent instanceof U.AST_VarDef && parent.name === node) 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 ...) - if (parent instanceof U.AST_ForIn && parent.init === node) return node; + // preserve for (xxx in/of ...) + if (parent instanceof U.AST_ForEnumeration && parent.init === node) return node; // node specific permutations with no parent logic @@ -303,7 +303,7 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) return to_statement(expr); } } - else if (node instanceof U.AST_ForIn) { + else if (node instanceof U.AST_ForEnumeration) { var expr; switch ((node.start._permute * steps | 0) % 3) { case 0: diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index cfe35afd..b4a26272 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -27,7 +27,7 @@ var STMT_IF_ELSE = STMT_("ifelse"); var STMT_DO_WHILE = STMT_("dowhile"); var STMT_WHILE = STMT_("while"); var STMT_FOR_LOOP = STMT_("forloop"); -var STMT_FOR_IN = STMT_("forin"); +var STMT_FOR_ENUM = STMT_("forenum"); var STMT_SEMI = STMT_("semi"); var STMT_EXPR = STMT_("expr"); var STMT_SWITCH = STMT_("switch"); @@ -142,6 +142,8 @@ var SUPPORT = function(matrix) { default_value: "[ a = 0 ] = [];", destructuring: "[] = [];", exponentiation: "0 ** 0", + for_await_of: "async function f(a) { for await (a of []); }", + for_of: "for (var a of []);", generator: "function* f(){}", let: "let a;", rest: "var [...a] = [];", @@ -901,21 +903,58 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); return label.target + "for (var brake" + loop + " = 5; " + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + " && brake" + loop + " > 0; --brake" + loop + ")" + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth); - case STMT_FOR_IN: + case STMT_FOR_ENUM: var label = createLabel(canBreak, canContinue); canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); var key = rng(10) ? "key" + loop : getVarName(NO_CONST); - return [ - "{var expr" + loop + " = " + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "; ", - label.target + " for (", - !/^key/.test(key) ? rng(10) ? "" : "var " : !SUPPORT.let || rng(10) ? "var " : rng(2) ? "let " : "const ", - !SUPPORT.destructuring || rng(10) ? key : rng(5) ? "[ " + key + " ]" : "{ length: " + key + " }", - " in expr" + loop + ") {", - rng(5) > 1 ? "c = 1 + c; var " + createVarName(MANDATORY) + " = expr" + loop + "[" + key + "]; " : "", - createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), - "}}", - ].join(""); + var of = SUPPORT.for_of && rng(20) == 0; + var init = ""; + if (!/^key/.test(key)) { + if (!(of && bug_for_of_var) && rng(10) == 0) init = "var "; + } else if (!SUPPORT.let || !(of && bug_for_of_var) && rng(10)) { + init = "var "; + } else if (rng(2)) { + init = "let "; + } else { + init = "const "; + } + if (!SUPPORT.destructuring || of && !(canThrow && rng(10) == 0) || rng(10)) { + init += key; + } else if (rng(5)) { + init += "[ " + key + " ]"; + } else { + init += "{ length: " + key + " }"; + } + var s = "var expr" + loop + " = "; + if (of) { + var await = SUPPORT.for_await_of && async && rng(20) == 0; + if (SUPPORT.generator && rng(20) == 0) { + var gen = getVarName(); + if (canThrow && rng(10) == 0) { + s += gen + "; "; + } else { + s += gen + " && typeof " + gen + "[Symbol."; + s += await ? "asyncIterator" : "iterator"; + s += '] == "function" ? ' + gen + " : " + createArrayLiteral(recurmax, stmtDepth, canThrow) + "; "; + } + } else if (rng(5)) { + s += createArrayLiteral(recurmax, stmtDepth, canThrow) + "; "; + } else if (canThrow && rng(10) == 0) { + s += createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "; "; + } else { + s += '"" + (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "); "; + } + s += label.target + " for "; + if (await) s += "await "; + s += "(" + init + " of expr" + loop + ") {"; + } else { + s += createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "; "; + s += label.target + " for (" + init + " in expr" + loop + ") {"; + } + if (rng(3)) s += "c = 1 + c; var " + createVarName(MANDATORY) + " = expr" + loop + "[" + key + "]; "; + s += createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + "}"; + return "{" + s + "}"; case STMT_SEMI: return use_strict && rng(20) === 0 ? '"use strict";' : ";"; case STMT_EXPR: @@ -2048,12 +2087,13 @@ if (typeof sandbox.run_code("A:if (0) B:; else B:;") != "string") { if (o.mangle) o.mangle.v8 = true; }); } -var is_bug_async_arrow_rest = function() {}; +var bug_async_arrow_rest = function() {}; if (SUPPORT.arrow && SUPPORT.async && SUPPORT.rest && typeof sandbox.run_code("async (a = f(...[], b)) => 0;") != "string") { - is_bug_async_arrow_rest = function(ex) { + bug_async_arrow_rest = function(ex) { return ex.name == "SyntaxError" && ex.message == "Rest parameter must be last formal parameter"; }; } +var bug_for_of_var = SUPPORT.for_of && SUPPORT.let && typeof sandbox.run_code("try {} catch (e) { for (var e of []); }") != "string"; if (SUPPORT.destructuring && typeof sandbox.run_code("console.log([ 1 ], {} = 2);") != "string") { beautify_options.output.v8 = true; minify_options.forEach(function(o) { @@ -2083,7 +2123,7 @@ for (var round = 1; round <= num_iterations; round++) { println(result); println(); // ignore v8 parser bug - return is_bug_async_arrow_rest(result); + return bug_async_arrow_rest(result); })) continue; minify_options.forEach(function(options) { var o = JSON.parse(options); @@ -2097,7 +2137,7 @@ for (var round = 1; round <= num_iterations; round++) { uglify_result = sandbox.run_code(uglify_code, toplevel); ok = sandbox.same_stdout(original_result, uglify_result); // ignore v8 parser bug - if (!ok && is_bug_async_arrow_rest(uglify_result)) ok = true; + if (!ok && bug_async_arrow_rest(uglify_result)) ok = true; // 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))); -- 2.34.1