From 2cbbf5c375a0fae88345c3ed3bc2829b4b1ac250 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 6 Dec 2020 21:22:40 +0000 Subject: [PATCH] support async function (#4333) --- lib/ast.js | 48 +++++++-- lib/compress.js | 43 +++++--- lib/output.js | 37 +++++-- lib/parse.js | 72 +++++++++++--- lib/scope.js | 2 +- lib/transform.js | 3 + test/compress/async.js | 218 +++++++++++++++++++++++++++++++++++++++++ test/mocha/async.js | 23 +++++ test/reduce.js | 2 +- test/sandbox.js | 5 +- test/ufuzz/index.js | 43 +++++--- 11 files changed, 434 insertions(+), 62 deletions(-) create mode 100644 test/compress/async.js create mode 100644 test/mocha/async.js diff --git a/lib/ast.js b/lib/ast.js index 792264ff..ecaecc5b 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -211,7 +211,7 @@ function must_be_expression(node, prop) { if (!(node[prop] instanceof AST_Node)) throw new Error(prop + " must be AST_Node"); if (node[prop] instanceof AST_Hole) throw new Error(prop + " cannot be AST_Hole"); if (node[prop] instanceof AST_Spread) throw new Error(prop + " cannot be AST_Spread"); - if (node[prop] instanceof AST_Statement && !(node[prop] instanceof AST_Function)) { + if (node[prop] instanceof AST_Statement && !is_function(node[prop])) { throw new Error(prop + " cannot be AST_Statement"); } } @@ -280,7 +280,7 @@ var AST_Block = DEFNODE("Block", "body", { _validate: function() { this.body.forEach(function(node) { if (!(node instanceof AST_Statement)) throw new Error("body must be AST_Statement[]"); - if (node instanceof AST_Function) throw new Error("body cannot contain AST_Function"); + if (is_function(node)) throw new Error("body cannot contain AST_Function"); }); }, }, AST_BlockScope); @@ -296,7 +296,7 @@ var AST_StatementWithBody = DEFNODE("StatementWithBody", "body", { }, _validate: function() { if (!(this.body instanceof AST_Statement)) throw new Error("body must be AST_Statement"); - if (this.body instanceof AST_Function) throw new Error("body cannot be AST_Function"); + if (is_function(this.body)) throw new Error("body cannot be AST_Function"); }, }, AST_BlockScope); @@ -390,7 +390,7 @@ var AST_For = DEFNODE("For", "init condition step", { if (this.init != null) { if (!(this.init instanceof AST_Node)) throw new Error("init must be AST_Node"); if (this.init instanceof AST_Statement - && !(this.init instanceof AST_Definitions || this.init instanceof AST_Function)) { + && !(this.init instanceof AST_Definitions || is_function(this.init))) { throw new Error("init cannot be AST_Statement"); } } @@ -547,6 +547,19 @@ var AST_Accessor = DEFNODE("Accessor", null, { }, }, AST_Lambda); +function is_function(node) { + return node instanceof AST_AsyncFunction || node instanceof AST_Function; +} + +var AST_AsyncFunction = DEFNODE("AsyncFunction", null, { + $documentation: "An asynchronous function expression", + _validate: function() { + if (this.name != null) { + if (!(this.name instanceof AST_SymbolLambda)) throw new Error("name must be AST_SymbolLambda"); + } + }, +}, AST_Lambda); + var AST_Function = DEFNODE("Function", "inlined", { $documentation: "A function expression", _validate: function() { @@ -556,6 +569,13 @@ var AST_Function = DEFNODE("Function", "inlined", { }, }, AST_Lambda); +var AST_AsyncDefun = DEFNODE("AsyncDefun", null, { + $documentation: "An asynchronous function definition", + _validate: function() { + if (!(this.name instanceof AST_SymbolDefun)) throw new Error("name must be AST_SymbolDefun"); + }, +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", "inlined", { $documentation: "A function definition", _validate: function() { @@ -642,7 +662,7 @@ var AST_If = DEFNODE("If", "condition alternative", { must_be_expression(this, "condition"); if (this.alternative != null) { if (!(this.alternative instanceof AST_Statement)) throw new Error("alternative must be AST_Statement"); - if (this.alternative instanceof AST_Function) throw new error("alternative cannot be AST_Function"); + if (is_function(this.alternative)) throw new error("alternative cannot be AST_Function"); } }, }, AST_StatementWithBody); @@ -824,7 +844,7 @@ function must_be_expressions(node, prop, allow_spread, allow_hole) { if (!(node instanceof AST_Node)) throw new Error(prop + " must be AST_Node[]"); if (!allow_hole && node instanceof AST_Hole) throw new Error(prop + " cannot be AST_Hole"); if (!allow_spread && node instanceof AST_Spread) throw new Error(prop + " cannot be AST_Spread"); - if (node instanceof AST_Statement && !(node instanceof AST_Function)) { + if (node instanceof AST_Statement && !is_function(node)) { throw new Error(prop + " cannot contain AST_Statement"); } }); @@ -1024,6 +1044,22 @@ var AST_Assign = DEFNODE("Assign", null, { }, }, AST_Binary); +var AST_Await = DEFNODE("Await", "expression", { + $documentation: "An await expression", + $propdoc: { + expression: "[AST_Node] expression with Promise to resolve on", + }, + walk: function(visitor) { + var node = this; + visitor.visit(node, function() { + node.expression.walk(visitor); + }); + }, + _validate: function() { + must_be_expression(this, "expression"); + }, +}); + /* -----[ LITERALS ]----- */ var AST_Array = DEFNODE("Array", "elements", { diff --git a/lib/compress.js b/lib/compress.js index e84465f6..cf138377 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -322,7 +322,7 @@ merge(Compressor.prototype, { if (value instanceof AST_String) return native_fns.String[name]; if (name == "valueOf") return false; if (value instanceof AST_Array) return native_fns.Array[name]; - if (value instanceof AST_Function) return native_fns.Function[name]; + if (value instanceof AST_Lambda) return native_fns.Function[name]; if (value instanceof AST_Object) return native_fns.Object[name]; if (value instanceof AST_RegExp) return native_fns.RegExp[name] && !value.value.global; } @@ -650,14 +650,6 @@ merge(Compressor.prototype, { lhs.walk(scanner); } - def(AST_Accessor, function(tw, descend, compressor) { - push(tw); - reset_variables(tw, compressor, this); - descend(); - pop(tw); - walk_defuns(tw, this); - return true; - }); def(AST_Assign, function(tw, descend, compressor) { var node = this; var left = node.left; @@ -935,6 +927,14 @@ merge(Compressor.prototype, { pop(tw); return true; }); + def(AST_Lambda, function(tw, descend, compressor) { + push(tw); + reset_variables(tw, compressor, this); + descend(); + pop(tw); + walk_defuns(tw, this); + return true; + }); def(AST_Switch, function(tw, descend, compressor) { this.variables.each(function(def) { reset_def(tw, compressor, def); @@ -1365,7 +1365,7 @@ merge(Compressor.prototype, { function is_iife_call(node) { if (node.TYPE != "Call") return false; - return node.expression instanceof AST_Function || is_iife_call(node.expression); + return is_function(node.expression) || is_iife_call(node.expression); } function is_undeclared_ref(node) { @@ -1744,6 +1744,7 @@ merge(Compressor.prototype, { } function is_last_node(node, parent) { + if (node instanceof AST_Await) return true; if (node.TYPE == "Binary") return node.operator == "in" && !is_object(node.right); if (node instanceof AST_Call) { var def, fn = node.expression; @@ -1887,6 +1888,8 @@ merge(Compressor.prototype, { if (expr.left instanceof AST_SymbolRef) { assignments[expr.left.name] = (assignments[expr.left.name] || 0) + 1; } + } else if (expr instanceof AST_Await) { + extract_candidates(expr.expression); } else if (expr instanceof AST_Binary) { extract_candidates(expr.left); extract_candidates(expr.right); @@ -1975,6 +1978,7 @@ merge(Compressor.prototype, { var parent = scanner.parent(level); if (parent instanceof AST_Array) return node; if (parent instanceof AST_Assign) return node; + if (parent instanceof AST_Await) return node; if (parent instanceof AST_Binary) return node; if (parent instanceof AST_Call) return node; if (parent instanceof AST_Case) return node; @@ -2074,6 +2078,7 @@ merge(Compressor.prototype, { if (parent instanceof AST_Assign) { return may_throw(parent) ? node : find_stop_unused(parent, level + 1); } + if (parent instanceof AST_Await) return node; if (parent instanceof AST_Binary) return find_stop_unused(parent, level + 1); if (parent instanceof AST_Call) return find_stop_unused(parent, level + 1); if (parent instanceof AST_Case) return find_stop_unused(parent, level + 1); @@ -4015,7 +4020,7 @@ merge(Compressor.prototype, { def(AST_Call, function(compressor, ignore_side_effects, cached, depth) { var exp = this.expression; var fn = exp instanceof AST_SymbolRef ? exp.fixed_value() : exp; - if (fn instanceof AST_Lambda) { + if (fn instanceof AST_Defun || fn instanceof AST_Function) { if (fn.evaluating) return this; if (fn.name && fn.name.definition().recursive_refs > 0) return this; if (this.is_expr_pure(compressor)) return this; @@ -4142,6 +4147,9 @@ merge(Compressor.prototype, { def(AST_Statement, function() { throw new Error("Cannot negate a statement"); }); + def(AST_AsyncFunction, function() { + return basic_negation(this); + }); def(AST_Function, function() { return basic_negation(this); }); @@ -5107,7 +5115,7 @@ merge(Compressor.prototype, { } if (node === self) return; if (scope === self) { - if (node instanceof AST_Defun) { + if (node instanceof AST_AsyncDefun || node instanceof AST_Defun) { var def = node.name.definition(); if (!drop_funcs && !(def.id in in_use_ids)) { in_use_ids[def.id] = true; @@ -5258,7 +5266,7 @@ merge(Compressor.prototype, { if (node instanceof AST_Call) calls_to_drop_args.push(node); if (scope !== self) return; if (node instanceof AST_Lambda) { - if (drop_funcs && node !== self && node instanceof AST_Defun) { + if (drop_funcs && node !== self && (node instanceof AST_AsyncDefun || node instanceof AST_Defun)) { var def = node.name.definition(); if (!(def.id in in_use_ids)) { log(node.name, "Dropping unused function {name}"); @@ -5266,7 +5274,7 @@ merge(Compressor.prototype, { return in_list ? List.skip : make_node(AST_EmptyStatement, node); } } - if (node instanceof AST_Function && node.name && drop_fn_name(node.name.definition())) { + if (is_function(node) && node.name && drop_fn_name(node.name.definition())) { unused_fn_names.push(node); } if (!(node instanceof AST_Accessor)) { @@ -7642,7 +7650,7 @@ merge(Compressor.prototype, { } } var fn = exp instanceof AST_SymbolRef ? exp.fixed_value() : exp; - var is_func = fn instanceof AST_Lambda; + var is_func = fn instanceof AST_Defun || fn instanceof AST_Function; var stat = is_func && fn.first_statement(); var can_inline = is_func && compressor.option("inline") @@ -8967,7 +8975,10 @@ merge(Compressor.prototype, { def.single_use = false; fixed._squeezed = true; fixed.single_use = true; - if (fixed instanceof AST_Defun) { + if (fixed instanceof AST_AsyncDefun) { + fixed = make_node(AST_AsyncFunction, fixed, fixed); + fixed.name = make_node(AST_SymbolLambda, fixed.name, fixed.name); + } else if (fixed instanceof AST_Defun) { fixed = make_node(AST_Function, fixed, fixed); fixed.name = make_node(AST_SymbolLambda, fixed.name, fixed.name); } diff --git a/lib/output.js b/lib/output.js index 5830f2e7..bf243862 100644 --- a/lib/output.js +++ b/lib/output.js @@ -661,7 +661,7 @@ function OutputStream(options) { // a function expression needs parens around it when it's provably // the first token to appear in a statement. - PARENS(AST_Function, function(output) { + function needs_parens_function(output) { if (!output.has_parens() && first_in_statement(output)) return true; if (output.option("webkit")) { var p = output.parent(); @@ -671,7 +671,9 @@ function OutputStream(options) { var p = output.parent(); if (p instanceof AST_Call && p.expression === this) return true; } - }); + } + PARENS(AST_AsyncFunction, needs_parens_function); + PARENS(AST_Function, needs_parens_function); // same goes for an object literal, because otherwise it would be // interpreted as a block of code. @@ -689,6 +691,8 @@ function OutputStream(options) { var p = output.parent(); // [ 1, (2, 3), 4 ] ==> [ 1, 3, 4 ] return p instanceof AST_Array + // await (foo, bar) + || p instanceof AST_Await // 1 + (2, 3) + 4 ==> 8 || p instanceof AST_Binary // new (foo, bar) or foo(1, (2, 3), 4) @@ -712,6 +716,8 @@ function OutputStream(options) { PARENS(AST_Binary, function(output) { var p = output.parent(); + // await (foo && bar) + if (p instanceof AST_Await) return true; // this deals with precedence: 3 * (2 + 1) if (p instanceof AST_Binary) { var po = p.operator, pp = PRECEDENCE[po]; @@ -779,6 +785,8 @@ function OutputStream(options) { function needs_parens_assign_cond(self, output) { var p = output.parent(); + // await (a = foo) + if (p instanceof AST_Await) return true; // 1 + (a = 2) + 3 β†’ 6, side effect setting a = 2 if (p instanceof AST_Binary) return !(p instanceof AST_Assign); // (a = func)() β€”orβ€” new (a = Object)() @@ -967,11 +975,7 @@ function OutputStream(options) { }); /* -----[ functions ]----- */ - DEFPRINT(AST_Lambda, function(output, nokeyword) { - var self = this; - if (!nokeyword) { - output.print("function"); - } + function print_lambda(self, output) { if (self.name) { output.space(); self.name.print(output); @@ -984,7 +988,19 @@ function OutputStream(options) { }); output.space(); print_braced(self, output, true); + } + DEFPRINT(AST_Lambda, function(output) { + output.print("function"); + print_lambda(this, output); }); + function print_async(output) { + output.print("async"); + output.space(); + output.print("function"); + print_lambda(this, output); + } + DEFPRINT(AST_AsyncDefun, print_async); + DEFPRINT(AST_AsyncFunction, print_async); /* -----[ jumps ]----- */ function print_jump(kind, prop) { @@ -1272,6 +1288,11 @@ function OutputStream(options) { output.colon(); self.alternative.print(output); }); + DEFPRINT(AST_Await, function(output) { + output.print("await"); + output.space(); + this.expression.print(output); + }); /* -----[ literals ]----- */ DEFPRINT(AST_Array, function(output) { @@ -1375,7 +1396,7 @@ function OutputStream(options) { output.print(type); output.space(); print_property_key(self, output); - self.value._codegen(output, true); + print_lambda(self.value, output); }; } DEFPRINT(AST_ObjectGetter, print_accessor("get")); diff --git a/lib/parse.js b/lib/parse.js index 872561ff..b3c3f977 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -47,7 +47,7 @@ var KEYWORDS = "break case catch const continue debugger default delete do else finally for function if in instanceof let new return switch throw try typeof var void while with"; var KEYWORDS_ATOM = "false null true"; var RESERVED_WORDS = [ - "abstract boolean byte char class double enum export extends final float goto implements import int interface let long native package private protected public short static super synchronized this throws transient volatile yield", + "await abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield", KEYWORDS_ATOM, KEYWORDS, ].join(" "); @@ -656,6 +656,7 @@ function parse($TEXT, options) { token : null, prev : null, peeked : null, + in_async : false, in_function : 0, in_directives : true, in_loop : 0, @@ -786,9 +787,20 @@ function parse($TEXT, options) { return simple_statement(); case "name": - return is_token(peek(), "punc", ":") - ? labeled_statement() - : simple_statement(); + switch (S.token.value) { + case "async": + if (is_token(peek(), "keyword", "function")) { + next(); + next(); + return function_(AST_AsyncDefun); + } + case "await": + if (S.in_async) return simple_statement(); + default: + return is_token(peek(), "punc", ":") + ? labeled_statement() + : simple_statement(); + } case "punc": switch (S.token.value) { @@ -1026,10 +1038,18 @@ function parse($TEXT, options) { } var function_ = function(ctor) { - var in_statement = ctor === AST_Defun; - var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; - if (in_statement && !name) - expect_token("name"); + var was_async = S.in_async; + var name; + if (ctor === AST_AsyncDefun) { + name = as_symbol(AST_SymbolDefun); + S.in_async = true; + } else if (ctor === AST_Defun) { + name = as_symbol(AST_SymbolDefun); + S.in_async = false; + } else { + S.in_async = ctor === AST_AsyncFunction; + name = as_symbol(AST_SymbolLambda, true); + } if (name && ctor !== AST_Accessor && !(name instanceof AST_SymbolDeclaration)) unexpected(prev()); expect("("); @@ -1055,6 +1075,7 @@ function parse($TEXT, options) { --S.in_function; S.in_loop = loop; S.labels = labels; + S.in_async = was_async; return new ctor({ name: name, argnames: argnames, @@ -1304,9 +1325,16 @@ function parse($TEXT, options) { } unexpected(); } - if (is("keyword", "function")) { + var ctor; + if (is("name", "async") && is_token(peek(), "keyword", "function")) { next(); - var func = function_(AST_Function); + ctor = AST_AsyncFunction; + } else if (is("keyword", "function")) { + ctor = AST_Function; + } + if (ctor) { + next(); + var func = function_(ctor); func.start = start; func.end = prev(); return subscripts(func, allow_calls); @@ -1451,6 +1479,7 @@ function parse($TEXT, options) { function _make_symbol(type, token) { var name = token.value; + if (name === "await" && S.in_async) unexpected(token); return new (name === "this" ? AST_This : type)({ name: "" + name, start: token, @@ -1573,17 +1602,17 @@ function parse($TEXT, options) { return expr; }; - var maybe_unary = function(allow_calls) { + function maybe_unary() { var start = S.token; if (is("operator") && UNARY_PREFIX[start.value]) { next(); handle_regexp(); - var ex = make_unary(AST_UnaryPrefix, start, maybe_unary(allow_calls)); + var ex = make_unary(AST_UnaryPrefix, start, maybe_await()); ex.start = start; ex.end = prev(); return ex; } - var val = expr_atom(allow_calls); + var val = expr_atom(true); while (is("operator") && UNARY_POSTFIX[S.token.value] && !has_newline_before(S.token)) { val = make_unary(AST_UnaryPostfix, S.token, val); val.start = start; @@ -1591,7 +1620,7 @@ function parse($TEXT, options) { next(); } return val; - }; + } function make_unary(ctor, token, expr) { var op = token.value; @@ -1609,13 +1638,24 @@ function parse($TEXT, options) { return new ctor({ operator: op, expression: expr }); } + function maybe_await() { + var start = S.token; + if (!(S.in_async && is("name", "await"))) return maybe_unary(); + next(); + return new AST_Await({ + start: start, + expression: maybe_await(), + end: prev(), + }); + } + var expr_op = function(left, min_prec, no_in) { var op = is("operator") ? S.token.value : null; if (op == "in" && no_in) op = null; var prec = op != null ? PRECEDENCE[op] : null; if (prec != null && prec > min_prec) { next(); - var right = expr_op(maybe_unary(true), prec, no_in); + var right = expr_op(maybe_await(), prec, no_in); return expr_op(new AST_Binary({ start : left.start, left : left, @@ -1628,7 +1668,7 @@ function parse($TEXT, options) { }; function expr_ops(no_in) { - return expr_op(maybe_unary(true), 0, no_in); + return expr_op(maybe_await(), 0, no_in); } var maybe_conditional = function(no_in) { diff --git a/lib/scope.js b/lib/scope.js index 4d080fee..c1bbc086 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -112,7 +112,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) { var next_def_id = 0; var scope = self.parent_scope = null; var tw = new TreeWalker(function(node, descend) { - if (node instanceof AST_Defun) { + if (node instanceof AST_AsyncDefun || node instanceof AST_Defun) { node.name.walk(tw); walk_scope(function() { node.argnames.forEach(function(argname) { diff --git a/lib/transform.js b/lib/transform.js index 65e6cd60..398cf73d 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -138,6 +138,9 @@ TreeTransformer.prototype = new TreeWalker; DEF(AST_Sequence, function(self, tw) { self.expressions = do_list(self.expressions, tw); }); + DEF(AST_Await, function(self, tw) { + self.expression = self.expression.transform(tw); + }); DEF(AST_Dot, function(self, tw) { self.expression = self.expression.transform(tw); }); diff --git a/test/compress/async.js b/test/compress/async.js new file mode 100644 index 00000000..2e4698d2 --- /dev/null +++ b/test/compress/async.js @@ -0,0 +1,218 @@ +await_await: { + input: { + (async function() { + console.log("PASS"); + await await 42; + })(); + } + expect: { + (async function() { + console.log("PASS"); + await await 42; + })(); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +defun_name: { + input: { + async function await() { + console.log("PASS"); + } + await(); + } + expect: { + async function await() { + console.log("PASS"); + } + await(); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +nested_await: { + input: { + (async function() { + console.log(function(await) { + return await; + }("PASS")); + })(); + } + expect: { + (async function() { + console.log(function(await) { + return await; + }("PASS")); + })(); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +reduce_single_use_defun: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + async function f(a) { + console.log(a); + } + f("PASS"); + } + expect: { + (async function(a) { + console.log(a); + })("PASS"); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +dont_inline: { + options = { + inline: true, + } + input: { + (async function() { + A; + })().catch(function() {}); + console.log("PASS"); + } + expect: { + (async function() { + A; + })().catch(function() {}); + console.log("PASS"); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +evaluate: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a = async function() {}(); + console.log(typeof a); + } + expect: { + var a = async function() {}(); + console.log(typeof a); + } + expect_stdout: "object" + node_version: ">=8" +} + +negate: { + options = { + side_effects: true, + } + input: { + console && async function() {} && console.log("PASS"); + } + expect: { + console && async function() {} && console.log("PASS"); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +negate_iife: { + options = { + negate_iife: true, + } + input: { + (async function() { + console.log("PASS"); + })(); + } + expect: { + !async function() { + console.log("PASS"); + }(); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +collapse_vars_1: { + options = { + collapse_vars: true, + } + input: { + var a = "FAIL"; + (async function() { + a = "PASS"; + await 42; + return "PASS"; + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (async function() { + a = "PASS"; + await 42; + return "PASS"; + })(); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +collapse_vars_2: { + options = { + collapse_vars: true, + } + input: { + var a = "FAIL"; + (async function() { + await (a = "PASS"); + return "PASS"; + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (async function() { + await (a = "PASS"); + return "PASS"; + })(); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=8" +} + +collapse_vars_3: { + options = { + collapse_vars: true, + } + input: { + var a = "FAIL"; + (async function() { + await (a = "PASS", 42); + return "PASS"; + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (async function() { + await (a = "PASS", 42); + return "PASS"; + })(); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=8" +} diff --git a/test/mocha/async.js b/test/mocha/async.js new file mode 100644 index 00000000..dc1aa2f4 --- /dev/null +++ b/test/mocha/async.js @@ -0,0 +1,23 @@ +var assert = require("assert"); +var UglifyJS = require("../node"); + +describe("async", function() { + it("Should reject `await` as symbol name within async functions only", function() { + [ + "function await() {}", + "function(await) {}", + "function() { await; }", + "function() { await:{} }", + "function() { var await; }", + "function() { function await() {} }", + "function() { try {} catch (await) {} }", + ].forEach(function(code) { + UglifyJS.parse("(" + code + ")();"); + assert.throws(function() { + UglifyJS.parse("(async " + code + ")();"); + }, function(e) { + return e instanceof UglifyJS.JS_Parse_Error; + }, code); + }); + }); +}); diff --git a/test/reduce.js b/test/reduce.js index cda79f8a..3cd7f67d 100644 --- a/test/reduce.js +++ b/test/reduce.js @@ -618,7 +618,7 @@ function is_timed_out(result) { } function is_statement(node) { - return node instanceof U.AST_Statement && !(node instanceof U.AST_Function); + return node instanceof U.AST_Statement && !(node instanceof U.AST_AsyncFunction || node instanceof U.AST_Function); } function merge_sequence(array, node) { diff --git a/test/sandbox.js b/test/sandbox.js index c1575783..46b1a410 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -32,11 +32,12 @@ function createContext() { function safe_log(arg, level) { if (arg) switch (typeof arg) { - case "function": + case "function": return arg.toString(); - case "object": + case "object": if (arg === global) return "[object global]"; if (/Error$/.test(arg.name)) return arg.toString(); + if (typeof arg.then == "function") return "[object Promise]"; arg.constructor.toString(); if (level--) for (var key in arg) { var desc = Object.getOwnPropertyDescriptor(arg, key); diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 7c76b348..e47e5410 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -298,6 +298,8 @@ var VAR_NAMES = [ "arguments", "Math", "parseInt", + "async", + "await", ]; var INITIAL_NAMES_LEN = VAR_NAMES.length; @@ -317,6 +319,7 @@ var TYPEOF_OUTCOMES = [ var avoid_vars = []; var block_vars = []; var unique_vars = []; +var async = false; var loops = 0; var funcs = 0; var called = Object.create(null); @@ -335,6 +338,7 @@ function createTopLevelCode() { VAR_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list block_vars.length = 0; unique_vars.length = 0; + async = false; loops = 0; funcs = 0; called = Object.create(null); @@ -393,7 +397,7 @@ function createArgs(recurmax, stmtDepth, canThrow) { return args.join(", "); } -function createAssignmentPairs(recurmax, noComma, stmtDepth, canThrow, varNames, maybe, dontStore) { +function createAssignmentPairs(recurmax, noComma, stmtDepth, canThrow, varNames, was_async) { var avoid = []; var len = unique_vars.length; var pairs = createPairs(recurmax); @@ -403,7 +407,10 @@ function createAssignmentPairs(recurmax, noComma, stmtDepth, canThrow, varNames, function createAssignmentValue(recurmax) { var current = VAR_NAMES; VAR_NAMES = (varNames || VAR_NAMES).slice(); + var save_async = async; + if (was_async != null) async = was_async; var value = varNames && rng(2) ? createValue() : createExpression(recurmax, noComma, stmtDepth, canThrow); + async = save_async; VAR_NAMES = current; return value; } @@ -429,7 +436,7 @@ function createAssignmentPairs(recurmax, noComma, stmtDepth, canThrow, varNames, } if (i < m) { unique_vars.push("a", "b", "c", "undefined", "NaN", "Infinity"); - var name = createVarName(maybe, dontStore); + var name = createVarName(MANDATORY); unique_vars.length -= 6; avoid.push(name); unique_vars.push(name); @@ -490,7 +497,7 @@ function createAssignmentPairs(recurmax, noComma, stmtDepth, canThrow, varNames, break; default: unique_vars.push("a", "b", "c", "undefined", "NaN", "Infinity"); - var name = createVarName(maybe, dontStore); + var name = createVarName(MANDATORY); unique_vars.length -= 6; avoid.push(name); unique_vars.push(name); @@ -572,12 +579,18 @@ function createBlockVariables(recurmax, stmtDepth, canThrow, fn) { } } +function makeFunction(name) { + return (async ? "async function " : "function ") + name; +} + function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { if (--recurmax < 0) { return ";"; } if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; var s = []; var name, args; var varNames = VAR_NAMES.slice(); + var save_async = async; + async = rng(50) == 0; createBlockVariables(recurmax, stmtDepth, canThrow, function(defns) { if (allowDefun || rng(5) > 0) { name = "f" + funcs++; @@ -589,13 +602,13 @@ function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { var params; if ((!allowDefun || !(name in called)) && rng(2)) { called[name] = false; - var pairs = createAssignmentPairs(recurmax, COMMA_OK, stmtDepth, canThrow, varNames, MANDATORY); + var pairs = createAssignmentPairs(recurmax, COMMA_OK, stmtDepth, canThrow, varNames, save_async); params = pairs.names.join(", "); args = pairs.values.join(", "); } else { params = createParams(); } - s.push("function " + name + "(" + params + "){", strictMode()); + s.push(makeFunction(name) + "(" + params + "){", strictMode()); s.push(defns()); if (rng(5) === 0) { // functions with functions. lower the recursion to prevent a mess. @@ -607,6 +620,7 @@ function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { s.push("}", ""); s = filterDirective(s).join("\n"); }); + async = save_async; VAR_NAMES = varNames; if (!allowDefun) { @@ -733,7 +747,7 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn return "switch (" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") { " + createSwitchParts(recurmax, 4, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + "}"; case STMT_VAR: if (!rng(20)) { - var pairs = createAssignmentPairs(recurmax, NO_COMMA, stmtDepth, canThrow, null, MANDATORY); + var pairs = createAssignmentPairs(recurmax, NO_COMMA, stmtDepth, canThrow); return "var " + pairs.names.map(function(name, index) { return index in pairs.values ? name + " = " + pairs.values[index] : name; }).join(", ") + ";"; @@ -853,7 +867,8 @@ function createExpression(recurmax, noComma, stmtDepth, canThrow) { case 2: return "((c = c + 1) + (" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + "))"; // c only gets incremented default: - return "(" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + ")"; + var expr = "(" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + ")"; + return async && rng(50) == 0 ? "(await" + expr + ")" : expr; } } @@ -909,6 +924,8 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { case p++: case p++: var nameLenBefore = VAR_NAMES.length; + var save_async = async; + async = rng(50) == 0; unique_vars.push("c"); var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that. unique_vars.pop(); @@ -916,7 +933,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { switch (rng(5)) { case 0: s.push( - "(function " + name + "(){", + "(" + makeFunction(name) + "(){", strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), rng(2) == 0 ? "})" : "})()" @@ -924,7 +941,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { break; case 1: s.push( - "+function " + name + "(){", + "+" + makeFunction(name) + "(){", strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), "}()" @@ -932,7 +949,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { break; case 2: s.push( - "!function " + name + "(){", + "!" + makeFunction(name) + "(){", strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), "}()" @@ -940,13 +957,14 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { break; case 3: s.push( - "void function " + name + "(){", + "void " + makeFunction(name) + "(){", strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), "}()" ); break; default: + async = false; createBlockVariables(recurmax, stmtDepth, canThrow, function(defns) { var instantiate = rng(4) ? "new " : ""; s.push( @@ -965,6 +983,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { }); break; } + async = save_async; VAR_NAMES.length = nameLenBefore; return filterDirective(s).join("\n"); case p++: @@ -1361,7 +1380,7 @@ function createVarName(maybe, dontStore) { do { name = VAR_NAMES[rng(VAR_NAMES.length)]; if (suffix) name += "_" + suffix; - } while (unique_vars.indexOf(name) >= 0 || block_vars.indexOf(name) >= 0); + } while (unique_vars.indexOf(name) >= 0 || block_vars.indexOf(name) >= 0 || async && name == "await"); if (suffix && !dontStore) VAR_NAMES.push(name); return name; } -- 2.34.1