From 32f76f7ff89592e2dcb8cfa66d3ee1f95ae78d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 03:03:38 +0000 Subject: [PATCH 001/121] Starting destructuring. --- lib/ast.js | 90 +++++++++++++++++++++++++++++++++++++++- lib/compress.js | 34 ++++++++++----- lib/output.js | 11 +++++ lib/parse.js | 102 ++++++++++++++++++++++++++++++++------------- lib/scope.js | 14 +++++++ test/parser.js | 103 ++++++++++++++++++++++++++++++++++++++++++++++ test/run-tests.js | 4 ++ 7 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 test/parser.js diff --git a/lib/ast.js b/lib/ast.js index 2e539cff..b8562233 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -359,13 +359,73 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); +var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { + $documentation: "A set of arrow function parameters or a sequence expression. This is used because when the parser sees a \"(\" it could be the start of a seq, or the start of a parameter list of an arrow function.", + $propdoc: { + expressions: "[AST_Expression|AST_Destructuring*] array of expressions or argument names or destructurings." + }, + as_params: function (croak) { + // We don't want anything which doesn't belong in a destructuring + var root = this; + return this.expressions.map(function to_fun_args(ex) { + if (ex instanceof AST_Object) { + if (ex.properties.length == 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + return new AST_Destructuring({ + start: ex.start, + end: ex.end, + is_array: false, + names: ex.properties.map(to_fun_args) + }); + } else if (ex instanceof AST_ObjectSymbol) { + return new AST_SymbolFunarg({ + name: ex.symbol.name, + start: ex.start, + end: ex.end + }); + } else if (ex instanceof AST_SymbolRef) { + return new AST_SymbolFunarg({ + name: ex.name, + start: ex.start, + end: ex.end + }); + } else if (ex instanceof AST_Array) { + if (ex.elements.length === 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + return new AST_Destructuring({ + start: ex.start, + end: ex.end, + is_array: true, + names: ex.elements.map(to_fun_args) + }); + } else { + console.log(ex.__proto__.TYPE) + croak("Invalid function parameter", ex.start.line, ex.start.col); + } + }); + }, + as_expr: function (croak) { + return AST_Seq.from_array(this.expressions); + } +}); + var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { $documentation: "Base class for functions", $propdoc: { name: "[AST_SymbolDeclaration?] the name of this function", - argnames: "[AST_SymbolFunarg*] array of function arguments", + argnames: "[AST_SymbolFunarg|AST_Destructuring*] array of function arguments or destructurings", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, + args_as_names: function () { + var out = []; + this.walk(new TreeWalker(function (parm) { + var that = this; + if (parm instanceof AST_SymbolFunarg) { + out.push(parm); + } + })); + return out; + }, _walk: function(visitor) { return visitor._visit(this, function(){ if (this.name) this.name._walk(visitor); @@ -385,10 +445,26 @@ var AST_Function = DEFNODE("Function", null, { $documentation: "A function expression" }, AST_Lambda); +var AST_Arrow = DEFNODE("Arrow", null, { + $documentation: "An ES6 Arrow function ((a) => b)" +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); +/* -----[ DESTRUCTURING ]----- */ +var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { + $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.names.forEach(function(name){ + name._walk(visitor); + }); + }); + } +}); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { @@ -774,6 +850,18 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { } }, AST_ObjectProperty); +var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { + $propdoc: { + symbol: "[AST_SymbolRef] what symbol it is" + }, + $documentation: "A symbol in an object", + _walk: function (visitor) { + return visitor._visit(this, function(){ + this.symbol._walk(visitor); + }); + } +}, AST_ObjectProperty); + var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { $documentation: "An object setter property", }, AST_ObjectProperty); diff --git a/lib/compress.js b/lib/compress.js index 401a1c75..9a7ccfc3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -231,6 +231,7 @@ merge(Compressor.prototype, { } function make_arguments_names_list(func) { return func.argnames.map(function(sym){ + // TODO not sure what to do here with destructuring return make_node(AST_String, sym, { value: sym.name }); }); } @@ -1089,17 +1090,26 @@ merge(Compressor.prototype, { if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { if (compressor.option("unsafe") && !compressor.option("keep_fargs")) { for (var a = node.argnames, i = a.length; --i >= 0;) { - var sym = a[i]; - if (sym.unreferenced()) { - a.pop(); - compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { - name : sym.name, - file : sym.start.file, - line : sym.start.line, - col : sym.start.col - }); + if (a[i] instanceof AST_Destructuring) { + // Do not drop destructuring arguments. + // They constitute a type assertion, so dropping + // them would stop that TypeError which would happen + // if someone called it with an incorrectly formatted + // parameter. + break; + } else { + var sym = a[i]; + if (sym.unreferenced()) { + a.pop(); + compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { + name : sym.name, + file : sym.start.file, + line : sym.start.line, + col : sym.start.col + }); + } + else break; } - else break; } } } @@ -1263,9 +1273,10 @@ merge(Compressor.prototype, { // collect only vars which don't show up in self's arguments list var defs = []; vars.each(function(def, name){ + // TODO test this too if (self instanceof AST_Lambda && find_if(function(x){ return x.name == def.name.name }, - self.argnames)) { + self.args_as_names())) { vars.del(name); } else { def = def.clone(); @@ -1785,6 +1796,7 @@ merge(Compressor.prototype, { if (ex !== ast) throw ex; }; if (!fun) return self; + // TODO does this work with destructuring? Test it. var args = fun.argnames.map(function(arg, i){ return make_node(AST_String, self.args[i], { value: arg.print_to_string() diff --git a/lib/output.js b/lib/output.js index 1d67b1b9..fcaa364b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -598,6 +598,17 @@ function OutputStream(options) { output.print_string(self.value, self.quote); output.semicolon(); }); + + DEFPRINT(AST_Destructuring, function (self, output) { + output.print(self.is_array ? "[" : "{"); + var first = true; + self.names.forEach(function (name) { + if (first) first = false; else { output.comma(); output.space(); } + name.print(output); + }) + output.print(self.is_array ? "]" : "}"); + }) + DEFPRINT(AST_Debugger, function(self, output){ output.print("debugger"); output.semicolon(); diff --git a/lib/parse.js b/lib/parse.js index e65c4faa..4e16171a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -644,6 +644,7 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, + in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -957,35 +958,58 @@ function parse($TEXT, options) { }; var function_ = function(ctor) { + var start = S.token + var in_statement = ctor === AST_Defun; var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; if (in_statement && !name) unexpected(); - expect("("); + + var args = params_or_seq_().as_params(croak); + var body = _function_body(); return new ctor({ - name: name, - argnames: (function(first, a){ - while (!is("punc", ")")) { - if (first) first = false; else expect(","); - a.push(as_symbol(AST_SymbolFunarg)); - } - next(); - return a; - })(true, []), - body: (function(loop, labels){ - ++S.in_function; - S.in_directives = true; - S.in_loop = 0; - S.labels = []; - var a = block_(); - --S.in_function; - S.in_loop = loop; - S.labels = labels; - return a; - })(S.in_loop, S.labels) + start : args.start, + end : body.end, + name : name, + argnames: args, + body : body }); }; + function params_or_seq_() { + var start = S.token + expect("("); + var first = true; + var a = []; + S.in_parameters = true; + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + a.push(expression(false)); + } + S.in_parameters = false; + var end = S.token + next(); + return new AST_ArrowParametersOrSeq({ + start: start, + end: end, + expressions: a + }); + } + + function _function_body() { + var loop = S.in_loop; + var labels = S.labels; + ++S.in_function; + S.in_directives = true; + S.in_loop = 0; + S.labels = []; + var a = block_(); + --S.in_function; + S.in_loop = loop; + S.labels = labels; + return a; + } + function if_() { var cond = parenthesised(), body = statement(), belse = null; if (is("keyword", "else")) { @@ -1224,6 +1248,7 @@ function parse($TEXT, options) { }); var object_ = embed_tokens(function() { + var start = S.token; expect("{"); var first = true, a = []; while (!is("punc", "}")) { @@ -1254,14 +1279,33 @@ function parse($TEXT, options) { continue; } } - expect(":"); - a.push(new AST_ObjectKeyVal({ - start : start, - quote : start.quote, - key : name, - value : expression(false), - end : prev() - })); + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + if (!S.in_parameters) { + croak("Invalid syntax", S.token.line, S.token.col); + } + a.push(new AST_ObjectSymbol({ + start: start, + end: start, + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + if (S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); + } } next(); return new AST_Object({ properties: a }); diff --git a/lib/scope.js b/lib/scope.js index 6c19c19a..a251f55b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -50,6 +50,7 @@ function SymbolDef(scope, index, orig) { this.references = []; this.global = false; this.mangled_name = null; + this.object_destructuring_arg = false; this.undeclared = false; this.constant = false; this.index = index; @@ -60,6 +61,7 @@ SymbolDef.prototype = { if (!options) options = {}; return (this.global && !options.toplevel) + || this.object_destructuring_arg || this.undeclared || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) || (options.keep_fnames @@ -94,6 +96,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var defun = null; var nesting = 0; + var object_destructuring_arg = false; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -104,6 +107,12 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ scope = save_scope; return true; } + if (node instanceof AST_Destructuring && node.is_array === false) { + object_destructuring_arg = true; // These don't nest + descend(); + object_destructuring_arg = false; + return true; + } if (node instanceof AST_Scope) { node.init_scope_vars(nesting); var save_scope = node.parent_scope = scope; @@ -127,6 +136,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ if (node instanceof AST_Symbol) { node.scope = scope; } + if (node instanceof AST_SymbolFunarg) { + node.object_destructuring_arg = object_destructuring_arg; + defun.def_variable(node); + } if (node instanceof AST_SymbolLambda) { defun.def_function(node); } @@ -250,6 +263,7 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ if (!this.variables.has(symbol.name)) { def = new SymbolDef(this, this.variables.size(), symbol); this.variables.set(symbol.name, def); + def.object_destructuring_arg = symbol.object_destructuring_arg; def.global = !this.parent_scope; } else { def = this.variables.get(symbol.name); diff --git a/test/parser.js b/test/parser.js new file mode 100644 index 00000000..b512f1ca --- /dev/null +++ b/test/parser.js @@ -0,0 +1,103 @@ + +var UglifyJS = require(".."); +var ok = require('assert'); + +module.exports = function () { + console.log("--- Parser tests"); + + // Destructuring arguments + + // Function argument nodes are correct + function get_args(args) { + return args.map(function (arg) { + return [arg.TYPE, arg.name]; + }); + } + + // Destructurings as arguments + var destr_fun1 = UglifyJS.parse('(function ({a, b}) {})').body[0].body; + var destr_fun2 = UglifyJS.parse('(function ([a, [b]]) {})').body[0].body; + + ok.equal(destr_fun1.argnames.length, 1); + ok.equal(destr_fun2.argnames.length, 1); + + var destruct1 = destr_fun1.argnames[0]; + var destruct2 = destr_fun2.argnames[0]; + + ok(destruct1 instanceof UglifyJS.AST_Destructuring); + ok(destruct2 instanceof UglifyJS.AST_Destructuring); + ok(destruct2.names[1] instanceof UglifyJS.AST_Destructuring); + + ok.equal(destruct1.start.value, '{'); + ok.equal(destruct1.end.value, '}'); + ok.equal(destruct2.start.value, '['); + ok.equal(destruct2.end.value, ']'); + + ok.equal(destruct1.is_array, false); + ok.equal(destruct2.is_array, true); + + var aAndB = [ + ['SymbolFunarg', 'a'], + ['SymbolFunarg', 'b'] + ]; + + ok.deepEqual( + [ + destruct1.names[0].TYPE, + destruct1.names[0].name], + aAndB[0]); + + ok.deepEqual( + [ + destruct2.names[1].names[0].TYPE, + destruct2.names[1].names[0].name + ], + aAndB[1]); + + ok.deepEqual( + get_args(destr_fun1.args_as_names()), + aAndB) + ok.deepEqual( + get_args(destr_fun2.args_as_names()), + aAndB) + + // Making sure we don't accidentally accept things which + // Aren't argument destructurings + + ok.throws(function () { + UglifyJS.parse('(function ([]) {})'); + }, /Invalid destructuring function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function ( { a, [ b ] } ) { })') + }); + + ok.throws(function () { + UglifyJS.parse('(function (1) { })'); + }, /Invalid function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function (this) { })'); + }); + + ok.throws(function () { + UglifyJS.parse('(function ([1]) { })'); + }, /Invalid function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function [a] { })'); + }); + + ok.throws(function () { + // Note: this *is* a valid destructuring, but before we implement + // destructuring (right now it's only destructuring *arguments*), + // this won't do. + UglifyJS.parse('[{a}]'); + }); +} + +// Run standalone +if (module.parent === null) { + module.exports(); +} + diff --git a/test/run-tests.js b/test/run-tests.js index 215f6af8..92872f92 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -23,6 +23,10 @@ run_ast_conversion_tests({ iterations: 1000 }); +var run_parser_tests = require('./parser.js'); + +run_parser_tests(); + /* -----[ utils ]----- */ function tmpl() { From f7460166dd5f12a4966c6d55c6515f6d1dab4cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 10:23:17 +0000 Subject: [PATCH 002/121] remove trace statement --- lib/ast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index b8562233..16cfc387 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -399,7 +399,6 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { names: ex.elements.map(to_fun_args) }); } else { - console.log(ex.__proto__.TYPE) croak("Invalid function parameter", ex.start.line, ex.start.col); } }); From 4644becb9b2e272dcecf506c2911246c0298a3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 10:44:41 +0000 Subject: [PATCH 003/121] do not support destructuring arguments and ngInject --- lib/compress.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 9a7ccfc3..bfdb1952 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -230,9 +230,16 @@ merge(Compressor.prototype, { return /@ngInject/.test(comment.value); } function make_arguments_names_list(func) { + var foundDestructuring = false; return func.argnames.map(function(sym){ - // TODO not sure what to do here with destructuring + if (sym instanceof AST_Destructuring) { + compressor.warn("Function with destructuring arguments marked with @ngInject [{file}:{line},{col}]", token); + foundDestructuring = true; + } + if (foundDestructuring) { return null; } return make_node(AST_String, sym, { value: sym.name }); + }).filter(function (name) { + return name !== null; }); } function make_array(orig, elements) { From 96b89e34a3c404749b9b58efa78b490989aaba3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 13:27:30 +0000 Subject: [PATCH 004/121] test that names used in destructurings don't get hoisted --- lib/compress.js | 1 - test/compress/hoist.js | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 test/compress/hoist.js diff --git a/lib/compress.js b/lib/compress.js index bfdb1952..9e94a6b7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1280,7 +1280,6 @@ merge(Compressor.prototype, { // collect only vars which don't show up in self's arguments list var defs = []; vars.each(function(def, name){ - // TODO test this too if (self instanceof AST_Lambda && find_if(function(x){ return x.name == def.name.name }, self.args_as_names())) { diff --git a/test/compress/hoist.js b/test/compress/hoist.js new file mode 100644 index 00000000..2b359fc5 --- /dev/null +++ b/test/compress/hoist.js @@ -0,0 +1,67 @@ + +hoist_vars: { + options = { + hoist_vars: true + } + input: { + function a() { + bar(); + var var1; + var var2; + } + function b(anArg) { + bar(); + var var1; + var anArg; + } + } + expect: { + function a() { + var var1, var2; // Vars go up and are joined + bar(); + } + function b(anArg) { + var var1; + bar(); + // But vars named like arguments go away! + } + } +} + +hoist_funs: { + options = { + hoist_funs: true + } + input: { + function a() { + bar(); + function foo() {} + } + } + expect: { + function a() { + function foo() {} // Funs go up + bar(); + } + } +} + +hoist_no_destructurings: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + function a([anArg]) { + bar(); + var var1; + var anArg; // Because anArg is already declared, this goes away! + } + } + expect: { + function a([anArg]) { + var var1; + bar(); + } + } +} From ad344c5be31d9c5df1ae906d7bb49b430fffed88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 20:08:06 +0000 Subject: [PATCH 005/121] Add a test to verify that destructuring arguments work with #203 code --- lib/compress.js | 1 - test/compress/issue-203.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-203.js diff --git a/lib/compress.js b/lib/compress.js index 9e94a6b7..3788ab7d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1802,7 +1802,6 @@ merge(Compressor.prototype, { if (ex !== ast) throw ex; }; if (!fun) return self; - // TODO does this work with destructuring? Test it. var args = fun.argnames.map(function(arg, i){ return make_node(AST_String, self.args[i], { value: arg.print_to_string() diff --git a/test/compress/issue-203.js b/test/compress/issue-203.js new file mode 100644 index 00000000..d894c586 --- /dev/null +++ b/test/compress/issue-203.js @@ -0,0 +1,30 @@ + +compress_new_function: { + options = { + unsafe: true + } + input: { + new Function("aa, bb", 'return aa;'); + } + expect: { + Function("a", "b", "return a"); + } +} + +compress_new_function_with_destruct: { + options = { + unsafe: true + } + input: { + new Function("aa, [bb]", 'return aa;'); + new Function("aa, {bb}", 'return aa;'); + new Function("[[aa]], [{bb}]", 'return aa;'); + } + expect: { + Function("a", "[b]", "return a"); + Function("a", "{bb}", "return a"); + Function("[[a]]", "[{bb}]", 'return a'); + } +} + + From 9d7d365c2bfbd1e75197607d3a5ee97cc7fa6c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 13 Apr 2015 01:25:46 +0100 Subject: [PATCH 006/121] for...of --- lib/ast.js | 4 ++++ lib/output.js | 6 +++++- lib/parse.js | 24 +++++++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 16cfc387..7caccfd3 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -262,6 +262,10 @@ var AST_ForIn = DEFNODE("ForIn", "init name object", { } }, AST_IterationStatement); +var AST_ForOf = DEFNODE("ForOf", null, { + $documentation: "A `for ... of` statement", +}, AST_ForIn); + var AST_With = DEFNODE("With", "expression", { $documentation: "A `with` statement", $propdoc: { diff --git a/lib/output.js b/lib/output.js index fcaa364b..28c1facd 100644 --- a/lib/output.js +++ b/lib/output.js @@ -719,7 +719,11 @@ function OutputStream(options) { output.with_parens(function(){ self.init.print(output); output.space(); - output.print("in"); + if (self instanceof AST_ForOf) { + output.print("of"); + } else { + output.print("in"); + } output.space(); self.object.print(output); }); diff --git a/lib/parse.js b/lib/parse.js index 4e16171a..7b175a7e 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var void while with'; +var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof 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 long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -921,11 +921,17 @@ function parse($TEXT, options) { init = is("keyword", "var") ? (next(), var_(true)) : expression(true, true); - if (is("operator", "in")) { + var is_in = is("operator", "in"); + var is_of = is("keyword", "of"); + if (is_in || is_of) { if (init instanceof AST_Var && init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); next(); - return for_in(init); + if (is_in) { + return for_in(init); + } else { + return for_of(init); + } } } return regular_for(init); @@ -945,6 +951,18 @@ function parse($TEXT, options) { }); }; + function for_of(init) { + var lhs = init instanceof AST_Var ? init.definitions[0].name : null; + var obj = expression(true); + expect(")"); + return new AST_ForOf({ + init : init, + name : lhs, + object : obj, + body : in_loop(statement) + }); + }; + function for_in(init) { var lhs = init instanceof AST_Var ? init.definitions[0].name : null; var obj = expression(true); From fa5c4f2d036abad60538b58ad22e5f863bcc5667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 11 Jan 2015 20:07:19 +0000 Subject: [PATCH 007/121] Adding arrow functions --- lib/ast.js | 2 +- lib/compress.js | 2 ++ lib/output.js | 28 ++++++++++++++++++++ lib/parse.js | 56 ++++++++++++++++++++++++++++++++++++++++ lib/transform.js | 6 ++++- test/compress/harmony.js | 37 ++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 test/compress/harmony.js diff --git a/lib/ast.js b/lib/ast.js index 7caccfd3..5c78a99f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -142,7 +142,7 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { }, AST_Statement); function walk_body(node, visitor) { - if (node.body instanceof AST_Statement) { + if (node.body instanceof AST_Node) { node.body._walk(visitor); } else node.body.forEach(function(stat){ diff --git a/lib/compress.js b/lib/compress.js index 3788ab7d..89fdaec5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1019,6 +1019,7 @@ merge(Compressor.prototype, { }); OPT(AST_Block, function(self, compressor){ + if (self.body instanceof AST_Node) { return self; } self.body = tighten_body(self.body, compressor); return self; }); @@ -1225,6 +1226,7 @@ merge(Compressor.prototype, { var hoist_funs = compressor.option("hoist_funs"); var hoist_vars = compressor.option("hoist_vars"); var self = this; + if (!(self.body instanceof Array)) { return self; } // Hoisting makes no sense in an arrow func if (hoist_funs || hoist_vars) { var dirs = []; var hoisted = []; diff --git a/lib/output.js b/lib/output.js index 28c1facd..80235ca9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -763,6 +763,34 @@ function OutputStream(options) { self._do_print(output); }); + AST_Arrow.DEFMETHOD("_do_print", function(output){ + var self = this; + var parent = output.parent(); + var needs_parens = parent instanceof AST_Binary || + parent instanceof AST_Unary || + parent instanceof AST_Call; + if (needs_parens) { output.print("(") } + if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol) { + self.argnames[0].print(output); + } else { + output.with_parens(function(){ + self.argnames.forEach(function(arg, i){ + if (i) output.comma(); + arg.print(output); + }); + }); + } + output.space(); + output.print('=>'); + output.space(); + if (self.body instanceof AST_Node) { + this.body.print(output); + } else { + print_bracketed(this.body, output); + } + if (needs_parens) { output.print(")") } + }); + /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); diff --git a/lib/parse.js b/lib/parse.js index 7b175a7e..96d3b4c3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -510,6 +510,16 @@ function tokenizer($TEXT, filename, html5_comments) { return S.regex_allowed ? read_regexp("") : read_operator("/"); }; + function handle_eq_sign() { + next(); + if (peek() === ">") { + next(); + return token("arrow", "=>"); + } else { + return read_operator("="); + } + }; + function handle_dot() { next(); return is_digit(peek().charCodeAt(0)) @@ -559,6 +569,7 @@ function tokenizer($TEXT, filename, html5_comments) { case 34: case 39: return read_string(ch); case 46: return handle_dot(); case 47: return handle_slash(); + case 61: return handle_eq_sign(); } if (is_digit(code)) return read_num(); if (PUNC_CHARS(ch)) return token("punc", next()); @@ -975,6 +986,41 @@ function parse($TEXT, options) { }); }; + var arrow_function = function(args) { + expect_token("arrow", "=>"); + + if (args instanceof AST_SymbolRef) { + args = [args]; + } else if (args instanceof AST_Seq) { + args = args.to_array(); + } else if (args instanceof AST_Node) { + croak("Invalid syntax", args.start.line, args.start.col); + } + + for (var i = 0; i < args.length; i++) { + if (!(args[i] instanceof AST_SymbolRef)) { + croak("Invalid parameter for an arrow function", args[i].start.line, args[i].start.col); + } + + args[i] = new AST_SymbolFunarg({ + name: args[i].name, + start: args[i].start, + end: args[i].end + }) + } + + return new AST_Arrow({ + argnames: args, + body: (function(){ + if (is("punc", "{")) { + return _function_body(); + } else { + return expression(true); + } + })() + }); + }; + var function_ = function(ctor) { var start = S.token @@ -1486,6 +1532,13 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + + if (start.value == "(" && peek().value == ")") { + next(); // ( + next(); // ) + return arrow_function([]); + } + var left = maybe_conditional(no_in), val = S.token.value; if (is("operator") && ASSIGNMENT(val)) { if (is_assignable(left)) { @@ -1500,6 +1553,9 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } + if (is("arrow")) { + return arrow_function(left) + } return left; }; diff --git a/lib/transform.js b/lib/transform.js index c3c34f58..41c82c99 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -166,7 +166,11 @@ TreeTransformer.prototype = new TreeWalker; _(AST_Lambda, function(self, tw){ if (self.name) self.name = self.name.transform(tw); self.argnames = do_list(self.argnames, tw); - self.body = do_list(self.body, tw); + if (self.body instanceof AST_Node) { + self.body = self.body.transform(tw); + } else { + self.body = do_list(self.body, tw); + } }); _(AST_Call, function(self, tw){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js new file mode 100644 index 00000000..3c07c49e --- /dev/null +++ b/test/compress/harmony.js @@ -0,0 +1,37 @@ +arrow_functions: { + input: { + (a) => b; // 1 args + (a, b) => c; // n args + () => b; // 0 args + (a) => (b) => c; // func returns func returns func + (a) => ((b) => c); // So these parens are dropped + () => (b,c) => d; // func returns func returns func + a=>{return b;} + a => 'lel'; // Dropping the parens + } + expect_exact: "a=>b;(a,b)=>c;()=>b;a=>b=>c;a=>b=>c;()=>(b,c)=>d;a=>{return b};a=>\"lel\";" +} + +arrow_function_parens: { + input: { + something && (() => {}); + } + expect_exact: "something&&(()=>{});" +} +arrow_function_parens_2: { + input: { + (() => null)(); + } + expect_exact: "(()=>null)();" +} + +regression_arrow_functions_and_hoist: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + (a) => b; + } + expect_exact: "a=>b;" +} From a68953c491fdabc9476fdc8d4db50be87ccf5741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 4 Aug 2015 00:14:18 +0100 Subject: [PATCH 008/121] => with destructuring arguments. Requires a lot of parser changes --- lib/ast.js | 5 + lib/parse.js | 218 +++++++++++++++++++++------------------ test/compress/harmony.js | 62 +++++++++++ test/parser.js | 6 ++ 4 files changed, 193 insertions(+), 98 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 5c78a99f..0569133e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -387,6 +387,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end }); + } else if (ex instanceof AST_Destructuring) { + if (ex.names.length == 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + ex.names = ex.names.map(to_fun_args); + return ex; } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, diff --git a/lib/parse.js b/lib/parse.js index 96d3b4c3..913da4bc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -989,35 +989,17 @@ function parse($TEXT, options) { var arrow_function = function(args) { expect_token("arrow", "=>"); - if (args instanceof AST_SymbolRef) { - args = [args]; - } else if (args instanceof AST_Seq) { - args = args.to_array(); - } else if (args instanceof AST_Node) { - croak("Invalid syntax", args.start.line, args.start.col); - } + var argnames = args.as_params(croak); - for (var i = 0; i < args.length; i++) { - if (!(args[i] instanceof AST_SymbolRef)) { - croak("Invalid parameter for an arrow function", args[i].start.line, args[i].start.col); - } - - args[i] = new AST_SymbolFunarg({ - name: args[i].name, - start: args[i].start, - end: args[i].end - }) - } + var body = is("punc", "{") ? + _function_body(true) : + _function_body(false); return new AST_Arrow({ - argnames: args, - body: (function(){ - if (is("punc", "{")) { - return _function_body(); - } else { - return expression(true); - } - })() + start : args.start, + end : body.end, + argnames : argnames, + body : body }); }; @@ -1030,7 +1012,7 @@ function parse($TEXT, options) { unexpected(); var args = params_or_seq_().as_params(croak); - var body = _function_body(); + var body = _function_body(true); return new ctor({ start : args.start, end : body.end, @@ -1060,14 +1042,18 @@ function parse($TEXT, options) { }); } - function _function_body() { + function _function_body(block) { var loop = S.in_loop; var labels = S.labels; ++S.in_function; - S.in_directives = true; + if (block) + S.in_directives = true; S.in_loop = 0; S.labels = []; - var a = block_(); + if (block) + var a = block_(); + else + var a = expression(false); --S.in_function; S.in_loop = loop; S.labels = labels; @@ -1263,16 +1249,17 @@ function parse($TEXT, options) { if (is("punc")) { switch (start.value) { case "(": - next(); - var ex = expression(true); + var ex = params_or_seq_(); ex.start = start; ex.end = S.token; - expect(")"); - return subscripts(ex, allow_calls); + if (is("arrow", "=>")) { + return arrow_function(ex); + } + return subscripts(ex.as_expr(croak), allow_calls); case "[": return subscripts(array_(), allow_calls); case "{": - return subscripts(object_(), allow_calls); + return subscripts(object_or_object_destructuring_(), allow_calls); } unexpected(); } @@ -1311,68 +1298,101 @@ function parse($TEXT, options) { }); }); - var object_ = embed_tokens(function() { + var object_or_object_destructuring_ = embed_tokens(function() { var start = S.token; expect("{"); - var first = true, a = []; - while (!is("punc", "}")) { - if (first) first = false; else expect(","); - if (!options.strict && is("punc", "}")) - // allow trailing comma - break; - var start = S.token; - var type = start.type; - var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; + function try_an_object() { + var first = true, a = []; + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!options.strict && is("punc", "}")) + // allow trailing comma + break; + var start = S.token; + var type = start.type; + var name = as_property_name(); + if (type == "name" && !is("punc", ":")) { + if (name == "get") { + a.push(new AST_ObjectGetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; + } + if (name == "set") { + a.push(new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; + } } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - } - - if (!is("punc", ":")) { - // It's one of those object destructurings, the value is its own name - if (!S.in_parameters) { - croak("Invalid syntax", S.token.line, S.token.col); - } - a.push(new AST_ObjectSymbol({ - start: start, - end: start, - symbol: new AST_SymbolRef({ + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + if (!S.in_parameters) { + croak("Invalid syntax", S.token.line, S.token.col); + } + a.push(new AST_ObjectSymbol({ start: start, end: start, - name: name - }) - })); - } else { - if (S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + if (S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); } - expect(":"); - a.push(new AST_ObjectKeyVal({ - start : start, - key : name, - value : expression(false), - end : prev() - })); } + next(); + return new AST_Object({ properties: a }) } - next(); - return new AST_Object({ properties: a }); + + var obj = try_an_object(); + if (obj instanceof AST_Object) { return obj; } + + if (!S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + + var firstName = obj; + + var namesInDestructuring = []; + + namesInDestructuring.push( new AST_SymbolRef({ + start : prev(), + end : prev(), + name : firstName + })); + + while (!is("punc", "}")) { + expect(","); + namesInDestructuring.push(as_symbol(AST_SymbolRef)) + } + + expect('}'); + + return new AST_Destructuring({ + start : start, + end : S.token, + names : namesInDestructuring, + is_array : false + }) }); function as_property_name() { @@ -1530,16 +1550,13 @@ function parse($TEXT, options) { return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); }; + // In ES6, AssignmentExpression can also be an ArrowFunction var maybe_assign = function(no_in) { var start = S.token; - if (start.value == "(" && peek().value == ")") { - next(); // ( - next(); // ) - return arrow_function([]); - } + var left = maybe_conditional(no_in); + var val = S.token.value; - var left = maybe_conditional(no_in), val = S.token.value; if (is("operator") && ASSIGNMENT(val)) { if (is_assignable(left)) { next(); @@ -1553,15 +1570,20 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } - if (is("arrow")) { - return arrow_function(left) - } return left; }; var expression = function(commas, no_in) { var start = S.token; var expr = maybe_assign(no_in); + if (expr instanceof AST_SymbolRef && is("arrow", "=>")) { + expr = new AST_ArrowParametersOrSeq({ + start: expr.start, + end: expr.end, + expressions: [expr] + }); + return arrow_function(expr); + } if (commas && is("punc", ",")) { next(); return new AST_Seq({ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 3c07c49e..02d2b299 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -35,3 +35,65 @@ regression_arrow_functions_and_hoist: { } expect_exact: "a=>b;" } + +destructuring_arguments: { + input: { + (function ( a ) { }); + (function ( [ a ] ) { }); + (function ( [ a, b ] ) { }); + (function ( [ [ a ] ] ) { }); + (function ( [ [ a, b ] ] ) { }); + (function ( [ a, [ b ] ] ) { }); + (function ( [ [ b ], a ] ) { }); + + (function ( { a } ) { }); + (function ( { a, b } ) { }); + + (function ( [ { a } ] ) { }); + (function ( [ { a, b } ] ) { }); + (function ( [ a, { b } ] ) { }); + (function ( [ { b }, a ] ) { }); + + ( [ a ] ) => { }; + ( [ a, b ] ) => { }; + + ( { a } ) => { }; + ( { a, b, c, d, e } ) => { }; + + ( [ a ] ) => b; + ( [ a, b ] ) => c; + + ( { a } ) => b; + ( { a, b } ) => c; + } + expect: { + (function(a){}); + (function([a]){}); + (function([a,b]){}); + (function([[a]]){}); + (function([[a,b]]){}); + (function([a,[b]]){}); + (function([[b],a]){}); + + (function({a}){}); + (function({a,b}){}); + + (function([{a}]){}); + (function([{a,b}]){}); + (function([a,{b}]){}); + (function([{b},a]){}); + + ([a])=>{}; + ([a,b])=>{}; + + ({a})=>{}; + ({a,b,c,d,e})=>{}; + + ([a])=>b; + ([a,b])=>c; + + ({a})=>b; + ({a,b})=>c; + } +} + diff --git a/test/parser.js b/test/parser.js index b512f1ca..e661907a 100644 --- a/test/parser.js +++ b/test/parser.js @@ -21,6 +21,12 @@ module.exports = function () { ok.equal(destr_fun1.argnames.length, 1); ok.equal(destr_fun2.argnames.length, 1); + var destr_fun1 = UglifyJS.parse('({a, b}) => null').body[0].body; + var destr_fun2 = UglifyJS.parse('([a, [b]]) => null').body[0].body; + + ok.equal(destr_fun1.argnames.length, 1); + ok.equal(destr_fun2.argnames.length, 1); + var destruct1 = destr_fun1.argnames[0]; var destruct2 = destr_fun2.argnames[0]; From ddd30eeaaad77662edb7c0de5e500379e81e8095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Wed, 5 Aug 2015 11:49:31 +0100 Subject: [PATCH 009/121] Uglifyjs already supports super as an implicit global! Just adding a test to indicate that. --- test/compress/super.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/compress/super.js diff --git a/test/compress/super.js b/test/compress/super.js new file mode 100644 index 00000000..d297a84e --- /dev/null +++ b/test/compress/super.js @@ -0,0 +1,9 @@ + +super_can_be_parsed: { + input: { + super(1,2); + super.meth(); + } + expect_exact: "super(1,2);super.meth();" +} + From 9863f0efa3e873a99cfa5b627c67e96761ee38de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 13 Apr 2015 01:26:26 +0100 Subject: [PATCH 010/121] expand parameters Conflicts: test/compress/harmony.js --- lib/ast.js | 26 +++++++++++++++++++-- lib/compress.js | 3 +++ lib/output.js | 5 +++++ lib/parse.js | 45 ++++++++++++++++++++++++++++++++----- test/compress/expansions.js | 17 ++++++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 test/compress/expansions.js diff --git a/lib/ast.js b/lib/ast.js index 0569133e..2622cf18 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -363,10 +363,27 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); +// TODO besides parameters and function calls, expansions can go in +// arrays, array destructuring parameters, and array destructuring +// assignment. But I'm not adding this right now because I'm trying +// to do the most minimal and independent changesets. +var AST_Expansion = DEFNODE("AST_Expansion", "symbol", { + $documentation: "An expandible argument, such as ...rest", + $propdoc: { + symbol: "AST_SymbolFunarg the name of the argument as a SymbolFunarg" + }, + _walk: function(visitor) { + var self = this; + return visitor._visit(this, function(){ + self.symbol.walk(visitor); + }); + } +}); + var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { $documentation: "A set of arrow function parameters or a sequence expression. This is used because when the parser sees a \"(\" it could be the start of a seq, or the start of a parameter list of an arrow function.", $propdoc: { - expressions: "[AST_Expression|AST_Destructuring*] array of expressions or argument names or destructurings." + expressions: "[AST_Expression|AST_Destructuring|AST_Expansion*] array of expressions or argument names or destructurings." }, as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring @@ -398,6 +415,8 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end }); + } else if (ex instanceof AST_Expansion) { + return ex; } else if (ex instanceof AST_Array) { if (ex.elements.length === 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -421,7 +440,7 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { $documentation: "Base class for functions", $propdoc: { name: "[AST_SymbolDeclaration?] the name of this function", - argnames: "[AST_SymbolFunarg|AST_Destructuring*] array of function arguments or destructurings", + argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, args_as_names: function () { @@ -431,6 +450,9 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { if (parm instanceof AST_SymbolFunarg) { out.push(parm); } + if (parm instanceof AST_Expansion) { + out.push(parm.symbol); + } })); return out; }, diff --git a/lib/compress.js b/lib/compress.js index 89fdaec5..6b480390 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1107,6 +1107,9 @@ merge(Compressor.prototype, { break; } else { var sym = a[i]; + if (sym instanceof AST_Expansion) { + sym = sym.symbol; + } if (sym.unreferenced()) { a.pop(); compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { diff --git a/lib/output.js b/lib/output.js index 0324d896..de54f909 100644 --- a/lib/output.js +++ b/lib/output.js @@ -607,6 +607,11 @@ function OutputStream(options) { output.semicolon(); }); + DEFPRINT(AST_Expansion, function (self, output) { + output.print('...'); + self.symbol.print(output); + }); + DEFPRINT(AST_Destructuring, function (self, output) { output.print(self.is_array ? "[" : "{"); var first = true; diff --git a/lib/parse.js b/lib/parse.js index 48bc70bb..ba87acf7 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -526,9 +526,16 @@ function tokenizer($TEXT, filename, html5_comments) { function handle_dot() { next(); - return is_digit(peek().charCodeAt(0)) - ? read_num(".") - : token("punc", "."); + if (is_digit(peek().charCodeAt(0))) { + return read_num("."); + } + if (peek() === ".") { + next(); // Consume second dot + next(); // Consume third dot + return token("expand", "..."); + } + + return token("punc", "."); }; function read_word() { @@ -1034,7 +1041,16 @@ function parse($TEXT, options) { S.in_parameters = true; while (!is("punc", ")")) { if (first) first = false; else expect(","); - a.push(expression(false)); + if (is("expand", "...")) { + next(); + a.push(new AST_Expansion({ + start: prev(), + symbol: as_symbol(AST_SymbolFunarg), + end: S.token, + })); + } else { + a.push(expression(false)); + } } S.in_parameters = false; var end = S.token @@ -1475,13 +1491,32 @@ function parse($TEXT, options) { return subscripts(new AST_Call({ start : start, expression : expr, - args : expr_list(")"), + args : call_args(), end : prev() }), true); } return expr; }; + var call_args = embed_tokens(function call_args() { + var first = true; + var args = []; + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + if (is("expand", "...")) { + next(); + args.push(new AST_Expansion({ + start: prev(), + symbol: as_symbol(AST_SymbolFunarg) + })); + } else { + args.push(expression(false)); + } + } + next(); + return args; + }); + var maybe_unary = function(allow_calls) { var start = S.token; if (is("operator") && UNARY_PREFIX(start.value)) { diff --git a/test/compress/expansions.js b/test/compress/expansions.js new file mode 100644 index 00000000..a6537547 --- /dev/null +++ b/test/compress/expansions.js @@ -0,0 +1,17 @@ + +expand_arguments: { + input: { + func(a, ...rest); + func(...all); + } + expect_exact: "func(a,...rest);func(...all);" +} + +expand_parameters: { + input: { + (function (a, ...b){}); + (function (...args){}); + } + expect_exact: "(function(a,...b){});(function(...args){});" +} + From e80ed38772db814040e2cc7c2fe6840a82939b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 7 Aug 2015 02:44:53 +0100 Subject: [PATCH 011/121] Super! --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 5 ++++- lib/scope.js | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 7caccfd3..078f748e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -940,6 +940,10 @@ var AST_This = DEFNODE("This", null, { $documentation: "The `this` symbol", }, AST_Symbol); +var AST_Super = DEFNODE("Super", null, { + $documentation: "The `super` symbol", +}, AST_Symbol); + var AST_Constant = DEFNODE("Constant", null, { $documentation: "Base class for all constants", getValue: function() { diff --git a/lib/output.js b/lib/output.js index 28c1facd..275cf9b6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1151,6 +1151,9 @@ function OutputStream(options) { DEFPRINT(AST_This, function(self, output){ output.print("this"); }); + DEFPRINT(AST_Super, function(self, output){ + output.print("super"); + }); DEFPRINT(AST_Constant, function(self, output){ output.print(self.getValue()); }); diff --git a/lib/parse.js b/lib/parse.js index 7b175a7e..269bb235 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1361,7 +1361,9 @@ function parse($TEXT, options) { function _make_symbol(type) { var name = S.token.value; - return new (name == "this" ? AST_This : type)({ + return new (name == "this" ? AST_This : + name == "super" ? AST_Super : + type)({ name : String(name), start : S.token, end : S.token @@ -1481,6 +1483,7 @@ function parse($TEXT, options) { function is_assignable(expr) { if (!options.strict) return true; if (expr instanceof AST_This) return false; + if (expr instanceof AST_Super) return false; return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); }; diff --git a/lib/scope.js b/lib/scope.js index a251f55b..55d7c4e9 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -472,6 +472,8 @@ AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ base54.consider("new"); else if (node instanceof AST_This) base54.consider("this"); + else if (node instanceof AST_Super) + base54.consider("super"); else if (node instanceof AST_Try) base54.consider("try"); else if (node instanceof AST_Catch) From 6f864402d3a026b23838c624caf6d1d2db3d1f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 03:24:54 +0100 Subject: [PATCH 012/121] Parse binary number literals --- lib/parse.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/parse.js b/lib/parse.js index ba87acf7..0b3b45cd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -59,6 +59,7 @@ var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; var RE_OCT_NUMBER = /^0[0-7]+$/; +var RE_BIN_NUMBER = /^0b[01]+$/; var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; var OPERATORS = makePredicate([ @@ -182,6 +183,8 @@ function parse_js_number(num) { return parseInt(num.substr(2), 16); } else if (RE_OCT_NUMBER.test(num)) { return parseInt(num.substr(1), 8); + } else if (RE_BIN_NUMBER.test(num)) { + return parseInt(num.substr(2), 2); } else if (RE_DEC_NUMBER.test(num)) { return parseFloat(num); } From dcce4e5c6625e2ebb14d12801ce934890e47081f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 22:05:42 +0100 Subject: [PATCH 013/121] Fix evaluating the typeof an arrow function. Using evaluate on used to cause a crash. --- lib/compress.js | 6 +++++- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 6b480390..7b07667e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -738,6 +738,9 @@ merge(Compressor.prototype, { // places too. :-( Wish JS had multiple inheritance. throw def; }); + def(AST_Arrow, function() { + throw def; + }); function ev(node, compressor) { if (!compressor) throw new Error("Compressor must be passed"); @@ -756,7 +759,8 @@ merge(Compressor.prototype, { case "typeof": // Function would be evaluated to an array and so typeof would // incorrectly return 'object'. Hence making is a special case. - if (e instanceof AST_Function) return typeof function(){}; + if (e instanceof AST_Function || + e instanceof AST_Arrow) return typeof function(){}; e = ev(e, compressor); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 02d2b299..812ad9d9 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,16 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +typeof_arrow_functions: { + options = { + evaluate: true + } + input: { + typeof (x) => null; + } + expect_exact: "\"function\";" +} + destructuring_arguments: { input: { (function ( a ) { }); From 4c12cccff9ed22ce924cbf0bad2424fbd89af98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 22:44:16 +0100 Subject: [PATCH 014/121] remove Symbol's argument when we're unsafe and it's undeclared --- lib/compress.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 6b480390..f6fe0f48 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1831,6 +1831,11 @@ merge(Compressor.prototype, { } } break; + case "Symbol": + // Symbol's argument is only used for debugging. + self.args = []; + return self; + break; } } else if (exp instanceof AST_Dot && exp.property == "toString" && self.args.length == 0) { From a8f8aa518b3c61e5dec847fb6d5d262bc8c595a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 17 Aug 2015 11:50:56 +0100 Subject: [PATCH 015/121] Add new-style octal literals and make the B and the O case insensitive. --- lib/parse.js | 5 ++++- test/compress/harmony.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 0b3b45cd..f3a585d1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -59,7 +59,8 @@ var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; var RE_OCT_NUMBER = /^0[0-7]+$/; -var RE_BIN_NUMBER = /^0b[01]+$/; +var RE_ES6_OCT_NUMBER = /^0o[0-7]+$/i; +var RE_BIN_NUMBER = /^0b[01]+$/i; var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; var OPERATORS = makePredicate([ @@ -183,6 +184,8 @@ function parse_js_number(num) { return parseInt(num.substr(2), 16); } else if (RE_OCT_NUMBER.test(num)) { return parseInt(num.substr(1), 8); + } else if (RE_ES6_OCT_NUMBER.test(num)) { + return parseInt(num.substr(2), 8); } else if (RE_BIN_NUMBER.test(num)) { return parseInt(num.substr(2), 2); } else if (RE_DEC_NUMBER.test(num)) { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 02d2b299..82cac95f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -97,3 +97,18 @@ destructuring_arguments: { } } +binary_literals: { + input: { + 0b1001; + 0B1001; + 0o11; + 0O11; + } + + expect: { + 9; + 9; + 9; + 9; + } +} From 36420183fd98107db055038bcf968335d947c1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 17 Aug 2015 16:23:43 +0100 Subject: [PATCH 016/121] s/binary/number/g --- test/compress/harmony.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 82cac95f..15130424 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -97,7 +97,7 @@ destructuring_arguments: { } } -binary_literals: { +number_literals: { input: { 0b1001; 0B1001; From ceebc466b9f5fb9da3c76a7efdfa6f5847e12312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 13 Aug 2015 02:56:57 +0100 Subject: [PATCH 017/121] prepare AST_Destructuring for the Ents --- lib/transform.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/transform.js b/lib/transform.js index 41c82c99..7858759a 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -163,6 +163,10 @@ TreeTransformer.prototype = new TreeWalker; if (self.value) self.value = self.value.transform(tw); }); + _(AST_Destructuring, function(self, tw) { + self.names = do_list(self.names, tw); + }); + _(AST_Lambda, function(self, tw){ if (self.name) self.name = self.name.transform(tw); self.argnames = do_list(self.argnames, tw); From 824ecfb8a278858cbc4b430ef91f23f498ca7109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 13 Aug 2015 02:58:08 +0100 Subject: [PATCH 018/121] A little refactoring. Add a new function to get all symbols in a destructuring. --- lib/ast.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index c050aee1..fe56bed5 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -445,15 +445,13 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { }, args_as_names: function () { var out = []; - this.walk(new TreeWalker(function (parm) { - var that = this; - if (parm instanceof AST_SymbolFunarg) { - out.push(parm); + for (var i = 0; i < this.argnames.length; i++) { + if (this.argnames[i] instanceof AST_Destructuring) { + out = out.concat(this.argnames[i].all_symbols()); + } else { + out.push(this.argnames[i]); } - if (parm instanceof AST_Expansion) { - out.push(parm.symbol); - } - })); + } return out; }, _walk: function(visitor) { @@ -492,6 +490,18 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { name._walk(visitor); }); }); + }, + all_symbols: function() { + var out = []; + this.walk(new TreeWalker(function (node) { + if (node instanceof AST_Symbol) { + out.push(node); + } + if (node instanceof AST_Expansion) { + out.push(node.symbol); + } + })); + return out; } }); From c44c2d6c21b9d0e884ca47aac294ee23d883ecc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 00:20:21 +0100 Subject: [PATCH 019/121] Parse and compress destructuring VarDefs --- lib/ast.js | 5 ++++- lib/compress.js | 15 ++++++++++++++- lib/parse.js | 33 +++++++++++++++++++++++++++------ test/compress/destructuring.js | 19 +++++++++++++++++++ test/compress/drop-unused.js | 15 +++++++++++++++ test/compress/hoist.js | 18 ++++++++++++++++++ test/parser.js | 8 ++++++++ 7 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 test/compress/destructuring.js diff --git a/lib/ast.js b/lib/ast.js index fe56bed5..2c73b6d3 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -665,9 +665,12 @@ var AST_Const = DEFNODE("Const", null, { var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { - name: "[AST_SymbolVar|AST_SymbolConst] name of the variable", + name: "[AST_SymbolVar|AST_SymbolConst|AST_Destructuring] name of the variable", value: "[AST_Node?] initializer, or null of there's no initializer" }, + is_destructuring: function() { + return this.name instanceof AST_Destructuring; + }, _walk: function(visitor) { return visitor._visit(this, function(){ this.name._walk(visitor); diff --git a/lib/compress.js b/lib/compress.js index cbd1caee..51fd18dc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1056,6 +1056,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && scope === self) { node.definitions.forEach(function(def){ + if (def.is_destructuring()) return; /* Destructurings are type assertions! */ if (def.value) { initializations.add(def.name.name, def.value); if (def.value.has_side_effects(compressor)) { @@ -1142,6 +1143,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ + if (def.is_destructuring()) return true; if (member(def.name.definition(), in_use)) return true; var w = { name : def.name.name, @@ -1262,6 +1264,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Var && hoist_vars) { node.definitions.forEach(function(def){ + if (def.is_destructuring()) { return; } vars.set(def.name.name, def); ++vars_found; }); @@ -1702,13 +1705,23 @@ merge(Compressor.prototype, { AST_Definitions.DEFMETHOD("to_assignments", function(){ var assignments = this.definitions.reduce(function(a, def){ - if (def.value) { + if (def.value && !def.is_destructuring()) { var name = make_node(AST_SymbolRef, def.name, def.name); a.push(make_node(AST_Assign, def, { operator : "=", left : name, right : def.value })); + } else if (def.value) { + // Because it's a destructuring, do not turn into an assignment. + var varDef = make_node(AST_VarDef, def, { + name: def.name, + value: def.value + }); + var var_ = make_node(AST_Var, def, { + definitions: [ varDef ] + }); + a.push(var_); } return a; }, []); diff --git a/lib/parse.js b/lib/parse.js index 2351dfc1..a0717c1f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1180,13 +1180,24 @@ function parse($TEXT, options) { function vardefs(no_in, in_const) { var a = []; + var def; for (;;) { - a.push(new AST_VarDef({ - start : S.token, - name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), - value : is("operator", "=") ? (next(), expression(false, no_in)) : null, - end : prev() - })); + if (is("punc", "{") || is("punc", "[")) { + def = new AST_VarDef({ + start: S.token, + name: destructuring_(), + value: (expect_token("operator", "="), expression(false, no_in)), + end: prev() + }); + } else { + def = new AST_VarDef({ + start : S.token, + name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), + value : is("operator", "=") ? (next(), expression(false, no_in)) : null, + end : prev() + }) + } + a.push(def); if (!is("punc", ",")) break; next(); @@ -1194,6 +1205,16 @@ function parse($TEXT, options) { return a; }; + var destructuring_ = embed_tokens(function () { + var is_array = is("punc", "["); + var closing = is_array ? ']' : '}'; + next() + return new AST_Destructuring({ + names: expr_list(closing), + is_array: is_array + }) + }); + var var_ = function(no_in) { return new AST_Var({ start : prev(), diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js new file mode 100644 index 00000000..58a77cd4 --- /dev/null +++ b/test/compress/destructuring.js @@ -0,0 +1,19 @@ + +destructuring_arrays: { + input: { + var [aa, bb] = cc; + } + expect: { + var[aa,bb]=cc; + } +} + +destructuring_objects: { + input: { + var {aa, bb} = {aa:1, bb:2}; + } + expect: { + var{aa,bb}={aa:1,bb:2}; + } +} + diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index c1cf5c3c..9bee9a2b 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -164,6 +164,21 @@ used_var_in_catch: { } } +unused_keep_harmony_destructuring: { + options = { unused: true }; + input: { + function foo() { + var {x, y} = foo; + var a = foo; + } + } + expect: { + function foo() { + var {x, y} = foo; + } + } +} + keep_fnames: { options = { unused: true, keep_fnames: true, unsafe: true }; input: { diff --git a/test/compress/hoist.js b/test/compress/hoist.js index 2b359fc5..68f5a8c3 100644 --- a/test/compress/hoist.js +++ b/test/compress/hoist.js @@ -65,3 +65,21 @@ hoist_no_destructurings: { } } } + +dont_hoist_var_destructurings: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + function x() { + // If foo is null or undefined, this should be an exception + var {x,y} = foo; + } + } + expect: { + function x() { + var {x,y} = foo; + } + } +} diff --git a/test/parser.js b/test/parser.js index e661907a..85c2e23e 100644 --- a/test/parser.js +++ b/test/parser.js @@ -94,6 +94,14 @@ module.exports = function () { UglifyJS.parse('(function [a] { })'); }); + // Destructuring variable declaration + + var decls = UglifyJS.parse('var {a,b} = foo, { c, d } = bar'); + + ok.equal(decls.body[0].TYPE, 'Var'); + ok.equal(decls.body[0].definitions.length, 2); + ok.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); + ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement // destructuring (right now it's only destructuring *arguments*), From 025d34bfa216eaa09819de58d174b6892bc7af29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 01:16:54 +0100 Subject: [PATCH 020/121] Add holes in destructuring defs, also make them nestable --- lib/parse.js | 30 +++++++++++++++++++++++++----- test/compress/destructuring.js | 6 ++++++ test/parser.js | 11 +++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index a0717c1f..8f57bfca 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -753,7 +753,7 @@ function parse($TEXT, options) { function embed_tokens(parser) { return function() { var start = S.token; - var expr = parser(); + var expr = parser.apply(null, arguments); var end = prev(); expr.start = start; expr.end = end; @@ -1182,17 +1182,18 @@ function parse($TEXT, options) { var a = []; var def; for (;;) { + var sym_type = in_const ? AST_SymbolConst : AST_SymbolVar; if (is("punc", "{") || is("punc", "[")) { def = new AST_VarDef({ start: S.token, - name: destructuring_(), + name: destructuring_(sym_type), value: (expect_token("operator", "="), expression(false, no_in)), end: prev() }); } else { def = new AST_VarDef({ start : S.token, - name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), + name : as_symbol(sym_type), value : is("operator", "=") ? (next(), expression(false, no_in)) : null, end : prev() }) @@ -1205,12 +1206,31 @@ function parse($TEXT, options) { return a; }; - var destructuring_ = embed_tokens(function () { + var destructuring_ = embed_tokens(function (sym_type) { var is_array = is("punc", "["); var closing = is_array ? ']' : '}'; + var sym_type = sym_type || AST_SymbolRef; + + next(); + + var first = true, children = []; + while (!is("punc", closing)) { + if (first) first = false; else expect(","); + if (is("punc", closing)) break; + if (is("punc", ",")) { + children.push(new AST_Hole({ start: S.token, end: S.token })); + } else if (is("punc", "[") || is("punc", "{")) { + children.push(destructuring_(sym_type)); + } else if (is("name")) { + children.push(_make_symbol(sym_type)); + next(); + } else { + children.push(expression()); + } + } next() return new AST_Destructuring({ - names: expr_list(closing), + names: children, is_array: is_array }) }); diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 58a77cd4..b667c9ea 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -17,3 +17,9 @@ destructuring_objects: { } } +nested_destructuring_objects: { + input: { + var [{a},b] = c; + } + expect_exact: 'var[{a},b]=c;'; +} diff --git a/test/parser.js b/test/parser.js index 85c2e23e..125f76c2 100644 --- a/test/parser.js +++ b/test/parser.js @@ -101,6 +101,17 @@ module.exports = function () { ok.equal(decls.body[0].TYPE, 'Var'); ok.equal(decls.body[0].definitions.length, 2); ok.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); + ok.equal(decls.body[0].definitions[0].value.TYPE, 'SymbolRef'); + + var nested_def = UglifyJS.parse('var [{x}] = foo').body[0].definitions[0]; + + ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar') + ok.equal(nested_def.name.names[0].names[0].name, 'x') + + var holey_def = UglifyJS.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; + + ok.equal(holey_def.name.names[0].TYPE, 'Hole') + ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst') ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement From e99bc914ca4159828dc139b0be02f4e8f97e827c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:00:31 +0100 Subject: [PATCH 021/121] Do not mangle a name if it is in a destructuring vardef. --- lib/scope.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 55d7c4e9..068daa60 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -96,7 +96,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var defun = null; var nesting = 0; - var object_destructuring_arg = false; + var in_destructuring = null; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -108,9 +108,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ return true; } if (node instanceof AST_Destructuring && node.is_array === false) { - object_destructuring_arg = true; // These don't nest + in_destructuring = node; // These don't nest descend(); - object_destructuring_arg = false; + in_destructuring = null; return true; } if (node instanceof AST_Scope) { @@ -137,7 +137,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.scope = scope; } if (node instanceof AST_SymbolFunarg) { - node.object_destructuring_arg = object_destructuring_arg; + node.object_destructuring_arg = !!in_destructuring; defun.def_variable(node); } if (node instanceof AST_SymbolLambda) { @@ -155,6 +155,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); def.constant = node instanceof AST_SymbolConst; + def.destructuring = in_destructuring; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -412,7 +413,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ } }); this.walk(tw); - to_mangle.forEach(function(def){ def.mangle(options) }); + to_mangle.forEach(function(def){ + if (def.destructuring && !def.destructuring.is_array) return; + def.mangle(options); + }); if (options.cache) { options.cache.cname = this.cname; From d4f17f29aec6ae9ddcc1b958f3fa4e02af606973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:11:38 +0100 Subject: [PATCH 022/121] Destructuring vardef in for..of and for..in --- lib/parse.js | 2 +- test/compress/destructuring.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 8f57bfca..8ed7163a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1187,7 +1187,7 @@ function parse($TEXT, options) { def = new AST_VarDef({ start: S.token, name: destructuring_(sym_type), - value: (expect_token("operator", "="), expression(false, no_in)), + value: is("operator", "=") ? (expect_token("operator", "="), expression(false, no_in)) : null, end: prev() }); } else { diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index b667c9ea..30fbbee8 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -23,3 +23,12 @@ nested_destructuring_objects: { } expect_exact: 'var[{a},b]=c;'; } + +destructuring_vardef_in_loops: { + input: { + for (var [x,y] in pairs); + for (var [a] = 0;;); + for (var {c} of cees); + } + expect_exact: "for(var[x,y]in pairs);for(var[a]=0;;);for(var{c}of cees);" +} From 079aaa0d4858cb1ddbd8af9e49e50c63d2ced870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:19:53 +0100 Subject: [PATCH 023/121] Tolerate expansions in vardefs, too! --- lib/ast.js | 10 +++------- lib/parse.js | 9 +++++++++ test/parser.js | 14 ++++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 2c73b6d3..7e7503ee 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -363,14 +363,10 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); -// TODO besides parameters and function calls, expansions can go in -// arrays, array destructuring parameters, and array destructuring -// assignment. But I'm not adding this right now because I'm trying -// to do the most minimal and independent changesets. -var AST_Expansion = DEFNODE("AST_Expansion", "symbol", { - $documentation: "An expandible argument, such as ...rest", +var AST_Expansion = DEFNODE("Expansion", "symbol", { + $documentation: "An expandible argument, such as ...rest, a splat, such as [1,2,...all], or an expansion in a variable declaration, such as var [first, ...rest] = list", $propdoc: { - symbol: "AST_SymbolFunarg the name of the argument as a SymbolFunarg" + symbol: "AST_Symbol the thing to be expanded" }, _walk: function(visitor) { var self = this; diff --git a/lib/parse.js b/lib/parse.js index 8ed7163a..fd870bbb 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1221,6 +1221,15 @@ function parse($TEXT, options) { children.push(new AST_Hole({ start: S.token, end: S.token })); } else if (is("punc", "[") || is("punc", "{")) { children.push(destructuring_(sym_type)); + } else if (is("expand", "...")) { + next(); + var symbol = _make_symbol(sym_type); + children.push(new AST_Expansion({ + start: prev(), + symbol: symbol, + end: S.token + })); + next(); } else if (is("name")) { children.push(_make_symbol(sym_type)); next(); diff --git a/test/parser.js b/test/parser.js index 125f76c2..66676b9d 100644 --- a/test/parser.js +++ b/test/parser.js @@ -105,13 +105,19 @@ module.exports = function () { var nested_def = UglifyJS.parse('var [{x}] = foo').body[0].definitions[0]; - ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar') - ok.equal(nested_def.name.names[0].names[0].name, 'x') + ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar'); + ok.equal(nested_def.name.names[0].names[0].name, 'x'); var holey_def = UglifyJS.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; - ok.equal(holey_def.name.names[0].TYPE, 'Hole') - ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst') + ok.equal(holey_def.name.names[0].TYPE, 'Hole'); + ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst'); + + var expanding_def = UglifyJS.parse('var [first, ...rest] = [1,2,3]').body[0].definitions[0]; + + ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); + ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); + ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement From dc5db9b6ca485647cff2e1821de5bf560ec83cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 03:03:38 +0000 Subject: [PATCH 024/121] Starting destructuring expressions --- lib/output.js | 11 +++++++++++ lib/parse.js | 3 --- test/compress/destructuring.js | 9 +++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/output.js b/lib/output.js index e2f7f060..080b3da8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1179,6 +1179,17 @@ function OutputStream(options) { var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); }); + DEFPRINT(AST_ObjectSymbol, function(self, output){ + var def = self.symbol.definition(); + if (def && def.mangled_name) { + output.print(self.symbol.name); + output.print(':'); + output.space(); + output.print(def.mangled_name); + } else { + output.print(self.symbol.name); + } + }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index fd870bbb..3d3eb664 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1410,9 +1410,6 @@ function parse($TEXT, options) { if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name - if (!S.in_parameters) { - croak("Invalid syntax", S.token.line, S.token.col); - } a.push(new AST_ObjectSymbol({ start: start, end: start, diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 30fbbee8..6ea54ac1 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -32,3 +32,12 @@ destructuring_vardef_in_loops: { } expect_exact: "for(var[x,y]in pairs);for(var[a]=0;;);for(var{c}of cees);" } +destructuring_expressions: { + input: { + ({a, b}); + [{a}]; + f({x}); + } + expect_exact: "({a,b});[{a}];f({x});" +} + From 7ee8f3512eef5eb3fe7d2f678d6c110c3fc810f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 8 Aug 2015 16:01:16 +0100 Subject: [PATCH 025/121] play nice with propmangle --- lib/output.js | 5 +++-- lib/propmangle.js | 8 ++++++++ lib/transform.js | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index 080b3da8..505f94b5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1180,14 +1180,15 @@ function OutputStream(options) { output.print_name(def ? def.mangled_name || def.name : self.name); }); DEFPRINT(AST_ObjectSymbol, function(self, output){ + var name = self.mangled_key || self.symbol.name; var def = self.symbol.definition(); if (def && def.mangled_name) { - output.print(self.symbol.name); + output.print(name); output.print(':'); output.space(); output.print(def.mangled_name); } else { - output.print(self.symbol.name); + output.print(name); } }); DEFPRINT(AST_Undefined, function(self, output){ diff --git a/lib/propmangle.js b/lib/propmangle.js index 840bda91..ff782b57 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -90,6 +90,9 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { add(node.key); } + else if (node instanceof AST_ObjectSymbol) { + add(node.symbol.name); + } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above add(node.key.name); @@ -111,6 +114,11 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { node.key = mangle(node.key); } + else if (node instanceof AST_ObjectSymbol) { + if (should_mangle(node.symbol.name)) { + node.mangled_key = mangle(node.symbol.name) + } + } else if (node instanceof AST_ObjectProperty) { // setter or getter node.key.name = mangle(node.key.name); diff --git a/lib/transform.js b/lib/transform.js index 7858759a..266e686f 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -219,6 +219,10 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); + _(AST_ObjectSymbol, function(self, tw){ + self.symbol = self.symbol.transform(tw); + }); + _(AST_ObjectProperty, function(self, tw){ self.value = self.value.transform(tw); }); From adee5023c02ee929b68aa8a1ff3a220d67947831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 10 Aug 2015 21:13:30 +0100 Subject: [PATCH 026/121] What about --mangle-props being on and --mangle being off? --- lib/output.js | 5 +++++ test/parser.js | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/output.js b/lib/output.js index 505f94b5..5d8bef10 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1187,6 +1187,11 @@ function OutputStream(options) { output.print(':'); output.space(); output.print(def.mangled_name); + } else if (!(def && def.mangled_name) && self.mangled_key) { + output.print(name); + output.print(':'); + output.space(); + output.print(def.name); } else { output.print(name); } diff --git a/test/parser.js b/test/parser.js index 66676b9d..a84c2df9 100644 --- a/test/parser.js +++ b/test/parser.js @@ -118,13 +118,6 @@ module.exports = function () { ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); - - ok.throws(function () { - // Note: this *is* a valid destructuring, but before we implement - // destructuring (right now it's only destructuring *arguments*), - // this won't do. - UglifyJS.parse('[{a}]'); - }); } // Run standalone From e1cb1a0e3ca2312fd403d10fee5960691287a436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 22:32:57 +0100 Subject: [PATCH 027/121] Parse and output ES6 template strings. Yikes! --- lib/ast.js | 16 ++++++++++++++++ lib/output.js | 14 ++++++++++++++ lib/parse.js | 40 +++++++++++++++++++++++++++++++++++++++- test/compress/harmony.js | 10 ++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 7e7503ee..13260582 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -501,6 +501,22 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { } }); +var AST_TemplateString = DEFNODE("TemplateString", "segments", { + $documentation: "A template string literal", + $propdoc: { + segments: "[string|AST_Expression]* One or more segments. They can be the parts that are evaluated, or the raw string parts." + }, + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.segments.forEach(function(seg, i){ + if (i % 2 !== 0) { + seg._walk(visitor); + } + }); + }); + } +}); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { diff --git a/lib/output.js b/lib/output.js index 5d8bef10..c6cb254e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -776,6 +776,20 @@ function OutputStream(options) { self._do_print(output); }); + DEFPRINT(AST_TemplateString, function(self, output) { + output.print("`"); + for (var i = 0; i < self.segments.length; i++) { + if (typeof self.segments[i] !== "string") { + output.print("${"); + self.segments[i].print(output); + output.print("}"); + } else { + output.print(self.segments[i]); + } + } + output.print("`"); + }); + AST_Arrow.DEFMETHOD("_do_print", function(output){ var self = this; var parent = output.parent(); diff --git a/lib/parse.js b/lib/parse.js index 3d3eb664..00ba3f55 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -114,7 +114,7 @@ var WHITESPACE_CHARS = makePredicate(characters(" \u00a0\n\r\t\f\u000b\u200b\u18 var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); -var PUNC_CHARS = makePredicate(characters("[]{}(),;:")); +var PUNC_CHARS = makePredicate(characters("[]{}(),;:`")); var REGEXP_MODIFIERS = makePredicate(characters("gmsiy")); @@ -595,6 +595,9 @@ function tokenizer($TEXT, filename, html5_comments) { parse_error("Unexpected character '" + ch + "'"); }; + next_token.next = next; + next_token.peek = peek; + next_token.context = function(nc) { if (nc) S = nc; return S; @@ -805,6 +808,7 @@ function parse($TEXT, options) { }); case "[": case "(": + case "`": return simple_statement(); case ";": next(); @@ -1336,6 +1340,8 @@ function parse($TEXT, options) { return subscripts(array_(), allow_calls); case "{": return subscripts(object_or_object_destructuring_(), allow_calls); + case "`": + return subscripts(template_string(), allow_calls); } unexpected(); } @@ -1352,6 +1358,38 @@ function parse($TEXT, options) { unexpected(); }; + function template_string() { + var tokenizer_S = S.input, start = S.token, segments = [], segment = "", ch; + + while ((ch = tokenizer_S.next()) !== "`") { + if (ch === "$" && tokenizer_S.peek() === "{") { + segments.push(segment); segment = ""; + tokenizer_S.next(); + next(); + segments.push(expression()); + expect("}"); + if (is("punc", "`")) { + break; + } + continue; + } + segment += ch; + if (ch === "\\") { + segment += tokenizer_S.next(); + } + } + + segments.push(segment); + + next(); + + return new AST_TemplateString({ + start: start, + segments: segments, + end: S.token + }); + } + function expr_list(closing, allow_trailing_comma, allow_empty) { var first = true, a = []; while (!is("punc", closing)) { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2fcfd70f..f55c9868 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -46,6 +46,16 @@ typeof_arrow_functions: { expect_exact: "\"function\";" } +template_strings: { + input: { + ``; + `xx\`x`; + `${ foo + 2 }`; + ` foo ${ bar + `baz ${ qux }` }`; + } + expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; +} + destructuring_arguments: { input: { (function ( a ) { }); From 242c61be9485ce3ea1bc0752ff80c38468dc5ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 22:48:17 +0100 Subject: [PATCH 028/121] prefixed template strings, like "String.raw`foo\nbar`". --- lib/ast.js | 12 ++++++++++++ lib/output.js | 4 ++++ lib/parse.js | 7 +++++++ test/compress/harmony.js | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index 13260582..818dde28 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -501,6 +501,18 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { } }); +var AST_PrefixedTemplateString = DEFNODE("PrefixedTemplateString", "template_string prefix", { + $documentation: "A templatestring with a prefix, such as String.raw`foobarbaz`", + $propdoc: { + template_string: "[AST_TemplateString] The template string", + prefix: "[AST_SymbolRef|AST_PropAccess] The prefix, which can be a symbol such as `foo` or a dotted expression such as `String.raw`." + }, + _walk: function(visitor) { + this.prefix._walk(visitor); + this.template_string._walk(visitor); + } +}) + var AST_TemplateString = DEFNODE("TemplateString", "segments", { $documentation: "A template string literal", $propdoc: { diff --git a/lib/output.js b/lib/output.js index c6cb254e..2b40fdfc 100644 --- a/lib/output.js +++ b/lib/output.js @@ -776,6 +776,10 @@ function OutputStream(options) { self._do_print(output); }); + DEFPRINT(AST_PrefixedTemplateString, function(self, output) { + self.prefix.print(output); + self.template_string.print(output); + }); DEFPRINT(AST_TemplateString, function(self, output) { output.print("`"); for (var i = 0; i < self.segments.length; i++) { diff --git a/lib/parse.js b/lib/parse.js index 00ba3f55..759ce2a3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1717,6 +1717,13 @@ function parse($TEXT, options) { }); return arrow_function(expr); } + if ((expr instanceof AST_SymbolRef || expr instanceof AST_PropAccess) && is("punc", "`")) { + return new AST_PrefixedTemplateString({ + start: start, + prefix: expr, + template_string: template_string() + }) + } if (commas && is("punc", ",")) { next(); return new AST_Seq({ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index f55c9868..930bb03e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -56,6 +56,14 @@ template_strings: { expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; } +template_string_prefixes: { + input: { + String.raw`foo`; + foo `bar`; + } + expect_exact: "String.raw`foo`;foo`bar`;"; +} + destructuring_arguments: { input: { (function ( a ) { }); From 2fac2bbfe44c251e13316f43450900a828960d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 23:01:25 +0100 Subject: [PATCH 029/121] Remove unused state variable in_parameters, and also remove unreachable code (try_an_object always returned an object!) --- lib/parse.js | 134 ++++++++++++++------------------------- test/compress/harmony.js | 11 ++++ 2 files changed, 58 insertions(+), 87 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 3d3eb664..35096bc6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -672,7 +672,6 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, - in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -1044,7 +1043,6 @@ function parse($TEXT, options) { expect("("); var first = true; var a = []; - S.in_parameters = true; while (!is("punc", ")")) { if (first) first = false; else expect(","); if (is("expand", "...")) { @@ -1058,7 +1056,6 @@ function parse($TEXT, options) { a.push(expression(false)); } } - S.in_parameters = false; var end = S.token next(); return new AST_ArrowParametersOrSeq({ @@ -1375,97 +1372,60 @@ function parse($TEXT, options) { }); var object_or_object_destructuring_ = embed_tokens(function() { - var start = S.token; + var start = S.token, first = true, a = []; expect("{"); - function try_an_object() { - var first = true, a = []; - while (!is("punc", "}")) { - if (first) first = false; else expect(","); - if (!options.strict && is("punc", "}")) - // allow trailing comma - break; - var start = S.token; - var type = start.type; - var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - } - - if (!is("punc", ":")) { - // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectSymbol({ - start: start, - end: start, - symbol: new AST_SymbolRef({ - start: start, - end: start, - name: name - }) - })); - } else { - if (S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); - } - expect(":"); - a.push(new AST_ObjectKeyVal({ + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!options.strict && is("punc", "}")) + // allow trailing comma + break; + var start = S.token; + var type = start.type; + var name = as_property_name(); + if (type == "name" && !is("punc", ":")) { + if (name == "get") { + a.push(new AST_ObjectGetter({ start : start, - key : name, - value : expression(false), + key : as_atom_node(), + value : function_(AST_Accessor), end : prev() })); + continue; + } + if (name == "set") { + a.push(new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; } } - next(); - return new AST_Object({ properties: a }) + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + a.push(new AST_ObjectSymbol({ + start: start, + end: start, + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); + } } - - var obj = try_an_object(); - if (obj instanceof AST_Object) { return obj; } - - if (!S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); - } - - var firstName = obj; - - var namesInDestructuring = []; - - namesInDestructuring.push( new AST_SymbolRef({ - start : prev(), - end : prev(), - name : firstName - })); - - while (!is("punc", "}")) { - expect(","); - namesInDestructuring.push(as_symbol(AST_SymbolRef)) - } - - expect('}'); - - return new AST_Destructuring({ - start : start, - end : S.token, - names : namesInDestructuring, - is_array : false - }) + next(); + return new AST_Object({ properties: a }) }); function as_property_name() { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2fcfd70f..49b06354 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -122,3 +122,14 @@ number_literals: { 9; } } + +// Fabio: My patches accidentally caused a crash whenever +// there's an extraneous set of parens around an object. +regression_cannot_destructure: { + input: { + var x = ({ x : 3 }); + x(({ x: 3 })); + } + expect_exact: "var x={x:3};x({x:3});"; +} + From dde9e293df99cb417c00a60290598a960f4992cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 6 Sep 2015 21:33:17 +0100 Subject: [PATCH 030/121] parse, output the let statement --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 23 ++++++++++++++++++----- test/compress/block-scope.js | 8 ++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 test/compress/block-scope.js diff --git a/lib/ast.js b/lib/ast.js index 818dde28..3eba3a71 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -682,6 +682,10 @@ var AST_Var = DEFNODE("Var", null, { $documentation: "A `var` statement" }, AST_Definitions); +var AST_Let = DEFNODE("Let", null, { + $documentation: "A `let` statement" +}, AST_Definitions); + var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); diff --git a/lib/output.js b/lib/output.js index 2b40fdfc..1172bd58 100644 --- a/lib/output.js +++ b/lib/output.js @@ -994,6 +994,9 @@ function OutputStream(options) { if (!avoid_semicolon) output.semicolon(); }); + DEFPRINT(AST_Let, function(self, output){ + self._do_print(output, "let"); + }); DEFPRINT(AST_Var, function(self, output){ self._do_print(output, "var"); }); diff --git a/lib/parse.js b/lib/parse.js index 974734b0..05384812 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var void while with'; +var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var let 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 long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -879,6 +879,9 @@ function parse($TEXT, options) { case "var": return tmp = var_(), semicolon(), tmp; + case "let": + return tmp = let_(), semicolon(), tmp; + case "const": return tmp = const_(), semicolon(), tmp; @@ -949,13 +952,15 @@ function parse($TEXT, options) { expect("("); var init = null; if (!is("punc", ";")) { - init = is("keyword", "var") - ? (next(), var_(true)) - : expression(true, true); + init = + is("keyword", "var") ? (next(), var_(true)) : + is("keyword", "let") ? (next(), let_(true)) : + expression(true, true); var is_in = is("operator", "in"); var is_of = is("keyword", "of"); if (is_in || is_of) { - if (init instanceof AST_Var && init.definitions.length > 1) + if ((init instanceof AST_Var || init instanceof AST_Let) && + init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); next(); if (is_in) { @@ -1253,6 +1258,14 @@ function parse($TEXT, options) { }); }; + var let_ = function(no_in) { + return new AST_Let({ + start : prev(), + definitions : vardefs(no_in, false), + end : prev() + }); + }; + var const_ = function() { return new AST_Const({ start : prev(), diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js new file mode 100644 index 00000000..d0cdf75e --- /dev/null +++ b/test/compress/block-scope.js @@ -0,0 +1,8 @@ + +let_statement: { + input: { + let x = 6; + } + expect_exact: "let x=6;" +} + From 3d7f73114d52acbba9f482bdd289986253ba2ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 6 Sep 2015 21:56:55 +0100 Subject: [PATCH 031/121] Add a test to make sure future generations don't hoist lets --- test/compress/block-scope.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index d0cdf75e..d1953ce5 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -6,3 +6,28 @@ let_statement: { expect_exact: "let x=6;" } +do_not_hoist_let: { + options = { + hoist_vars: true, + }; + input: { + function x() { + if (FOO) { + let let1; + let let2; + var var1; + var var2; + } + } + } + expect: { + function x() { + var var1, var2; + if (FOO) { + let let1; + let let2; + } + } + } +} + From b31918bbf0e58d89cd1d4321d50c46a6ed5464f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 7 Sep 2015 22:46:07 +0100 Subject: [PATCH 032/121] computed properties --- lib/ast.js | 10 ++++++++++ lib/compress.js | 3 +++ lib/output.js | 7 +++++++ lib/parse.js | 15 +++++++++++++++ test/compress/harmony.js | 7 +++++++ 5 files changed, 42 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index 07126e7c..d9afcff4 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -921,6 +921,16 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { } }, AST_ObjectProperty); +var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { + $documentation: "An object property whose key is computed. Like `[Symbol.iterator]: function...` or `[routes.homepage]: renderHomepage`", + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.key._walk(visitor); + this.value._walk(visitor); + }); + } +}, AST_ObjectProperty); + var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { $propdoc: { symbol: "[AST_SymbolRef] what symbol it is" diff --git a/lib/compress.js b/lib/compress.js index 7dd43e69..c990712e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -952,6 +952,9 @@ merge(Compressor.prototype, { return false; }); def(AST_ObjectProperty, function(compressor){ + if (this instanceof AST_ObjectComputedKeyVal && + this.key.has_side_effects(compressor)) + return true; return this.value.has_side_effects(compressor); }); def(AST_Array, function(compressor){ diff --git a/lib/output.js b/lib/output.js index 03cda230..5aeed667 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1203,6 +1203,13 @@ function OutputStream(options) { self.key.print(output); self.value._do_print(output, true); }); + DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { + output.print("["); + self.key.print(output); + output.print("]:"); + output.space(); + self.value.print(output); + }); DEFPRINT(AST_Symbol, function(self, output){ var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); diff --git a/lib/parse.js b/lib/parse.js index 5ae2a1db..c0065217 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1465,6 +1465,15 @@ function parse($TEXT, options) { continue; } } + + if (type == "punc" && start.value == "[") { + expect(":"); + a.push(new AST_ObjectComputedKeyVal({ + key: name, + value: expression(false) + })); + continue; + } if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name @@ -1495,6 +1504,12 @@ function parse($TEXT, options) { var tmp = S.token; next(); switch (tmp.type) { + case "punc": + if (tmp.value === "[") { + var ex = expression(false); + expect("]"); + return ex; + } else unexpected(); case "num": case "string": case "name": diff --git a/test/compress/harmony.js b/test/compress/harmony.js index ab0c73be..b763318e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,13 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +computed_property_names: { + input: { + obj({ ["x" + "x"]: 6 }); + } + expect_exact: "obj({[\"x\"+\"x\"]:6});" +} + typeof_arrow_functions: { options = { evaluate: true From 76ed083e47f9b4c4a7c876f38c333cbff3e1f981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 12 Oct 2015 21:39:19 +0100 Subject: [PATCH 033/121] Using single quotes --- test/compress/harmony.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index b763318e..86c2d33a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -40,7 +40,7 @@ computed_property_names: { input: { obj({ ["x" + "x"]: 6 }); } - expect_exact: "obj({[\"x\"+\"x\"]:6});" + expect_exact: 'obj({["x"+"x"]:6});' } typeof_arrow_functions: { From 2cce61c564936f17c1ae174f3f2920e6a0e66ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 20:56:59 +0000 Subject: [PATCH 034/121] Allow 'of' to be a name. --- lib/parse.js | 4 ++-- test/compress/harmony.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index c0065217..95e44914 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var let 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 long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -969,7 +969,7 @@ function parse($TEXT, options) { is("keyword", "let") ? (next(), let_(true)) : expression(true, true); var is_in = is("operator", "in"); - var is_of = is("keyword", "of"); + var is_of = is("name", "of"); if (is_in || is_of) { if ((init instanceof AST_Var || init instanceof AST_Let) && init.definitions.length > 1) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 86c2d33a..54be70d4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -158,3 +158,20 @@ regression_cannot_destructure: { expect_exact: "var x={x:3};x({x:3});"; } +regression_cannot_use_of: { + input: { + function of() { + } + var of = "is a valid variable name"; + of = { of: "is ok" }; + x.of; + of: foo() + } + expect: { + function of(){} + var of="is a valid variable name"; + of={of:"is ok"}; + x.of; + foo(); /* Label statement missing? No prob. */ + } +} From 0d8dea9538c2305c3bacd507439b9f06abed2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 11 Oct 2015 18:22:07 +0100 Subject: [PATCH 035/121] start concise methods --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 10 ++++++++++ test/compress/harmony.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index d9afcff4..32d8486f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,6 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); +var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { + $documentation: "An ES6 concise method inside an object or class" +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); diff --git a/lib/output.js b/lib/output.js index f8929020..bdab47c6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -832,6 +832,9 @@ function OutputStream(options) { } if (needs_parens) { output.print(")") } }); + DEFPRINT(AST_ConciseMethod, function(self, output){ + self._do_print(output, true /* do not print "function" */); + }); /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ diff --git a/lib/parse.js b/lib/parse.js index 95e44914..9fd4dcdf 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1446,6 +1446,16 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type == "name" && !is("punc", ":")) { + if (is("punc", "(")) { + a.push(new AST_ConciseMethod({ + start : start, + name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + })) + continue; + } if (name == "get") { a.push(new AST_ObjectGetter({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 54be70d4..718a547a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,22 @@ destructuring_arguments: { } } +concise_methods: { + input: { + x = { + foo(a, b) { + return x; + } + } + y = { + foo([{a}]) { + return a; + } + } + } + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" +} + number_literals: { input: { 0b1001; From da8c428a077df6c579f26de950dc9f1028f22bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 23 Oct 2015 18:59:07 +0100 Subject: [PATCH 036/121] Just making sure that concise methods are separated by commas. When classes come, they won't be necessary. --- test/compress/harmony.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 718a547a..19628be4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -142,10 +142,11 @@ concise_methods: { y = { foo([{a}]) { return a; - } + }, + bar(){} } } - expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } number_literals: { From 34213ea2f8c80dde85d31dffb2ef173dd6b7bc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:14:55 +0000 Subject: [PATCH 037/121] Create a new symbol for methods' names --- lib/ast.js | 4 ++++ lib/parse.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 32d8486f..8dadf21c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -991,6 +991,10 @@ var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { $documentation: "Symbol defining a function", }, AST_SymbolDeclaration); +var AST_SymbolMethod = DEFNODE("SymbolMethod", null, { + $documentation: "Symbol in an object defining a method", +}, AST_Symbol); + var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index 9fd4dcdf..b44cc468 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1449,7 +1449,7 @@ function parse($TEXT, options) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, - name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), end : prev() From c99eaae360c8baab973a7c9d7e14dbbb2ade8529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:15:21 +0000 Subject: [PATCH 038/121] Make concise methods work with propmangle --- lib/propmangle.js | 8 ++++++++ test/compress/harmony.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/propmangle.js b/lib/propmangle.js index ff782b57..86da5de9 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -107,6 +107,9 @@ function mangle_properties(ast, options) { addStrings(node.property); } } + else if (node instanceof AST_ConciseMethod) { + add(node.name.name); + } })); // step 2: transform the tree, renaming properties @@ -129,6 +132,11 @@ function mangle_properties(ast, options) { else if (node instanceof AST_Sub) { node.property = mangleStrings(node.property); } + else if (node instanceof AST_ConciseMethod) { + if (should_mangle(node.name.name)) { + node.name.name = mangle(node.name.name); + } + } // else if (node instanceof AST_String) { // if (should_mangle(node.value)) { // AST_Node.warn( diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 19628be4..2525d252 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -149,6 +149,26 @@ concise_methods: { expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } +concise_methods_and_mangle_props: { + mangle_props = { + regex: /_/ + }; + input: { + function x() { + obj = { + _foo() { return 1; } + } + } + } + expect: { + function x() { + obj = { + a() { return 1; } + } + } + } +} + number_literals: { input: { 0b1001; From 64e7a00399a1bcdc9679f97db885e0bd00ed957f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 23:24:04 +0000 Subject: [PATCH 039/121] Accept keyword names as concise method names --- lib/parse.js | 2 +- test/compress/harmony.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index b44cc468..4ed32863 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1445,7 +1445,7 @@ function parse($TEXT, options) { var start = S.token; var type = start.type; var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { + if (type != "string" && type != "num" && !is("punc", ":")) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2525d252..731976f3 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -169,6 +169,18 @@ concise_methods_and_mangle_props: { } } +concise_methods_and_keyword_names: { + input: { + x = { + catch() {}, + throw() {} + } + } + expect: { + x={catch(){},throw(){}}; + } +} + number_literals: { input: { 0b1001; From 5f7cb6939c1d9006f208ffe804fef64aade0ee23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:40:46 +0000 Subject: [PATCH 040/121] Starting ES6 classes --- lib/ast.js | 12 +++++ lib/output.js | 31 ++++++++++++ lib/parse.js | 106 ++++++++++++++++++++++++++++----------- test/compress/harmony.js | 19 +++++++ 4 files changed, 140 insertions(+), 28 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 8dadf21c..a80ea76c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -955,6 +955,14 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { $documentation: "An object getter property", }, AST_ObjectProperty); +var AST_Class = DEFNODE("Class", "name extends", { + $propdoc: { + name: "[AST_SymbolClassName?] optional class name.", + extends: "[AST_Node]? optional parent class", + }, + $documentation: "An ES6 class", +}, AST_Object); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -999,6 +1007,10 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); +var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { + $documentation: "Symbol naming a class's name. Lexically scoped to the class." +}, AST_SymbolDeclaration); + var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); diff --git a/lib/output.js b/lib/output.js index bdab47c6..e597184f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -502,6 +502,12 @@ function OutputStream(options) { return first_in_statement(output); }); + // Not a class, though. It can be alone in a statement although + // it extends from AST_Object. + PARENS(AST_Class, function() { + return false; + }); + PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; @@ -1188,6 +1194,31 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_Class, function(self, output){ + output.print("class"); + output.space(); + if (self.name) { + self.name.print(output); + output.space(); + } + if (self.extends) { + output.print("extends"); + output.space(); + self.extends.print(output); + output.space(); + } + if (self.properties.length > 0) output.with_block(function(){ + self.properties.forEach(function(prop, i){ + if (i) { + output.newline(); + } + output.indent(); + prop.print(output); + }); + output.newline(); + }); + else output.print("{}"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index 4ed32863..9af773d5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let 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 long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char double enum export 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; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; @@ -855,6 +855,9 @@ function parse($TEXT, options) { case "for": return for_(); + case "class": + return class_(); + case "function": return function_(AST_Defun); @@ -1374,6 +1377,13 @@ function parse($TEXT, options) { func.end = prev(); return subscripts(func, allow_calls); } + if (is("keyword", "class")) { + next(); + var cls = class_(); + cls.start = start; + cls.end = prev(); + return subscripts(cls, allow_calls); + } if (ATOMIC_START_TOKEN[S.token.type]) { return subscripts(as_atom_node(), allow_calls); } @@ -1446,32 +1456,9 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type != "string" && type != "num" && !is("punc", ":")) { - if (is("punc", "(")) { - a.push(new AST_ConciseMethod({ - start : start, - name : new AST_SymbolMethod({ name: name }), - argnames : params_or_seq_().as_params(croak), - body : _function_body(true), - end : prev() - })) - continue; - } - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); + var concise = concise_method_or_getset(name, start); + if (concise) { + a.push(concise); continue; } } @@ -1510,6 +1497,69 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); + function class_() { + var start, method, class_name, name, extends_, a = []; + + if (S.token.type == "name" && S.token.value != "extends") { + class_name = as_symbol(AST_SymbolClassName) + } + + if (S.token.value == "extends") { + next(); + extends_ = expression(true); + } + + expect("{"); + + if (is("punc", ";")) { next(); } // Leading semicolons are okay in class bodies. + while (!is("punc", "}")) { + start = S.token; + name = as_property_name(); + method = concise_method_or_getset(name, start); + if (!method) { croak(); } + a.push(method); + if (is("punc", ";")) { next(); } + } + + next(); + + return new AST_Class({ + start: start, + name: class_name, + extends: extends_, + properties: a, + end: prev(), + }); + } + + function concise_method_or_getset(name, start) { + if (is("punc", "(")) { + return new AST_ConciseMethod({ + start : start, + name : new AST_SymbolMethod({ name: name }), + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + }); + } + if (name == "get") { + return new AST_ObjectGetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + if (name == "set") { + return new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 731976f3..95e9bd54 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -181,6 +181,25 @@ concise_methods_and_keyword_names: { } } +classes: { + input: { + class SomeClass { + constructor() { + }; + foo() {}; + }; + class NoSemi { + constructor(...args) { + } + foo() {} + }; + class ChildClass extends SomeClass {}; + var asExpression = class AsExpression {}; + var nameless = class {}; + } + expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" +} + number_literals: { input: { 0b1001; From 9ffed2bea6ff4cf2e487781adaee2ee7944e2d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:51:47 +0000 Subject: [PATCH 041/121] static properties --- lib/ast.js | 15 ++++++++++++--- lib/output.js | 16 +++++++++++++++- lib/parse.js | 9 +++++++++ test/compress/harmony.js | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index a80ea76c..9e2f3cee 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,7 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); -var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { +var AST_ConciseMethod = DEFNODE("ConciseMethod", "static", { + $propdoc: { + static: "[boolean] whether this method is static (classes only)", + }, $documentation: "An ES6 concise method inside an object or class" }, AST_Lambda); @@ -947,11 +950,17 @@ var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { } }, AST_ObjectProperty); -var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { +var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static setter (classes only)" + }, $documentation: "An object setter property", }, AST_ObjectProperty); -var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { +var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static getter (classes only)" + }, $documentation: "An object getter property", }, AST_ObjectProperty); diff --git a/lib/output.js b/lib/output.js index e597184f..e744788f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -775,9 +775,11 @@ function OutputStream(options) { var self = this; if (!nokeyword) { output.print("function"); + if (self.name) { + output.space(); + } } if (self.name) { - output.space(); self.name.print(output); } output.with_parens(function(){ @@ -839,6 +841,10 @@ function OutputStream(options) { if (needs_parens) { output.print(")") } }); DEFPRINT(AST_ConciseMethod, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } self._do_print(output, true /* do not print "function" */); }); @@ -1238,12 +1244,20 @@ function OutputStream(options) { self.value.print(output); }); DEFPRINT(AST_ObjectSetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("set"); output.space(); self.key.print(output); self.value._do_print(output, true); }); DEFPRINT(AST_ObjectGetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("get"); output.space(); self.key.print(output); diff --git a/lib/parse.js b/lib/parse.js index 9af773d5..5aee6017 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1533,9 +1533,16 @@ function parse($TEXT, options) { } function concise_method_or_getset(name, start) { + var is_static = false; + if (name === "static" && !is("punc", "(")) { + is_static = true; + name = S.token.value; + next(); + } if (is("punc", "(")) { return new AST_ConciseMethod({ start : start, + static : is_static, name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), @@ -1545,6 +1552,7 @@ function parse($TEXT, options) { if (name == "get") { return new AST_ObjectGetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() @@ -1553,6 +1561,7 @@ function parse($TEXT, options) { if (name == "set") { return new AST_ObjectSetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 95e9bd54..e1b3078a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -200,6 +200,20 @@ classes: { expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" } +class_statics: { + input: { + x = class { + static staticMethod() {} + static get foo() {} + static set bar() {} + static() { /* "static" can be a method name! */ } + get() { /* "get" can be a method name! */ } + set() { /* "set" can be a method name! */ } + } + } + expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" +} + number_literals: { input: { 0b1001; From 364d20f8fb8291525057451a491fe2924bd1f072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 20 Nov 2015 18:09:27 +0000 Subject: [PATCH 042/121] Add mangle = { ...mangleopts } option to tests. --- test/run-tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/run-tests.js b/test/run-tests.js index 1d0de8ca..6d7f7244 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -104,6 +104,9 @@ function run_compress_tests() { } var output = input.transform(cmp); output.figure_out_scope(); + if (test.mangle) { + output.mangle_names(test.mangle); + } output = make_code(output, false); if (expect != output) { log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", { From bb6b3a773af0e2682689c738744226cf57d2340c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 20 Nov 2015 19:34:10 +0000 Subject: [PATCH 043/121] Make AST_Class inherit AST_Scope instead of AST_Object This is one of those days I'd love to use multiple inheritance. An AST_Class has lots of common with AST_Object, but unfortunately `instanceof AST_Scope` is used very, very much, and a class has its name inside its own special pocket scope. This compels me to make AST_Class inherit Scope instead. It looks like, although there is much in common with AST_Object, `instanceof AST_Object` seldom are made, perhaps because it is less often necessary to traverse an object than a scope. --- lib/ast.js | 18 ++++++++++++++++-- lib/output.js | 6 ------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 9e2f3cee..b78a85b2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -964,13 +964,27 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { $documentation: "An object getter property", }, AST_ObjectProperty); -var AST_Class = DEFNODE("Class", "name extends", { +var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { name: "[AST_SymbolClassName?] optional class name.", extends: "[AST_Node]? optional parent class", + properties: "[AST_ObjectProperty*] array of properties" }, $documentation: "An ES6 class", -}, AST_Object); + _walk: function(visitor) { + return visitor._visit(this, function(){ + if (this.name) { + this.name._walk(visitor); + } + if (this.extends) { + this.extends._walk(visitor); + } + this.properties.forEach(function(prop){ + prop._walk(visitor); + }); + }); + }, +}, AST_Scope); var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { diff --git a/lib/output.js b/lib/output.js index e744788f..d1ebdeee 100644 --- a/lib/output.js +++ b/lib/output.js @@ -502,12 +502,6 @@ function OutputStream(options) { return first_in_statement(output); }); - // Not a class, though. It can be alone in a statement although - // it extends from AST_Object. - PARENS(AST_Class, function() { - return false; - }); - PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; From 69da8e53e022911b1f1b4b3785e694ce60c11c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 09:17:32 +0000 Subject: [PATCH 044/121] Separate class expressions from class declarations and their symbols like defuns --- lib/ast.js | 16 ++++++++++++++-- lib/parse.js | 14 +++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index b78a85b2..faae55fd 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -966,7 +966,7 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { - name: "[AST_SymbolClassName?] optional class name.", + name: "[AST_SymbolClass|AST_SymbolDefClass?] optional class name.", extends: "[AST_Node]? optional parent class", properties: "[AST_ObjectProperty*] array of properties" }, @@ -986,6 +986,14 @@ var AST_Class = DEFNODE("Class", "name extends properties", { }, }, AST_Scope); +var AST_DefClass = DEFNODE("DefClass", null, { + $documentation: "A class definition", +}, AST_Class); + +var AST_ClassExpression = DEFNODE("ClassExpression", null, { + $documentation: "A class expression." +}, AST_Class); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -1030,7 +1038,11 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); -var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { +var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, { + $documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class." +}, AST_SymbolDeclaration); + +var AST_SymbolClass = DEFNODE("SymbolClass", null, { $documentation: "Symbol naming a class's name. Lexically scoped to the class." }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index 5aee6017..9341ad39 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -856,7 +856,7 @@ function parse($TEXT, options) { return for_(); case "class": - return class_(); + return class_(AST_DefClass); case "function": return function_(AST_Defun); @@ -1379,7 +1379,7 @@ function parse($TEXT, options) { } if (is("keyword", "class")) { next(); - var cls = class_(); + var cls = class_(AST_ClassExpression); cls.start = start; cls.end = prev(); return subscripts(cls, allow_calls); @@ -1497,11 +1497,15 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); - function class_() { + function class_(KindOfClass) { var start, method, class_name, name, extends_, a = []; if (S.token.type == "name" && S.token.value != "extends") { - class_name = as_symbol(AST_SymbolClassName) + class_name = as_symbol(KindOfClass === AST_DefClass ? AST_SymbolDefClass : AST_SymbolClass); + } + + if (KindOfClass === AST_DefClass && !class_name) { + croak(); } if (S.token.value == "extends") { @@ -1523,7 +1527,7 @@ function parse($TEXT, options) { next(); - return new AST_Class({ + return new KindOfClass({ start: start, name: class_name, extends: extends_, From 425613b0d2ab8dfa5c3f4cff5d9e1c5a26bbbca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 12:20:20 +0000 Subject: [PATCH 045/121] mangle class names --- lib/scope.js | 16 ++++++++++++++++ test/compress/harmony.js | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/scope.js b/lib/scope.js index caaba157..4abebddb 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -155,6 +155,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // later. (node.scope = defun.parent_scope).def_function(node); } + else if (node instanceof AST_SymbolClass) { + defun.def_variable(node); + } + else if (node instanceof AST_SymbolDefClass) { + // This deals with the name of the class being available + // inside the class. + (node.scope = defun.parent_scope).def_function(node); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); @@ -171,6 +179,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // pass 2: find back references and eval var func = null; + var cls = null; var globals = self.globals = new Dictionary(); var tw = new TreeWalker(function(node, descend){ if (node instanceof AST_Lambda) { @@ -180,6 +189,13 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ func = prev_func; return true; } + if (node instanceof AST_Class) { + var prev_cls = cls; + cls = node; + descend(); + cls = prev_cls; + return true; + } if (node instanceof AST_SymbolRef) { var name = node.name; var sym = node.scope.find_variable(name); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e1b3078a..c959b5d1 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -214,6 +214,25 @@ class_statics: { expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" } +class_name_can_be_mangled: { + mangle = { }; + input: { + function x() { + class Foo { + } + var class1 = Foo + var class2 = class Bar {} + } + } + expect: { + function x() { + class a { } + var b = a + var c = class a {} + } + } +} + number_literals: { input: { 0b1001; From e076abdbf24bc6acdfe34a11b607613016692e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 13:59:18 +0000 Subject: [PATCH 046/121] Mangle class names correctly --- lib/scope.js | 9 +++++++-- test/compress/harmony.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 4abebddb..3ae51dd4 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -66,7 +66,11 @@ SymbolDef.prototype = { || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) || (options.keep_fnames && (this.orig[0] instanceof AST_SymbolLambda - || this.orig[0] instanceof AST_SymbolDefun)); + || this.orig[0] instanceof AST_SymbolDefun)) + || this.orig[0] instanceof AST_SymbolMethod + || (options.keep_classnames + && (this.orig[0] instanceof AST_SymbolClass + || this.orig[0] instanceof AST_SymbolDefClass)); }, mangle: function(options) { var cache = options.cache && options.cache.props; @@ -379,7 +383,8 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ sort : false, toplevel : false, screw_ie8 : false, - keep_fnames : false + keep_fnames : false, + keep_classnames : false }); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c959b5d1..1d18301d 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -233,6 +233,24 @@ class_name_can_be_mangled: { } } +class_name_can_be_preserved: { + mangle = { + keep_classnames: true + } + input: { + function x() { + (class Baz { }); + class Foo {}; + } + } + expect: { + function x() { + (class Baz { }); + class Foo {}; + } + } +} + number_literals: { input: { 0b1001; From a800356ad089c81a4bc25a90d58bc0071ebe7a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 14:48:23 +0000 Subject: [PATCH 047/121] Implement new.target --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 8 ++++++++ test/compress/harmony.js | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index faae55fd..c315c4a1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1003,6 +1003,10 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $documentation: "Base class for all symbols", }); +var AST_NewTarget = DEFNODE("NewTarget", null, { + $documentation: "A reference to new.target" +}); + var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); diff --git a/lib/output.js b/lib/output.js index d1ebdeee..74d88d25 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1219,6 +1219,9 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_NewTarget, function(self, output) { + output.print("new.target"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index 9341ad39..8f03435f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1292,6 +1292,14 @@ function parse($TEXT, options) { var new_ = function(allow_calls) { var start = S.token; expect_token("operator", "new"); + if (is("punc", ".")) { + next(); + expect_token("name"); + return subscripts(new AST_NewTarget({ + start : start, + end : prev() + }), allow_calls); + } var newexp = expr_atom(false), args; if (is("punc", "(")) { next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 1d18301d..e53f9458 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -251,6 +251,14 @@ class_name_can_be_preserved: { } } +new_target: { + input: { + new.target; + new.target.name; + } + expect_exact: "new.target;new.target.name;" +} + number_literals: { input: { 0b1001; From f07ab4666fdc2358e342c2037f7ad94f97a205d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 048/121] Non-destructuring default parameters --- lib/ast.js | 17 ++++++++++++++++- lib/output.js | 18 +++++++++++++++--- test/compress/harmony.js | 8 ++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 8dadf21c..281912f7 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,6 +422,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { is_array: true, names: ex.elements.map(to_fun_args) }); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -983,8 +990,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index b0dc64ce..f1dc1ad1 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1225,9 +1225,21 @@ function OutputStream(options) { output.space(); self.value.print(output); }); - DEFPRINT(AST_Symbol, function(self, output){ - var def = self.definition(); - output.print_name(def ? def.mangled_name || def.name : self.name); + AST_Symbol.DEFMETHOD("_do_print", function(output){ + var def = this.definition(); + output.print_name(def ? def.mangled_name || def.name : this.name); + }); + DEFPRINT(AST_Symbol, function (self, output) { + self._do_print(output); + }); + DEFPRINT(AST_SymbolFunarg, function(self, output){ + self._do_print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }); DEFPRINT(AST_ObjectSymbol, function(self, output){ var name = self.mangled_key || self.symbol.name; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 731976f3..90836e5e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,14 @@ destructuring_arguments: { } } +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}" +} + concise_methods: { input: { x = { From cbcb3ac44e4fe5a56135aae358c3da6e849502d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 049/121] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 16 +++++++--------- lib/output.js | 6 ++++++ test/compress/harmony.js | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 281912f7..e58dcba2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -384,7 +384,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring var root = this; - return this.expressions.map(function to_fun_args(ex) { + return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { if (ex.properties.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -392,6 +392,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: false, + default: default_seen_above, names: ex.properties.map(to_fun_args) }); } else if (ex instanceof AST_ObjectSymbol) { @@ -408,6 +409,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, + default: default_seen_above, start: ex.start, end: ex.end }); @@ -420,15 +422,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: true, + default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -489,7 +487,7 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index f1dc1ad1..b2f34720 100644 --- a/lib/output.js +++ b/lib/output.js @@ -631,6 +631,12 @@ function OutputStream(options) { name.print(output); }) output.print(self.is_array ? "]" : "}"); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }) DEFPRINT(AST_Debugger, function(self, output){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 90836e5e..986dcd18 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -136,8 +136,9 @@ default_arguments: { input: { function x(a = 6) { } function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } } - expect_exact: "function x(a=6){}function x(a=6+5){}" + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } concise_methods: { From 59e999597ee84c889e78fbd9c29ae81c0dc21bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 050/121] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 20 +++++++++----------- lib/output.js | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index e58dcba2..e0f5fa75 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -973,10 +973,16 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." + init: "[AST_Node*/S] array of initializers for this declaration.", + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); } }, AST_Symbol); @@ -988,16 +994,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index b2f34720..a4b02725 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1238,7 +1238,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 8220dbbea0d2438931d17b1c8581f644606ad122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:04:42 +0000 Subject: [PATCH 051/121] Default values inside destructurings --- lib/parse.js | 24 +++++++++++++++++++++--- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 4ed32863..237977ee 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1249,8 +1249,12 @@ function parse($TEXT, options) { })); next(); } else if (is("name")) { - children.push(_make_symbol(sym_type)); - next(); + children.push(new (sym_type)({ + name : String(S.token.value), + start : S.token, + default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, + end : S.token + })); } else { children.push(expression()); } @@ -1485,7 +1489,21 @@ function parse($TEXT, options) { continue; } - if (!is("punc", ":")) { + if (is("operator", "=")) { + next(); + a.push(new AST_Assign({ + start: start, + // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. + left: new AST_SymbolRef({ + start: start, + end: start, + name: name + }), + operator: "=", + right: expression(false), + end: prev() + })); + } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectSymbol({ start: start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 986dcd18..a3910ac6 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -141,6 +141,16 @@ default_arguments: { expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + concise_methods: { input: { x = { From 084437bc6dc478c54c2bedad137489b59f78951d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 052/121] Non-destructuring default parameters --- lib/ast.js | 17 ++++++++++++++++- lib/output.js | 18 +++++++++++++++--- test/compress/harmony.js | 8 ++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0541dc89..343dd42d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,6 +422,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { is_array: true, names: ex.elements.map(to_fun_args) }); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -1026,8 +1033,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 80f66e0e..4451d2e5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1275,9 +1275,21 @@ function OutputStream(options) { output.space(); self.value.print(output); }); - DEFPRINT(AST_Symbol, function(self, output){ - var def = self.definition(); - output.print_name(def ? def.mangled_name || def.name : self.name); + AST_Symbol.DEFMETHOD("_do_print", function(output){ + var def = this.definition(); + output.print_name(def ? def.mangled_name || def.name : this.name); + }); + DEFPRINT(AST_Symbol, function (self, output) { + self._do_print(output); + }); + DEFPRINT(AST_SymbolFunarg, function(self, output){ + self._do_print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }); DEFPRINT(AST_ObjectSymbol, function(self, output){ var name = self.mangled_key || self.symbol.name; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e53f9458..a1f2aa9c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,14 @@ destructuring_arguments: { } } +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}" +} + concise_methods: { input: { x = { From 5b553aafe29ccaebe35a6e3af65e190bb77cb856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 053/121] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 16 +++++++--------- lib/output.js | 6 ++++++ test/compress/harmony.js | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 343dd42d..78702e14 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -384,7 +384,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring var root = this; - return this.expressions.map(function to_fun_args(ex) { + return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { if (ex.properties.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -392,6 +392,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: false, + default: default_seen_above, names: ex.properties.map(to_fun_args) }); } else if (ex instanceof AST_ObjectSymbol) { @@ -408,6 +409,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, + default: default_seen_above, start: ex.start, end: ex.end }); @@ -420,15 +422,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: true, + default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -492,7 +490,7 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index 4451d2e5..9ce64c40 100644 --- a/lib/output.js +++ b/lib/output.js @@ -639,6 +639,12 @@ function OutputStream(options) { name.print(output); }) output.print(self.is_array ? "]" : "}"); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }) DEFPRINT(AST_Debugger, function(self, output){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index a1f2aa9c..69cf4caa 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -136,8 +136,9 @@ default_arguments: { input: { function x(a = 6) { } function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } } - expect_exact: "function x(a=6){}function x(a=6+5){}" + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } concise_methods: { From 7a8cffd631b6b76be3475c17e4a42889108ee114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 054/121] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 20 +++++++++----------- lib/output.js | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 78702e14..358862b1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1016,10 +1016,16 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." + init: "[AST_Node*/S] array of initializers for this declaration.", + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); } }, AST_Symbol); @@ -1031,16 +1037,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 9ce64c40..c0b86b96 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1288,7 +1288,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 028ff64e9aade190310aefd43fc6010113c42a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:04:42 +0000 Subject: [PATCH 055/121] Default values inside destructurings --- lib/parse.js | 24 +++++++++++++++++++++--- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 1a7bca94..8c8d19dd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1255,8 +1255,12 @@ function parse($TEXT, options) { })); next(); } else if (is("name")) { - children.push(_make_symbol(sym_type)); - next(); + children.push(new (sym_type)({ + name : String(S.token.value), + start : S.token, + default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, + end : S.token + })); } else { children.push(expression()); } @@ -1483,7 +1487,21 @@ function parse($TEXT, options) { continue; } - if (!is("punc", ":")) { + if (is("operator", "=")) { + next(); + a.push(new AST_Assign({ + start: start, + // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. + left: new AST_SymbolRef({ + start: start, + end: start, + name: name + }), + operator: "=", + right: expression(false), + end: prev() + })); + } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectSymbol({ start: start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 69cf4caa..c3f48b42 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -141,6 +141,16 @@ default_arguments: { expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + concise_methods: { input: { x = { From 1f7523206217a0d552bd89d2d418e7b4a08e92a7 Mon Sep 17 00:00:00 2001 From: Fugiman Date: Mon, 18 Jan 2016 21:28:26 -0800 Subject: [PATCH 056/121] Fix template string parsing --- lib/parse.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 8c8d19dd..b90f6de4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1414,10 +1414,8 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - expect("}"); - if (is("punc", "`")) { - break; - } + if (!is("punc", "}")) + token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); continue; } segment += ch; From 5e78f20f1c6ea2ff340d8e7a4f022a576d8c297e Mon Sep 17 00:00:00 2001 From: Fugiman Date: Tue, 19 Jan 2016 02:04:17 -0800 Subject: [PATCH 057/121] Remove duplicate error message --- lib/parse.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index b90f6de4..072e7fb4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1414,8 +1414,10 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - if (!is("punc", "}")) - token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); + if (!is("punc", "}")) { + // force error message + expect("}"); + } continue; } segment += ch; From c80ec625ec69656a57020d9007d7f53609502fe4 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 19 Jan 2016 19:21:53 +0100 Subject: [PATCH 058/121] Add test for bad template string parsing --- test/compress/issue-926.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/compress/issue-926.js diff --git a/test/compress/issue-926.js b/test/compress/issue-926.js new file mode 100644 index 00000000..e717efb0 --- /dev/null +++ b/test/compress/issue-926.js @@ -0,0 +1,9 @@ +template_strings: { + input: { + foo( + `${contents}`, + `${text}` + ); + } + expect_exact: "foo(`${contents}`,`${text}`);" +} From d7ec2ecc12ca24e5db94ec0fe634e6a41ff5dfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 9 Feb 2016 00:02:23 +0000 Subject: [PATCH 059/121] Fix #931: Create arrow functions in maybe_assign so that they can be used in assignments --- lib/parse.js | 21 ++++++++++++++++++++- test/compress/harmony.js | 11 +++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 072e7fb4..0a6072af 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1032,7 +1032,12 @@ function parse($TEXT, options) { var arrow_function = function(args) { expect_token("arrow", "=>"); - var argnames = args.as_params(croak); + var argnames; + if (typeof args.length === 'number') { + argnames = args; + } else { + argnames = args.as_params(croak); + } var body = is("punc", "{") ? _function_body(true) : @@ -1789,6 +1794,12 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + if (start.value == "(" && peek().value == ")") { + next(); + next(); + return arrow_function([]); + } + var left = maybe_conditional(no_in); var val = S.token.value; @@ -1805,6 +1816,14 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } + if (is("arrow")) { + left = new AST_SymbolFunarg({ + name: left.name, + start: left.start, + end: left.end, + }); + return arrow_function([left]) + } return left; }; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c3f48b42..622e00a0 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,17 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +regression_assign_arrow_functions: { + input: { + oninstall = e => false; + oninstall = () => false; + } + expect: { + oninstall=e=>false; + oninstall=()=>false; + } +} + computed_property_names: { input: { obj({ ["x" + "x"]: 6 }); From 0b303379c0cdc33a8c14c97ab29148d981b4887e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Thu, 4 Feb 2016 21:54:21 +0000 Subject: [PATCH 060/121] fix: don't fail if definition is undefined Running `uglifyjs --verbose --compress --mangle --screw-ie8 class.js` with `class.js`: ``` class Foo { bar() { } } ``` Fails with: ``` undefined:4041 return this.definition().unmangleable(options); TypeError: Cannot read property 'unmangleable' of undefined ... ``` --- lib/scope.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index ac58ad80..371afb2b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -350,7 +350,8 @@ AST_Scope.DEFMETHOD("references", function(sym){ }); AST_Symbol.DEFMETHOD("unmangleable", function(options){ - return this.definition().unmangleable(options); + var def = this.definition(); + return def && def.unmangleable(options); }); // property accessors are not mangleable From 6780d0906c324102ec655d8e844754223876382a Mon Sep 17 00:00:00 2001 From: viclm Date: Tue, 8 Mar 2016 15:43:01 +0800 Subject: [PATCH 061/121] Fix eager parsing of arrow functions for non-punc tokens --- lib/parse.js | 2 +- test/compress/issue-1001.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1001.js diff --git a/lib/parse.js b/lib/parse.js index 0a6072af..8e608e8a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1794,7 +1794,7 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; - if (start.value == "(" && peek().value == ")") { + if (start.type == "punc" && start.value == "(" && peek().value == ")") { next(); next(); return arrow_function([]); diff --git a/test/compress/issue-1001.js b/test/compress/issue-1001.js new file mode 100644 index 00000000..89adf81c --- /dev/null +++ b/test/compress/issue-1001.js @@ -0,0 +1,8 @@ +parenthesis_strings_in_parenthesis: { + input: { + ('('); + a(')'); + + } + expect_exact: '"(";a(")");' +} From 0465bd270d95e2fcf71c446438d3b9cfda527190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 29 Jan 2016 20:47:49 +0000 Subject: [PATCH 062/121] Starting out the import statement --- lib/ast.js | 7 +++++++ lib/output.js | 6 ++++++ lib/parse.js | 20 ++++++++++++++++++-- test/compress/harmony.js | 8 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 358862b1..0940647c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,6 +702,13 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); +var AST_Import = DEFNODE("Import", "module_name", { + $documentation: "An `import` statement", + $propdoc: { + module_name: "[AST_String] String literal describing where this module came from", + } +}); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index c0b86b96..4dc53798 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1037,6 +1037,12 @@ function OutputStream(options) { DEFPRINT(AST_Const, function(self, output){ self._do_print(output, "const"); }); + DEFPRINT(AST_Import, function(self, output) { + output.print("import"); + output.space(); + self.module_name.print(output); + output.semicolon(); + }); function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); diff --git a/lib/parse.js b/lib/parse.js index 8e608e8a..4a549fb1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; @@ -909,6 +909,9 @@ function parse($TEXT, options) { body : statement() }); + case "import": + return tmp = import_(), semicolon(), tmp; + default: unexpected(); } @@ -1607,6 +1610,19 @@ function parse($TEXT, options) { } } + function import_() { + return new AST_Import({ + start: prev(), + module_name: new AST_String({ + start : S.token, + value : S.token.value, + quote : S.token.quote, + end : S.token, + }), + end: next(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 622e00a0..4cb3921c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -305,6 +305,14 @@ number_literals: { } } +import_statement: { + input: { + import "mod-name"; + import "module2"; + } + expect_exact: "import\"mod-name\";import\"module2\";" +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From d35a9e783920fd286cdbb1574e703f199a1415f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 21 Feb 2016 17:06:09 +0000 Subject: [PATCH 063/121] Importing names from places --- lib/ast.js | 15 ++++++++++++++- lib/compress.js | 4 ++++ lib/output.js | 6 ++++++ lib/parse.js | 24 ++++++++++++++++++------ test/compress/harmony.js | 16 ++++++++++++++-- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0940647c..3457c134 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "module_name", { +var AST_Import = DEFNODE("Import", "imported_name module_name", { $documentation: "An `import` statement", $propdoc: { + imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", module_name: "[AST_String] String literal describing where this module came from", + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + if (this.imported_name) { + this.imported_name._walk(visitor); + } + this.module_name._walk(visitor); + }); } }); @@ -1072,6 +1081,10 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); +var AST_SymbolImport = DEFNODE("SymbolImport", null, { + $documentation: "Symbol refering to an imported name", +}, AST_SymbolDeclaration); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/compress.js b/lib/compress.js index 52fbb74d..b1fbdf25 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1775,6 +1775,10 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_Import, function(self, compressor) { + return self; + }); + OPT(AST_Function, function(self, compressor){ self = AST_Lambda.prototype.optimize.call(self, compressor); if (compressor.option("unused") && !compressor.option("keep_fnames")) { diff --git a/lib/output.js b/lib/output.js index 4dc53798..52747603 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1040,6 +1040,12 @@ function OutputStream(options) { DEFPRINT(AST_Import, function(self, output) { output.print("import"); output.space(); + if (self.imported_name) { + self.imported_name.print(output); + output.space(); + output.print("from") + output.space(); + } self.module_name.print(output); output.semicolon(); }); diff --git a/lib/parse.js b/lib/parse.js index 4a549fb1..4e94f589 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1611,15 +1611,27 @@ function parse($TEXT, options) { } function import_() { + var start = prev(); + var imported_name; + if (is("name")) { + imported_name = as_symbol(AST_SymbolImport); + expect_token("name", "from"); + } + var mod_str = S.token; + if (mod_str.type !== 'string') { + unexpected(); + } + next(); return new AST_Import({ - start: prev(), + start: start, + imported_name: imported_name, module_name: new AST_String({ - start : S.token, - value : S.token.value, - quote : S.token.quote, - end : S.token, + start: mod_str, + value: mod_str.value, + quote: mod_str.quote, + end: mod_str, }), - end: next(), + end: S.token, }); } diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 4cb3921c..dc1c2e4f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -308,9 +308,21 @@ number_literals: { import_statement: { input: { import "mod-name"; - import "module2"; + import Foo from "bar"; + } + expect_exact: "import\"mod-name\";import Foo from\"bar\";" +} + +import_statement_mangling: { + mangle = { }; + input: { + import Foo from "foo"; + Foo(); + } + expect: { + import a from "foo"; + a(); } - expect_exact: "import\"mod-name\";import\"module2\";" } // Fabio: My patches accidentally caused a crash whenever From 59e1601fb8fbcf03a2e26ce7eea72afac86ee880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 26 Feb 2016 21:12:19 +0000 Subject: [PATCH 064/121] importing names in the modules, not just default imports --- lib/ast.js | 15 ++++++++++++++- lib/output.js | 31 +++++++++++++++++++++++++++++++ lib/parse.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/compress/harmony.js | 5 ++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 3457c134..768bfc78 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "imported_name module_name", { +var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { + $documentation: "The part of the import statement that imports names from a module.", + $propdoc: { + foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", + name: "[AST_SymbolImport] The name as it becomes available to this module." + } +}) + +var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { $documentation: "An `import` statement", $propdoc: { imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", + imported_names: "[AST_NameImport*] The names of non-default imported variables", module_name: "[AST_String] String literal describing where this module came from", }, _walk: function(visitor) { @@ -1085,6 +1094,10 @@ var AST_SymbolImport = DEFNODE("SymbolImport", null, { $documentation: "Symbol refering to an imported name", }, AST_SymbolDeclaration); +var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { + $documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes", +}, AST_Symbol); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/output.js b/lib/output.js index 52747603..a9c78bbf 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1042,6 +1042,25 @@ function OutputStream(options) { output.space(); if (self.imported_name) { self.imported_name.print(output); + } + if (self.imported_name && self.imported_names) { + output.print(","); + output.space(); + } + if (self.imported_names) { + output.print("{"); + self.imported_names.forEach(function(name_import, i) { + output.space(); + name_import.print(output); + if (i < self.imported_names.length - 1) { + output.print(","); + output.space(); + } + }); + output.space(); + output.print("}"); + } + if (self.imported_name || self.imported_names) { output.space(); output.print("from") output.space(); @@ -1050,6 +1069,18 @@ function OutputStream(options) { output.semicolon(); }); + DEFPRINT(AST_NameImport, function(self, output) { + if (self.foreign_name) { + self.foreign_name.print(output); + output.space(); + output.print("as"); + output.space(); + self.name.print(output); + } else { + self.name.print(output); + } + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index 4e94f589..fd51eda2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1613,8 +1613,28 @@ function parse($TEXT, options) { function import_() { var start = prev(); var imported_name; + var imported_names; if (is("name")) { imported_name = as_symbol(AST_SymbolImport); + } + + if (is("punc", ",")) { + next(); + } + + if (is("punc", "{")) { + next(); + imported_names = []; + while (!is("punc", "}")) { + imported_names.push(import_name()); + if (is("punc", ",")) { + next(); + } + } + next(); + } + + if (imported_names || imported_name) { expect_token("name", "from"); } var mod_str = S.token; @@ -1625,6 +1645,7 @@ function parse($TEXT, options) { return new AST_Import({ start: start, imported_name: imported_name, + imported_names: imported_names, module_name: new AST_String({ start: mod_str, value: mod_str.value, @@ -1635,6 +1656,25 @@ function parse($TEXT, options) { }); } + function import_name() { + var start = S.token; + var foreign_name; + var name; + + if (peek().value === "as" && peek().type === "name") { + foreign_name = as_symbol(AST_SymbolImportForeign); + next(); // The "as" word + } + name = as_symbol(AST_SymbolImport); + + return new AST_NameImport({ + start: start, + foreign_name: foreign_name, + name: name, + end: prev(), + }) + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index dc1c2e4f..59c6238a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -309,8 +309,11 @@ import_statement: { input: { import "mod-name"; import Foo from "bar"; + import { Bar, Baz } from 'lel'; + import Bar, { Foo } from 'lel'; + import { Bar as kex, Baz as food } from 'lel'; } - expect_exact: "import\"mod-name\";import Foo from\"bar\";" + expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } import_statement_mangling: { From 86b5248837d3b4f2bfa14da6537e349b3306df26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:01:16 +0000 Subject: [PATCH 065/121] Mangling externally imported names by using aliasing --- lib/ast.js | 11 +++++++++++ lib/output.js | 8 ++++++-- lib/parse.js | 8 ++++++++ lib/scope.js | 9 +++++++++ test/compress/harmony.js | 10 ++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 768bfc78..6a814e35 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -707,6 +707,12 @@ var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { $propdoc: { foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", name: "[AST_SymbolImport] The name as it becomes available to this module." + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + this.foreign_name._walk(visitor); + this.name._walk(visitor); + }); } }) @@ -722,6 +728,11 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { if (this.imported_name) { this.imported_name._walk(visitor); } + if (this.imported_names) { + this.imported_names.forEach(function (name_import) { + name_import._walk(visitor); + }); + } this.module_name._walk(visitor); }); } diff --git a/lib/output.js b/lib/output.js index a9c78bbf..e755ec04 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1070,8 +1070,12 @@ function OutputStream(options) { }); DEFPRINT(AST_NameImport, function(self, output) { - if (self.foreign_name) { - self.foreign_name.print(output); + var definition = self.name.definition(); + var names_are_different = + (definition && definition.mangled_name || self.name.name) !== + self.foreign_name.name; + if (names_are_different) { + output.print(self.foreign_name.name); output.space(); output.print("as"); output.space(); diff --git a/lib/parse.js b/lib/parse.js index fd51eda2..134b3f19 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1667,6 +1667,14 @@ function parse($TEXT, options) { } name = as_symbol(AST_SymbolImport); + if (foreign_name === undefined) { + foreign_name = new AST_SymbolImportForeign({ + name: name.name, + start: name.start, + end: name.end, + }); + } + return new AST_NameImport({ start: start, foreign_name: foreign_name, diff --git a/lib/scope.js b/lib/scope.js index 371afb2b..23f70ad1 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -171,6 +171,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolClass) { defun.def_variable(node); } + else if (node instanceof AST_SymbolImport) { + scope.def_variable(node); + } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. @@ -302,6 +305,12 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ this.variables.set(symbol.name, def); def.object_destructuring_arg = symbol.object_destructuring_arg; def.global = !this.parent_scope; + if (symbol instanceof AST_SymbolImport) { + // Imports are not global + def.global = false; + // TODO The real fix comes with block scoping being first class in uglifyJS, + // enabling import definitions to behave like module-level let declarations + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 59c6238a..5ef1416e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -320,11 +320,21 @@ import_statement_mangling: { mangle = { }; input: { import Foo from "foo"; + import Bar, {Food} from "lel"; + import {What as Whatever} from "lel"; Foo(); + Bar(); + Food(); + Whatever(); } expect: { import a from "foo"; + import b, {Food as c} from "lel"; + import {What as d} from "lel"; a(); + b(); + c(); + d(); } } From ce84a706a3eab73589d0723bf8e03f8d4cbd776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:24:18 +0000 Subject: [PATCH 066/121] Implement the export statement --- lib/ast.js | 9 +++++++++ lib/output.js | 15 +++++++++++++++ lib/parse.js | 35 ++++++++++++++++++++++++++++++++++- test/compress/harmony.js | 12 ++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 6a814e35..e6867205 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -738,6 +738,15 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { } }); +var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default", { + $documentation: "An `export` statement", + $propdoc: { + exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition", + exported_value: "[AST_Node?] An exported value", + is_default: "[Boolean] Whether this is the default exported value of this module" + }, +}, AST_Statement); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index e755ec04..0dd5dcb9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1085,6 +1085,21 @@ function OutputStream(options) { } }); + DEFPRINT(AST_Export, function(self, output) { + output.print("export"); + output.space(); + if (self.is_default) { + output.print("default"); + output.space(); + } + if (self.exported_value) { + self.exported_value.print(output); + } else if (self.exported_definition) { + self.exported_definition.print(output); + } + output.semicolon(); + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index 134b3f19..5463576d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import export'; var KEYWORDS_ATOM = 'false null true'; var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -912,6 +912,9 @@ function parse($TEXT, options) { case "import": return tmp = import_(), semicolon(), tmp; + case "export": + return tmp = export_(), semicolon(), tmp; + default: unexpected(); } @@ -1683,6 +1686,36 @@ function parse($TEXT, options) { }) } + function export_() { + var start = S.token; + var is_default; + var exported_value; + var exported_definition; + + if (is("keyword", "default")) { + is_default = true; + next(); + } + + var is_definition = + is("keyword", "var") || is("keyword", "let") || is("keyword", "const") || + is("keyword", "class") || is("keyword", "function"); + + if (is_definition) { + exported_definition = statement(); + } else { + exported_value = expression(); + } + + return new AST_Export({ + start: start, + is_default: is_default, + exported_value: exported_value, + exported_definition: exported_definition, + end: prev(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 5ef1416e..d27d903b 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -316,6 +316,18 @@ import_statement: { expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } +export_statement: { + input: { + export default 1; + export var foo = 4; + export let foo = 6; + export const foo = 6; + export function foo() {}; + export class foo { }; + } + expect_exact: "export default 1;export var foo=4;export let foo=6;export const foo=6;export function foo(){};export class foo{};" +} + import_statement_mangling: { mangle = { }; input: { From 0bc4f6edb4ccfa04b124e244268bd95a01d9f642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:40:57 +0000 Subject: [PATCH 067/121] Don't mangle exported symbols --- lib/ast.js | 10 ++++++++++ lib/scope.js | 32 ++++++++++++++++++++++---------- test/compress/harmony.js | 16 ++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index e6867205..160b1cc2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -745,6 +745,16 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul exported_value: "[AST_Node?] An exported value", is_default: "[Boolean] Whether this is the default exported value of this module" }, + _walk: function (visitor) { + visitor._visit(this, function () { + if (this.exported_definition) { + this.exported_definition._walk(visitor); + } + if (this.exported_value) { + this.exported_value._walk(visitor); + } + }); + } }, AST_Statement); var AST_VarDef = DEFNODE("VarDef", "name value", { diff --git a/lib/scope.js b/lib/scope.js index 23f70ad1..f9241f2d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -49,6 +49,7 @@ function SymbolDef(scope, index, orig) { this.scope = scope; this.references = []; this.global = false; + this.export = false; this.mangled_name = null; this.object_destructuring_arg = false; this.undeclared = false; @@ -61,6 +62,7 @@ SymbolDef.prototype = { if (!options) options = {}; return (this.global && !options.toplevel) + || this.export || this.object_destructuring_arg || this.undeclared || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) @@ -102,6 +104,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var defun = null; var nesting = 0; var in_destructuring = null; + var in_export; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -131,6 +134,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ labels = save_labels; return true; // don't descend again in TreeWalker } + if (node instanceof AST_Export) { + in_export = true; + descend(); + in_export = false; + } if (node instanceof AST_LabeledStatement) { var l = node.label; if (labels.has(l.name)) { @@ -151,14 +159,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolFunarg) { node.object_destructuring_arg = !!in_destructuring; - defun.def_variable(node); + defun.def_variable(node, in_export); } if (node instanceof AST_Label) { node.thedef = node; node.references = []; } if (node instanceof AST_SymbolLambda) { - defun.def_function(node); + defun.def_function(node, in_export); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -166,22 +174,22 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolClass) { - defun.def_variable(node); + defun.def_variable(node, in_export); } else if (node instanceof AST_SymbolImport) { - scope.def_variable(node); + scope.def_variable(node, in_export); } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node); + var def = defun.def_variable(node, in_export); def.constant = node instanceof AST_SymbolConst; def.destructuring = in_destructuring; def.init = tw.parent().value; @@ -294,11 +302,11 @@ AST_Scope.DEFMETHOD("find_variable", function(name){ || (this.parent_scope && this.parent_scope.find_variable(name)); }); -AST_Scope.DEFMETHOD("def_function", function(symbol){ - this.functions.set(symbol.name, this.def_variable(symbol)); +AST_Scope.DEFMETHOD("def_function", function(symbol, in_export){ + this.functions.set(symbol.name, this.def_variable(symbol, in_export)); }); -AST_Scope.DEFMETHOD("def_variable", function(symbol){ +AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){ var def; if (!this.variables.has(symbol.name)) { def = new SymbolDef(this, this.variables.size(), symbol); @@ -311,6 +319,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ // TODO The real fix comes with block scoping being first class in uglifyJS, // enabling import definitions to behave like module-level let declarations } + if (!this.parent_scope && in_export) { + def.global = false; + def.export = true; + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index d27d903b..fec3a835 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -350,6 +350,22 @@ import_statement_mangling: { } } +export_statement_mangling: { + mangle = { }; + input: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } + expect: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From accca2445fcbf87b21702f34e378233dfaf195ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 13:27:18 +0000 Subject: [PATCH 068/121] fix crash: Import statements don't abort --- lib/compress.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compress.js b/lib/compress.js index b1fbdf25..f96fb04a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1025,6 +1025,7 @@ merge(Compressor.prototype, { var n = this.body.length; return n > 0 && aborts(this.body[n - 1]); }; + def(AST_Import, function(){ return null; }); def(AST_BlockStatement, block_aborts); def(AST_SwitchBranch, block_aborts); def(AST_If, function(){ From 6d2f77c18023833256d9059c1fbb36b5522b4131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 12 Mar 2016 15:53:57 +0000 Subject: [PATCH 069/121] fix #1003 by removing AST_ObjectSymbol and using AST_ObjectKeyVal for the same effect --- lib/ast.js | 21 +++++---------------- lib/output.js | 25 ++++++++----------------- lib/parse.js | 8 +++++--- lib/propmangle.js | 8 -------- lib/transform.js | 4 ---- test/compress/harmony.js | 14 ++++++++++++++ 6 files changed, 32 insertions(+), 48 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 160b1cc2..fc03a2c0 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -395,9 +395,9 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.properties.map(to_fun_args) }); - } else if (ex instanceof AST_ObjectSymbol) { + } else if (ex instanceof AST_ObjectKeyVal && ex.shorthand) { return new AST_SymbolFunarg({ - name: ex.symbol.name, + name: ex.key, start: ex.start, end: ex.end }); @@ -981,10 +981,11 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote shorthand", { $documentation: "A key: value object property", $propdoc: { - quote: "[string] the original quote character" + quote: "[string] the original quote character", + shorthand: "[boolean] whether this is a shorthand key:value pair, expressed as just the key." } }, AST_ObjectProperty); @@ -998,18 +999,6 @@ var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { } }, AST_ObjectProperty); -var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { - $propdoc: { - symbol: "[AST_SymbolRef] what symbol it is" - }, - $documentation: "A symbol in an object", - _walk: function (visitor) { - return visitor._visit(this, function(){ - this.symbol._walk(visitor); - }); - } -}, AST_ObjectProperty); - var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { $propdoc: { static: "[boolean] whether this is a static setter (classes only)" diff --git a/lib/output.js b/lib/output.js index 0dd5dcb9..cbd13893 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1301,6 +1301,14 @@ function OutputStream(options) { DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; + var print_as_shorthand = self.shorthand && + self.value instanceof AST_Symbol && + self.key == self.value.print_to_string(); + + if (print_as_shorthand) { + output.print_name(key); + return; + } if (output.option("quote_keys")) { output.print_string(key + ""); } else if ((typeof key == "number" @@ -1359,23 +1367,6 @@ function OutputStream(options) { self.default.print(output) } }); - DEFPRINT(AST_ObjectSymbol, function(self, output){ - var name = self.mangled_key || self.symbol.name; - var def = self.symbol.definition(); - if (def && def.mangled_name) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.mangled_name); - } else if (!(def && def.mangled_name) && self.mangled_key) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.name); - } else { - output.print(name); - } - }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index 5463576d..6bc79215 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1514,14 +1514,16 @@ function parse($TEXT, options) { })); } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectSymbol({ + a.push(new AST_ObjectKeyVal({ start: start, end: start, - symbol: new AST_SymbolRef({ + key: name, + value: new AST_SymbolRef({ start: start, end: start, name: name - }) + }), + shorthand: true, })); } else { expect(":"); diff --git a/lib/propmangle.js b/lib/propmangle.js index 86da5de9..050f07d7 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -90,9 +90,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { add(node.key); } - else if (node instanceof AST_ObjectSymbol) { - add(node.symbol.name); - } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above add(node.key.name); @@ -117,11 +114,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { node.key = mangle(node.key); } - else if (node instanceof AST_ObjectSymbol) { - if (should_mangle(node.symbol.name)) { - node.mangled_key = mangle(node.symbol.name) - } - } else if (node instanceof AST_ObjectProperty) { // setter or getter node.key.name = mangle(node.key.name); diff --git a/lib/transform.js b/lib/transform.js index 2cea8705..dc3a068f 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -219,10 +219,6 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); - _(AST_ObjectSymbol, function(self, tw){ - self.symbol = self.symbol.transform(tw); - }); - _(AST_ObjectProperty, function(self, tw){ self.value = self.value.transform(tw); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index fec3a835..da3f9d4e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -54,6 +54,20 @@ computed_property_names: { expect_exact: 'obj({["x"+"x"]:6});' } +shorthand_properties: { + mangle = true; + input: (function() { + var prop = 1; + const value = {prop}; + return value; + })(); + expect: (function() { + var a = 1; + const b = {prop:a}; + return b; + })(); +} + typeof_arrow_functions: { options = { evaluate: true From 6702cae9185976cc03b6509858a4c4a3df4fe438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 27 Mar 2016 12:21:39 +0100 Subject: [PATCH 070/121] fix #1021 --- lib/parse.js | 7 ++++--- test/compress/harmony.js | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 6bc79215..c2be35e7 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -979,11 +979,12 @@ function parse($TEXT, options) { init = is("keyword", "var") ? (next(), var_(true)) : is("keyword", "let") ? (next(), let_(true)) : + is("keyword", "const") ? (next(), const_(true)) : expression(true, true); var is_in = is("operator", "in"); var is_of = is("name", "of"); if (is_in || is_of) { - if ((init instanceof AST_Var || init instanceof AST_Let) && + if ((init instanceof AST_Definitions) && init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); next(); @@ -1012,7 +1013,7 @@ function parse($TEXT, options) { }; function for_of(init) { - var lhs = init instanceof AST_Var ? init.definitions[0].name : null; + var lhs = init instanceof AST_Definitions ? init.definitions[0].name : null; var obj = expression(true); expect(")"); return new AST_ForOf({ @@ -1024,7 +1025,7 @@ function parse($TEXT, options) { }; function for_in(init) { - var lhs = init instanceof AST_Var ? init.definitions[0].name : null; + var lhs = init instanceof AST_Definitions ? init.definitions[0].name : null; var obj = expression(true); expect(")"); return new AST_ForIn({ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index da3f9d4e..3a6f2464 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -380,6 +380,17 @@ export_statement_mangling: { } } +// https://github.com/mishoo/UglifyJS2/issues/1021 +regression_for_of_const: { + input: { + for (const x of y) {} + for (const x in y) {} + } + expect: { + for (const x of y);for (const x in y); + } +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From 634f231b78853dd0f02f6857eb9d4a646d571029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 28 Feb 2016 14:06:51 +0000 Subject: [PATCH 071/121] First class block scope - Make let, const, and class symbols be declared in a block scope. - Piggy back on existing catch symbol implementation to get block-aware mangling working - Make sure unused block-scoped declarations can be dropped - Don't eliminate a block if it has a block-scoped declaration - Remove silly empty anonymous blocks left over from drop_unused - AST_Toplevel now gets to call drop_unused too, since block-scoped variables aren't global! - Don't consider block declarations global --- lib/ast.js | 19 +++++-- lib/compress.js | 35 +++++++++--- lib/parse.js | 13 +++-- lib/scope.js | 41 +++++++++----- test/compress/block-scope.js | 100 +++++++++++++++++++++++++++++++++++ test/compress/dead-code.js | 22 ++++++++ test/compress/drop-unused.js | 66 +++++++++++++++++++++++ 7 files changed, 266 insertions(+), 30 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index fc03a2c0..abc277d3 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -282,9 +282,10 @@ var AST_With = DEFNODE("With", "expression", { /* -----[ scope and functions ]----- */ -var AST_Scope = DEFNODE("Scope", "directives variables functions uses_with uses_eval parent_scope enclosed cname", { +var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions uses_with uses_eval parent_scope enclosed cname", { $documentation: "Base class for all statements introducing a lexical scope", $propdoc: { + is_block_scope: "[boolean] identifies a block scope", directives: "[string*/S] an array of directives declared in this scope", variables: "[Object/S] a map of name -> SymbolDef for all variables/functions defined in this scope", functions: "[Object/S] like `variables`, but only lists function declarations", @@ -1077,9 +1078,17 @@ var AST_SymbolVar = DEFNODE("SymbolVar", null, { $documentation: "Symbol defining a variable", }, AST_SymbolDeclaration); +var AST_SymbolBlockDeclaration = DEFNODE("SymbolBlockDeclaration", null, { + $documentation: "Base class for block-scoped declaration symbols" +}, AST_SymbolDeclaration); + var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); + +var AST_SymbolLet = DEFNODE("SymbolLet", null, { + $documentation: "A block-scoped `let` declaration" +}, AST_SymbolBlockDeclaration); var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", @@ -1099,7 +1108,7 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, { $documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class." -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolClass = DEFNODE("SymbolClass", null, { $documentation: "Symbol naming a class's name. Lexically scoped to the class." @@ -1107,11 +1116,11 @@ var AST_SymbolClass = DEFNODE("SymbolClass", null, { var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolImport = DEFNODE("SymbolImport", null, { $documentation: "Symbol refering to an imported name", -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { $documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes", diff --git a/lib/compress.js b/lib/compress.js index f96fb04a..3dcb922a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -190,6 +190,14 @@ merge(Compressor.prototype, { return false; }; + function can_be_evicted_from_block(node) { + return !( + node instanceof AST_DefClass || + node instanceof AST_Let || + node instanceof AST_Const + ); + } + function loop_body(x) { if (x instanceof AST_Switch) return x; if (x instanceof AST_For || x instanceof AST_ForIn || x instanceof AST_DWLoop) { @@ -311,7 +319,7 @@ merge(Compressor.prototype, { function eliminate_spurious_blocks(statements) { var seen_dirs = []; return statements.reduce(function(a, stat){ - if (stat instanceof AST_BlockStatement) { + if (stat instanceof AST_BlockStatement && all(stat.body, can_be_evicted_from_block)) { CHANGED = true; a.push.apply(a, eliminate_spurious_blocks(stat.body)); } else if (stat instanceof AST_EmptyStatement) { @@ -633,7 +641,7 @@ merge(Compressor.prototype, { function extract_declarations_from_unreachable_code(compressor, stat, target) { compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); stat.walk(new TreeWalker(function(node){ - if (node instanceof AST_Definitions) { + if (node instanceof AST_Var) { compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start); node.remove_initializers(); target.push(node); @@ -957,6 +965,8 @@ merge(Compressor.prototype, { }); def(AST_Defun, function(compressor){ return true }); def(AST_Function, function(compressor){ return false }); + def(AST_Class, function(compressor){ return false }); + def(AST_DefClass, function(compressor){ return true }); def(AST_Binary, function(compressor){ return this.left.has_side_effects(compressor) || this.right.has_side_effects(compressor); @@ -1067,7 +1077,11 @@ merge(Compressor.prototype, { OPT(AST_BlockStatement, function(self, compressor){ self.body = tighten_body(self.body, compressor); switch (self.body.length) { - case 1: return self.body[0]; + case 1: + if (can_be_evicted_from_block(self.body[0])) { + return self.body[0]; + } + break; case 0: return make_node(AST_EmptyStatement, self); } return self; @@ -1077,7 +1091,6 @@ merge(Compressor.prototype, { var self = this; if (compressor.has_directive("use asm")) return self; if (compressor.option("unused") - && !(self instanceof AST_Toplevel) && !self.uses_eval ) { var in_use = []; @@ -1166,8 +1179,11 @@ merge(Compressor.prototype, { } } } - if (node instanceof AST_Defun && node !== self) { - if (!member(node.name.definition(), in_use)) { + if ((node instanceof AST_Defun || node instanceof AST_DefClass) && node !== self) { + var keep = + member(node.name.definition(), in_use) || + node.name.definition().global; + if (!keep) { compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, file : node.name.start.file, @@ -1182,6 +1198,7 @@ merge(Compressor.prototype, { var def = node.definitions.filter(function(def){ if (def.is_destructuring()) return true; if (member(def.name.definition(), in_use)) return true; + if (def.name.definition().global) return true; var w = { name : def.name.name, file : def.name.start.file, @@ -1260,6 +1277,12 @@ merge(Compressor.prototype, { }); } } + if (node instanceof AST_BlockStatement) { + descend(node, this); + if (in_list && all(node.body, can_be_evicted_from_block)) { + return MAP.splice(node.body); + } + } if (node instanceof AST_Scope && node !== self) return node; } diff --git a/lib/parse.js b/lib/parse.js index c2be35e7..bf90476f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1214,11 +1214,14 @@ function parse($TEXT, options) { }); }; - function vardefs(no_in, in_const) { + function vardefs(no_in, kind) { var a = []; var def; for (;;) { - var sym_type = in_const ? AST_SymbolConst : AST_SymbolVar; + var sym_type = + kind === "var" ? AST_SymbolVar : + kind === "const" ? AST_SymbolConst : + kind === "let" ? AST_SymbolLet : null; if (is("punc", "{") || is("punc", "[")) { def = new AST_VarDef({ start: S.token, @@ -1287,7 +1290,7 @@ function parse($TEXT, options) { var var_ = function(no_in) { return new AST_Var({ start : prev(), - definitions : vardefs(no_in, false), + definitions : vardefs(no_in, "var"), end : prev() }); }; @@ -1295,7 +1298,7 @@ function parse($TEXT, options) { var let_ = function(no_in) { return new AST_Let({ start : prev(), - definitions : vardefs(no_in, false), + definitions : vardefs(no_in, "let"), end : prev() }); }; @@ -1303,7 +1306,7 @@ function parse($TEXT, options) { var const_ = function() { return new AST_Const({ start : prev(), - definitions : vardefs(false, true), + definitions : vardefs(false, "const"), end : prev() }); }; diff --git a/lib/scope.js b/lib/scope.js index f9241f2d..ef92329b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -106,11 +106,15 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var in_destructuring = null; var in_export; var tw = new TreeWalker(function(node, descend){ - if (options.screw_ie8 && node instanceof AST_Catch) { + var create_a_block_scope = + (options.screw_ie8 && node instanceof AST_Catch) || + ((node instanceof AST_Block) && node.creates_block_scope()); + if (create_a_block_scope) { var save_scope = scope; scope = new AST_Scope(node); scope.init_scope_vars(nesting); scope.parent_scope = save_scope; + scope.is_block_scope = true; descend(); scope = save_scope; return true; @@ -174,7 +178,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node, in_export); + var parent_lambda = defun.parent_scope; + while (parent_lambda.is_block_scope) { + parent_lambda = parent_lambda.parent_scope; + } + (node.scope = parent_lambda).def_function(node, in_export); } else if (node instanceof AST_SymbolClass) { defun.def_variable(node, in_export); @@ -188,8 +196,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolVar - || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node, in_export); + || node instanceof AST_SymbolConst + || node instanceof AST_SymbolLet) { + var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node, in_export); def.constant = node instanceof AST_SymbolConst; def.destructuring = in_destructuring; def.init = tw.parent().value; @@ -279,6 +288,14 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ this.nesting = nesting; // the nesting level of this scope (0 means toplevel) }); +AST_Block.DEFMETHOD("creates_block_scope", function() { + return ( + !(this instanceof AST_Lambda) && + !(this instanceof AST_Toplevel) && + !(this instanceof AST_Class) + ); +}); + AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; @@ -312,17 +329,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){ def = new SymbolDef(this, this.variables.size(), symbol); this.variables.set(symbol.name, def); def.object_destructuring_arg = symbol.object_destructuring_arg; - def.global = !this.parent_scope; - if (symbol instanceof AST_SymbolImport) { - // Imports are not global - def.global = false; - // TODO The real fix comes with block scoping being first class in uglifyJS, - // enabling import definitions to behave like module-level let declarations - } - if (!this.parent_scope && in_export) { - def.global = false; + if (in_export) { def.export = true; } + def.global = !this.parent_scope && !(symbol instanceof AST_SymbolBlockDeclaration); } else { def = this.variables.get(symbol.name); def.orig.push(symbol); @@ -466,7 +476,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ node.mangled_name = name; return true; } - if (options.screw_ie8 && node instanceof AST_SymbolCatch) { + var mangle_with_block_scope = + (options.screw_ie8 && node instanceof AST_SymbolCatch) || + node instanceof AST_SymbolBlockDeclaration; + if (mangle_with_block_scope) { to_mangle.push(node.definition()); return; } diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index d1953ce5..47a38f74 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -31,3 +31,103 @@ do_not_hoist_let: { } } +do_not_remove_anon_blocks_if_they_have_decls: { + input: { + function x() { + { + let x; + } + { + var x; + } + { + const y; + class Zee {}; + } + } + { + let y; + } + { + var y; + } + } + expect: { + function x(){ + { + let x + } + var x; + { + const y; + class Zee {} + } + } + { + let y + } + var y; + } +} + +remove_unused_in_global_block: { + options = { + unused: true, + } + input: { + { + let x; + const y; + class Zee {}; + var w; + } + let ex; + const why; + class Zed {}; + var wut; + console.log(x, y, Zee); + } + expect: { + var w; + var wut; + console.log(x, y, Zee); + } +} + +regression_block_scope_resolves: { + mangle = { }; + options = { + dead_code: false + }; + input: { + (function () { + if(1) { + let x; + const y; + class Zee {}; + } + if(1) { + let ex; + const why; + class Zi {}; + } + console.log(x, y, Zee, ex, why, Zi); + }()); + } + expect: { + (function () { + if (1) { + let a; + const b; + class c {}; + } + if (1) { + let a; + const b; + class c {}; + } + console.log(x, y, Zee, ex, why, Zi); + }()); + } +} + diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 5009ae1e..dddc9159 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -87,3 +87,25 @@ dead_code_constant_boolean_should_warn_more: { var moo; } } + +dead_code_block_decls_die: { + options = { + dead_code : true, + conditionals : true, + booleans : true, + evaluate : true + }; + input: { + if (0) { + let foo = 6; + const bar = 12; + class Baz {}; + var qux; + } + console.log(foo, bar, Baz); + } + expect: { + var qux; + console.log(foo, bar, Baz); + } +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index eebb81c6..5c4ffde9 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -164,6 +164,72 @@ used_var_in_catch: { } } +unused_block_decls_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + let x = 10; + const y = 10; + class Zee {}; + } + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) {} + } + } +} + +used_block_decls_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + let x = 10; + const y = 10; + class Zee {}; + } + console.log(x, y, Zee); + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) {} + console.log(x, y, Zee); + } + } +} + +unused_block_decls: { + options = { unused: true }; + input: { + function foo() { + { + const x; + } + { + let y; + } + console.log(x, y); + } + } + expect: { + function foo() { + console.log(x, y); + } + } +} + unused_keep_harmony_destructuring: { options = { unused: true }; input: { From 91cdb93e579d22bc0a43baa766a7928d127b314c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Thu, 4 Feb 2016 22:19:48 +0100 Subject: [PATCH 072/121] Implement harmony generators and yield Uses #716's implementation and adds tests. Fixes #716. --- lib/ast.js | 3 ++- lib/output.js | 18 ++++++++++++++++++ lib/parse.js | 14 +++++++++++++- test/compress/harmony.js | 16 ++++++++++++++++ test/parser.js | 11 +++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index abc277d3..e6e39d75 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -438,9 +438,10 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } }); -var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { +var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments is_generator", { $documentation: "Base class for functions", $propdoc: { + is_generator: "is generatorFn or not", name: "[AST_SymbolDeclaration?] the name of this function", argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" diff --git a/lib/output.js b/lib/output.js index cbd13893..827ba98e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -226,6 +226,10 @@ function OutputStream(options) { OUTPUT += str; }; + var star = function(){ + print("*"); + } + var space = options.beautify ? function() { print(" "); } : function() { @@ -343,6 +347,7 @@ function OutputStream(options) { should_break : function() { return options.width && this.current_width() >= options.width }, newline : newline, print : print, + star : star, space : space, comma : comma, colon : colon, @@ -783,6 +788,9 @@ function OutputStream(options) { var self = this; if (!nokeyword) { output.print("function"); + if (this.is_generator) { + output.star(); + } if (self.name) { output.space(); } @@ -1203,8 +1211,13 @@ function OutputStream(options) { output.print(self.operator); }); DEFPRINT(AST_Binary, function(self, output){ + var isYield = (self.left.operator == "yield" || self.left.operator === "yield*"); var op = self.operator; + + isYield && output.print("("); self.left.print(output); + isYield && output.print(")"); + if (op[0] == ">" /* ">>" ">>>" ">" ">=" */ && self.left instanceof AST_UnaryPostfix && self.left.operator == "--") { @@ -1214,7 +1227,12 @@ function OutputStream(options) { // the space is optional depending on "beautify" output.space(); } + + isYield = (self.right.operator == "yield" || self.right.operator === "yield*"); + isYield && output.print("("); output.print(op); + isYield && output.print(")"); + if ((op == "<" || op == "<<") && self.right instanceof AST_UnaryPrefix && self.right.operator == "!" diff --git a/lib/parse.js b/lib/parse.js index bf90476f..b9eeb63e 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import export'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import yield'; var KEYWORDS_ATOM = 'false null true'; var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -68,6 +68,7 @@ var OPERATORS = makePredicate([ "instanceof", "typeof", "new", + "yield", "void", "delete", "++", @@ -627,6 +628,7 @@ var UNARY_PREFIX = makePredicate([ "typeof", "void", "delete", + "yield", "--", "++", "!", @@ -1062,6 +1064,11 @@ function parse($TEXT, options) { var start = S.token var in_statement = ctor === AST_Defun; + var is_generator = is("operator", "*"); + if (is_generator) { + next(); + } + var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; if (in_statement && !name) unexpected(); @@ -1071,6 +1078,7 @@ function parse($TEXT, options) { return new ctor({ start : args.start, end : body.end, + is_generator: is_generator, name : name, argnames: args, body : body @@ -1836,6 +1844,10 @@ function parse($TEXT, options) { var start = S.token; if (is("operator") && UNARY_PREFIX(start.value)) { next(); + if (start.type === "operator" && start.value === "yield" && is("operator", "*")) { + start.value = "yield*"; + next(); + } handle_regexp(); var ex = make_unary(AST_UnaryPrefix, start.value, maybe_unary(allow_calls)); ex.start = start; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 3a6f2464..84aba4bb 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -418,3 +418,19 @@ regression_cannot_use_of: { foo(); /* Label statement missing? No prob. */ } } + +generators: { + input: { + function* fn() {}; + } + expect_exact: "function*fn(){}" +} + +generators_yield: { + input: { + function* fn() { + yield remote(); + } + } + expect_exact: "function*fn(){yield remote()}" +} diff --git a/test/parser.js b/test/parser.js index a84c2df9..5f6f62f5 100644 --- a/test/parser.js +++ b/test/parser.js @@ -118,6 +118,17 @@ module.exports = function () { ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); + + // generators + var generators_def = UglifyJS.parse('function* fn() {}').body[0]; + ok.equal(generators_def.is_generator, true); + + ok.throws(function () { + UglifyJS.parse('function* (){ }'); + }); + + var generators_yield_def = UglifyJS.parse('function* fn() {\nyield remote();\}').body[0].body[0]; + ok.equal(generators_yield_def.body.operator, 'yield'); } // Run standalone From eaf3911c3141a9b735e6e33c7147461d60c833c1 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 13 Apr 2016 14:39:49 +0200 Subject: [PATCH 073/121] Consider yield expressions as having side-effects See #1043 --- lib/compress.js | 4 +--- test/compress/issue-1043.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 test/compress/issue-1043.js diff --git a/lib/compress.js b/lib/compress.js index 3dcb922a..9c2dcbba 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -978,9 +978,7 @@ merge(Compressor.prototype, { || this.alternative.has_side_effects(compressor); }); def(AST_Unary, function(compressor){ - return this.operator == "delete" - || this.operator == "++" - || this.operator == "--" + return member(this.operator, ["delete", "++", "--", "yield", "yield*"]) || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ diff --git a/test/compress/issue-1043.js b/test/compress/issue-1043.js new file mode 100644 index 00000000..78043f65 --- /dev/null +++ b/test/compress/issue-1043.js @@ -0,0 +1,30 @@ +issue_1043: { + options = { + side_effects: true + }; + + input: { + function* range(start = 0, end = null, step = 1) { + if (end == null) { + end = start; + start = 0; + } + + for (let i = start; i < end; i += step) { + yield i; + } + } + } + + expect: { + function* range(start = 0, end = null, step = 1) { + if (null == end) { + end = start; + start = 0; + } + + for (let i = start; i < end; i += step) + yield i; + } + } +} From 68cc14f84678bf7777f777b668d6c006e65250d3 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 18 Apr 2016 15:51:32 +0200 Subject: [PATCH 074/121] Fixups after merge --- lib/ast.js | 7 +++++++ lib/compress.js | 2 +- test/compress/block-scope.js | 8 ++++---- test/compress/harmony.js | 24 ++++++++++++------------ 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index a0d2aac1..b89a00d2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -294,6 +294,13 @@ var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions parent_scope: "[AST_Scope?/S] link to the parent scope", enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes", cname: "[integer/S] current index for mangling variables (used internally by the mangler)", + }, + get_defun_scope: function () { + var self = this; + while (self.is_block_scope && self.parent_scope) { + self = self.parent_scope; + } + return self; } }, AST_Block); diff --git a/lib/compress.js b/lib/compress.js index 699a95b6..fdb06839 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -323,7 +323,7 @@ merge(Compressor.prototype, { if (side_effects_encountered |= lvalues_encountered) continue; // Non-constant single use vars can only be replaced in same scope. - if (ref.scope !== self) { + if (ref.scope.get_defun_scope() !== self) { side_effects_encountered |= var_decl.value.has_side_effects(compressor); continue; } diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index 4c8ca796..dd243009 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -117,13 +117,13 @@ regression_block_scope_resolves: { expect: { (function () { if (1) { - let a; - const b; + let o; + const n; class c {}; } if (1) { - let a; - const b; + let o; + const n; class c {}; } console.log(x, y, Zee, ex, why, Zi); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 84aba4bb..b4abb470 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -62,9 +62,9 @@ shorthand_properties: { return value; })(); expect: (function() { - var a = 1; - const b = {prop:a}; - return b; + var n = 1; + const r = {prop:n}; + return r; })(); } @@ -271,8 +271,8 @@ class_name_can_be_mangled: { expect: { function x() { class a { } - var b = a - var c = class a {} + var n = a + var r = class a {} } } } @@ -354,13 +354,13 @@ import_statement_mangling: { Whatever(); } expect: { - import a from "foo"; - import b, {Food as c} from "lel"; - import {What as d} from "lel"; - a(); - b(); - c(); - d(); + import l from "foo"; + import e, {Food as o} from "lel"; + import {What as f} from "lel"; + l(); + e(); + o(); + f(); } } From 8571a08a930474b68cb0e2daf3c7e666dcdcb15e Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 19 Apr 2016 20:01:26 +0200 Subject: [PATCH 075/121] Do not attempt evaluating class expressions Broadly the same as function expressions - these actually are statements but we're limited by the inheritance tree. Fixes #1044 --- lib/compress.js | 3 +++ test/compress/issue-1044.js | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 test/compress/issue-1044.js diff --git a/lib/compress.js b/lib/compress.js index fdb06839..32d38cef 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -977,6 +977,9 @@ merge(Compressor.prototype, { def(AST_Arrow, function() { throw def; }); + def(AST_Class, function() { + throw def; + }); function ev(node, compressor) { if (!compressor) throw new Error("Compressor must be passed"); diff --git a/test/compress/issue-1044.js b/test/compress/issue-1044.js new file mode 100644 index 00000000..360075a4 --- /dev/null +++ b/test/compress/issue-1044.js @@ -0,0 +1,9 @@ +issue_1044: { + options = { evaluate: true, conditionals: true }; + input: { + const mixed = Base ? class extends Base {} : class {} + } + expect: { + const mixed = Base ? class extends Base {} : class {} + } +} From 5b893c8ec35c7e0a0e612889d0fe64b5f084f79c Mon Sep 17 00:00:00 2001 From: not-an-aardvark Date: Mon, 25 Apr 2016 19:14:44 -0400 Subject: [PATCH 076/121] Avoid syntax error in yield assignments (fixes #1054) --- lib/output.js | 2 -- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index 002983e2..2aa5a5a1 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1234,9 +1234,7 @@ function OutputStream(options) { } isYield = (self.right.operator == "yield" || self.right.operator === "yield*"); - isYield && output.print("("); output.print(op); - isYield && output.print(")"); if ((op == "<" || op == "<<") && self.right instanceof AST_UnaryPrefix diff --git a/test/compress/harmony.js b/test/compress/harmony.js index b4abb470..6ee1b246 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -434,3 +434,13 @@ generators_yield: { } expect_exact: "function*fn(){yield remote()}" } + +generators_yield_assign: { + input: { + function* fn() { + var x = {}; + x.prop = yield 5; + } + } + expect_exact: "function*fn(){var x={};x.prop=yield 5}" +} From 63be1f3a4d5d10ba923dd40ffd55f4e5b7d41372 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sun, 15 May 2016 21:00:51 +0200 Subject: [PATCH 077/121] Only allow `var` definitions to be moved into the for-init clause Fixes #1079 --- lib/compress.js | 2 +- test/compress/join-vars.js | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 test/compress/join-vars.js diff --git a/lib/compress.js b/lib/compress.js index 51122751..6fbe2c8b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -802,7 +802,7 @@ merge(Compressor.prototype, { CHANGED = true; } else if (stat instanceof AST_For - && prev instanceof AST_Definitions + && prev instanceof AST_Var && (!stat.init || stat.init.TYPE == prev.TYPE)) { CHANGED = true; a.pop(); diff --git a/test/compress/join-vars.js b/test/compress/join-vars.js new file mode 100644 index 00000000..ea6077b6 --- /dev/null +++ b/test/compress/join-vars.js @@ -0,0 +1,40 @@ +only_vars: { + options = { join_vars: true }; + input: { + let netmaskBinary = ''; + for (let i = 0; i < netmaskBits; ++i) { + netmaskBinary += '1'; + } + } + expect: { + let netmaskBinary = ''; + for (let i = 0; i < netmaskBits; ++i) netmaskBinary += '1'; + } +} + +issue_1079_with_vars: { + options = { join_vars: true }; + input: { + var netmaskBinary = ''; + for (var i = 0; i < netmaskBits; ++i) { + netmaskBinary += '1'; + } + } + expect: { + for (var netmaskBinary = '', i = 0; i < netmaskBits; ++i) netmaskBinary += '1'; + } +} + +issue_1079_with_mixed: { + options = { join_vars: true }; + input: { + var netmaskBinary = ''; + for (let i = 0; i < netmaskBits; ++i) { + netmaskBinary += '1'; + } + } + expect: { + var netmaskBinary = '' + for (let i = 0; i < netmaskBits; ++i) netmaskBinary += '1'; + } +} \ No newline at end of file From 8ad8d7b7177454fb9d430d4da31b01825ca4eff8 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 22 May 2016 21:49:12 +0200 Subject: [PATCH 078/121] Add Symbol to builtins --- lib/propmangle.js | 8 +++++++- test/mocha/builtins.js | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/mocha/builtins.js diff --git a/lib/propmangle.js b/lib/propmangle.js index 050f07d7..b20a3815 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -44,10 +44,16 @@ "use strict"; function find_builtins() { + + // Compatibility fix for es5.1 and earlier where Symbol isn't defined + if (!global.Symbol) { + global.Symbol = new Function(); + } + var a = []; [ Object, Array, Function, Number, String, Boolean, Error, Math, - Date, RegExp + Date, RegExp, Symbol ].forEach(function(ctor){ Object.getOwnPropertyNames(ctor).map(add); if (ctor.prototype) { diff --git a/test/mocha/builtins.js b/test/mocha/builtins.js new file mode 100644 index 00000000..2782830a --- /dev/null +++ b/test/mocha/builtins.js @@ -0,0 +1,26 @@ +var UglifyJS = require("../../"); +var assert = require("assert"); + +describe("builtins", function() { + it ("Should not mangle builtins", function() { + var test = "function foo(something){\n" + + " return [Object,Array,Function,Number,String,Boolean,Error,Math,Date,RegExp,Symbol,something];\n" + + "};"; + + var result = UglifyJS.minify(test, {fromString: true, parse: {bare_returns: true}}).code; + + assert.strictEqual(result.indexOf("something"), -1); + + assert.notEqual(result.indexOf("Object"), -1); + assert.notEqual(result.indexOf("Array"), -1); + assert.notEqual(result.indexOf("Function"), -1); + assert.notEqual(result.indexOf("Number"), -1); + assert.notEqual(result.indexOf("String"), -1); + assert.notEqual(result.indexOf("Boolean"), -1); + assert.notEqual(result.indexOf("Error"), -1); + assert.notEqual(result.indexOf("Math"), -1); + assert.notEqual(result.indexOf("Date"), -1); + assert.notEqual(result.indexOf("RegExp"), -1); + assert.notEqual(result.indexOf("Symbol"), -1); + }); +}); From dcfc514c382363452eec1b634d471541bd6652d3 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Thu, 26 May 2016 17:00:37 +0200 Subject: [PATCH 079/121] Improve yield support and restrict usage of strict - Partially reverting 91cdb93e57 and eaf3911c31 and reimplement - Add generators support for objects and classes - Only classes can have static methods so restrict use of it Special thanks to @rvanvelzen and @kzc for reviewing this patch and providing constructive feedback over and over again. --- lib/ast.js | 18 ++++++- lib/compress.js | 9 +++- lib/output.js | 36 ++++++++++--- lib/parse.js | 92 ++++++++++++++++++++++++-------- lib/scope.js | 2 + lib/transform.js | 4 ++ test/compress/harmony.js | 112 +++++++++++++++++++++++++++++++++++++++ test/mocha/object.js | 20 +++++++ test/mocha/yield.js | 55 +++++++++++++++++++ test/parser.js | 2 +- 10 files changed, 318 insertions(+), 32 deletions(-) create mode 100644 test/mocha/object.js create mode 100644 test/mocha/yield.js diff --git a/lib/ast.js b/lib/ast.js index b89a00d2..dd5c6b8c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -487,8 +487,9 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); -var AST_ConciseMethod = DEFNODE("ConciseMethod", "static", { +var AST_ConciseMethod = DEFNODE("ConciseMethod", "is_generator static", { $propdoc: { + is_generator: "is generatorFn or not", static: "[boolean] whether this method is static (classes only)", }, $documentation: "An ES6 concise method inside an object or class" @@ -1241,6 +1242,21 @@ var AST_True = DEFNODE("True", null, { value: true }, AST_Boolean); +/* -----[ Yield ]----- */ + +var AST_Yield = DEFNODE("Yield", "expression is_star", { + $documentation: "A `yield` statement", + $propdoc: { + expression: "[AST_Node?] the value returned or thrown by this statement; could be null (representing undefined) but only when is_star is set to false", + is_star: "[Boolean] Whether this is a yield or yield* statement" + }, + _walk: function(visitor) { + return visitor._visit(this, this.expression && function(){ + this.expression._walk(visitor); + }); + } +}); + /* -----[ TreeWalker ]----- */ function TreeWalker(callback) { diff --git a/lib/compress.js b/lib/compress.js index 31e42d57..9d6eb098 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1245,7 +1245,7 @@ merge(Compressor.prototype, { || this.alternative.has_side_effects(compressor); }); def(AST_Unary, function(compressor){ - return member(this.operator, ["delete", "++", "--", "yield", "yield*"]) + return member(this.operator, ["delete", "++", "--"]) || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ @@ -2968,4 +2968,11 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_Yield, function(self, compressor){ + if (!self.is_star && self.expression instanceof AST_Undefined) { + self.expression = null; + } + return self; + }); + })(); diff --git a/lib/output.js b/lib/output.js index a71cd36f..5de1596d 100644 --- a/lib/output.js +++ b/lib/output.js @@ -560,6 +560,20 @@ function OutputStream(options) { } }); + PARENS(AST_Yield, function(output){ + var p = output.parent(); + // (yield 1) + (yield 2) + // a = yield 3 + if (p instanceof AST_Binary && p.operator !== "=") + return true; + // (yield 1) ? yield 2 : yield 3 + if (p instanceof AST_Conditional && p.condition === this) + return true; + // -(yield 4) + if (p instanceof AST_Unary) + return true; + }); + PARENS(AST_PropAccess, function(output){ var p = output.parent(); if (p instanceof AST_New && p.expression === this) { @@ -868,6 +882,9 @@ function OutputStream(options) { output.print("static"); output.space(); } + if (self.is_generator) { + output.print("*"); + } self._do_print(output, true /* do not print "function" */); }); @@ -887,6 +904,17 @@ function OutputStream(options) { self._do_print(output, "throw"); }); + /* -----[ yield ]----- */ + + DEFPRINT(AST_Yield, function(self, output){ + var star = self.is_star ? "*" : ""; + output.print("yield" + star); + if (self.expression) { + output.space(); + self.expression.print(output); + } + }); + /* -----[ loop control ]----- */ AST_LoopControl.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); @@ -1218,13 +1246,8 @@ function OutputStream(options) { output.print(self.operator); }); DEFPRINT(AST_Binary, function(self, output){ - var isYield = (self.left.operator == "yield" || self.left.operator === "yield*"); var op = self.operator; - - isYield && output.print("("); self.left.print(output); - isYield && output.print(")"); - if (op[0] == ">" /* ">>" ">>>" ">" ">=" */ && self.left instanceof AST_UnaryPostfix && self.left.operator == "--") { @@ -1234,10 +1257,7 @@ function OutputStream(options) { // the space is optional depending on "beautify" output.space(); } - - isYield = (self.right.operator == "yield" || self.right.operator === "yield*"); output.print(op); - if ((op == "<" || op == "<<") && self.right instanceof AST_UnaryPrefix && self.right.operator == "!" diff --git a/lib/parse.js b/lib/parse.js index a36b9fa2..b776ec12 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,11 +44,11 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import yield'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; 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' + " " + KEYWORDS_ATOM + " " + KEYWORDS; -var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; +var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case yield'; KEYWORDS = makePredicate(KEYWORDS); RESERVED_WORDS = makePredicate(RESERVED_WORDS); @@ -68,7 +68,6 @@ var OPERATORS = makePredicate([ "instanceof", "typeof", "new", - "yield", "void", "delete", "++", @@ -113,6 +112,8 @@ var OPERATORS = makePredicate([ var WHITESPACE_CHARS = makePredicate(characters(" \u00a0\n\r\t\f\u000b\u200b\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\uFEFF")); +var PUNC_AFTER_EXPRESSION = makePredicate(characters(";]),:")); + var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); var PUNC_CHARS = makePredicate(characters("[]{}(),;:`")); @@ -647,7 +648,6 @@ var UNARY_PREFIX = makePredicate([ "typeof", "void", "delete", - "yield", "--", "++", "!", @@ -711,6 +711,7 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, + in_generator : -1, in_directives : true, in_loop : 0, labels : [] @@ -776,6 +777,10 @@ function parse($TEXT, options) { ); }; + function is_in_generator() { + return S.in_generator === S.in_function; + } + function semicolon(optional) { if (is("punc", ";")) next(); else if (!optional && !can_insert_semicolon()) unexpected(); @@ -944,6 +949,10 @@ function parse($TEXT, options) { function labeled_statement() { var label = as_symbol(AST_Label); + if (label.name === "yield" && is_in_generator()) { + // Ecma-262, 12.1.1 Static Semantics: Early Errors + croak("Yield cannot be used as label inside generators"); + } if (find_if(function(l){ return l.name == label.name }, S.labels)) { // ECMA-262, 12.12: An ECMAScript program is considered // syntactically incorrect if it contains a @@ -1093,7 +1102,7 @@ function parse($TEXT, options) { unexpected(); var args = params_or_seq_().as_params(croak); - var body = _function_body(true); + var body = _function_body(true, is_generator); return new ctor({ start : args.start, end : body.end, @@ -1131,10 +1140,13 @@ function parse($TEXT, options) { }); } - function _function_body(block) { + function _function_body(block, generator) { var loop = S.in_loop; var labels = S.labels; + var current_generator = S.in_generator; ++S.in_function; + if (generator) + S.in_generator = S.in_function; if (block) S.in_directives = true; S.in_loop = 0; @@ -1146,9 +1158,39 @@ function parse($TEXT, options) { --S.in_function; S.in_loop = loop; S.labels = labels; + S.in_generator = current_generator; return a; } + function _yield_expression() { + // Previous token must be keyword yield and not be interpret as an identifier + if (!is_in_generator()) { + throw new JS_Parse_Error("Unexpected yield expression outside generator function", + S.prev.file, S.prev.line, S.prev.col, S.prev.pos); + } + var star = false; + var has_expression = true; + var tmp; + + // Get expression behind yield, default to value `undefined` stored as `null` in ast + // Expression must start on same line, yield* has a mandatory expression + if (is("operator", "*")) { + star = true; + next(); + if (S.token.nlb) { + unexpected(S.prev); + } + } else if (can_insert_semicolon() || + (is("punc") && PUNC_AFTER_EXPRESSION(S.token.value))) { + has_expression = false; + } + + return new AST_Yield({ + is_star : star, + expression : has_expression ? expression() : null + }); + } + function if_() { var cond = parenthesised(), body = statement(), belse = null; if (is("keyword", "else")) { @@ -1585,7 +1627,7 @@ function parse($TEXT, options) { } if (KindOfClass === AST_DefClass && !class_name) { - croak(); + unexpected(); } if (S.token.value == "extends") { @@ -1599,8 +1641,8 @@ function parse($TEXT, options) { while (!is("punc", "}")) { start = S.token; name = as_property_name(); - method = concise_method_or_getset(name, start); - if (!method) { croak(); } + method = concise_method_or_getset(name, start, true); + if (!method) { unexpected(); } a.push(method); if (is("punc", ";")) { next(); } } @@ -1616,21 +1658,28 @@ function parse($TEXT, options) { }); } - function concise_method_or_getset(name, start) { + function concise_method_or_getset(name, start, is_class) { var is_static = false; - if (name === "static" && !is("punc", "(")) { + var is_generator = false; + if (is_class && name === "static" && !is("punc", "(")) { is_static = true; name = S.token.value; next(); } + if (name === "*") { + is_generator = true; + name = S.token.value; + next(); + } if (is("punc", "(")) { return new AST_ConciseMethod({ - start : start, - static : is_static, - name : new AST_SymbolMethod({ name: name }), - argnames : params_or_seq_().as_params(croak), - body : _function_body(true), - end : prev() + is_generator: is_generator, + start : start, + static : is_static, + name : new AST_SymbolMethod({ name: name }), + argnames : params_or_seq_().as_params(croak), + body : _function_body(true, is_generator), + end : prev() }); } if (name == "get") { @@ -1870,10 +1919,6 @@ function parse($TEXT, options) { var start = S.token; if (is("operator") && UNARY_PREFIX(start.value)) { next(); - if (start.type === "operator" && start.value === "yield" && is("operator", "*")) { - start.value = "yield*"; - next(); - } handle_regexp(); var ex = make_unary(AST_UnaryPrefix, start.value, maybe_unary(allow_calls)); ex.start = start; @@ -1947,6 +1992,11 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + if (start.type == "name" && start.value == "yield" && is_in_generator()) { + next(); + return _yield_expression(); + } + if (start.type == "punc" && start.value == "(" && peek().value == ")") { next(); next(); diff --git a/lib/scope.js b/lib/scope.js index 39bd9cbf..7b8d38a0 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -575,6 +575,8 @@ AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ base54.consider("catch"); else if (node instanceof AST_Finally) base54.consider("finally"); + else if (node instanceof AST_Yield) + base54.consider("yield"); else if (node instanceof AST_Symbol && node.unmangleable(options)) base54.consider(node.name); else if (node instanceof AST_Unary || node instanceof AST_Binary) diff --git a/lib/transform.js b/lib/transform.js index 34663351..6e3a9d96 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -196,6 +196,10 @@ TreeTransformer.prototype = new TreeWalker; self.property = self.property.transform(tw); }); + _(AST_Yield, function(self, tw){ + if (self.expression) self.expression = self.expression.transform(tw); + }); + _(AST_Unary, function(self, tw){ self.expression = self.expression.transform(tw); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 6ee1b246..50fb47af 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -213,6 +213,23 @@ concise_methods_and_mangle_props: { } } +concise_generators: { + input: { + x = { + *foo(a, b) { + return x; + } + } + y = { + *foo([{a}]) { + yield a; + }, + bar(){} + } + } + expect_exact: "x={*foo(a,b){return x}};y={*foo([{a}]){yield a},bar(){}};" +} + concise_methods_and_keyword_names: { input: { x = { @@ -295,6 +312,21 @@ class_name_can_be_preserved: { } } +classes_can_have_generators: { + input: { + class Foo { + *bar() {} + static *baz() {} + } + } + expect: { + class Foo { + *bar() {} + static *baz() {} + } + } +} + new_target: { input: { new.target; @@ -444,3 +476,83 @@ generators_yield_assign: { } expect_exact: "function*fn(){var x={};x.prop=yield 5}" } + +generator_yield_undefined: { + input: { + function* fn() { + yield; + } + } + expect_exact: "function*fn(){yield}" +} + +yield_optimize_expression: { + options = { + } + input: { + function* f1() { yield; } + function* f2() { yield undefined; } + function* f3() { yield null; } + function* f4() { yield* undefined; } + } + expect: { + function* f1() { yield } + function* f2() { yield; } + function* f3() { yield null; } + function* f4() { yield* void 0; } + } +} + +yield_statements: { + input: { + function* fn() { + var a = (yield 1) + (yield 2); + var b = (yield 3) === (yield 4); + var c = (yield 5) << (yield 6); + var d = yield 7; + var e = (yield 8) ? yield 9 : yield 10; + var f = -(yield 11); + } + } + expect_exact: "function*fn(){var a=(yield 1)+(yield 2);var b=(yield 3)===(yield 4);var c=(yield 5)<<(yield 6);var d=yield 7;var e=(yield 8)?yield 9:yield 10;var f=-(yield 11)}" +} + +yield_as_identifier_in_function_in_generator: { + input: { + var g = function*() { + function h() { + yield = 1; + } + }; + } + expect: { + var g = function*() { + function h() { + yield = 1; + } + }; + } +} + +yield_before_punctuators: { + input: { + iter = (function*() { + assignmentResult = [ x = yield ] = value; + })(); + function* g1() { (yield) } + function* g2() { [yield] } + function* g3() { return {yield} } // Added return to avoid {} drop + function* g4() { yield, yield; } + function* g5() { (yield) ? yield : yield; } + } + expect: { + iter = (function*() { + assignmentResult = [ x = yield ] = value; + })(); + function* g1() { (yield) } + function* g2() { [yield] } + function* g3() { return {yield} } + function* g4() { yield, yield; } + function* g5() { (yield) ? yield : yield; } + } +} \ No newline at end of file diff --git a/test/mocha/object.js b/test/mocha/object.js new file mode 100644 index 00000000..a9373747 --- /dev/null +++ b/test/mocha/object.js @@ -0,0 +1,20 @@ +var Uglify = require("../../"); +var assert = require("assert"); + +describe("Object", function() { + it ("Should allow objects to have a methodDefinition as property", function() { + var code = "var a = {test() {return true;}}"; + assert.equal(Uglify.minify(code, {fromString: true}).code, "var a={test(){return!0}};"); + }); + + it ("Should not allow objects to use static keywords like in classes", function() { + var code = "{static test() {}}"; + var parse = function() { + Uglify.parse(code); + } + var expect = function(e) { + return e instanceof Uglify.JS_Parse_Error; + } + assert.throws(parse, expect); + }); +}); \ No newline at end of file diff --git a/test/mocha/yield.js b/test/mocha/yield.js new file mode 100644 index 00000000..cd4cce52 --- /dev/null +++ b/test/mocha/yield.js @@ -0,0 +1,55 @@ +var UglifyJS = require("../../"); +var assert = require("assert"); + +describe("Yield", function() { + it("Should not delete statements after yield", function() { + var js = 'function *foo(bar) { yield 1; yield 2; return 3; }'; + var result = UglifyJS.minify(js, {fromString: true}); + assert.strictEqual(result.code, 'function*foo(e){return yield 1,yield 2,3}'); + }); + + it("Should not allow yield as labelIdentifier within generators", function() { + var js = "function* g() {yield: 1}" + var test = function() { + UglifyJS.parse(js); + } + var expect = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Yield cannot be used as label inside generators"; + } + assert.throws(test, expect); + }); + + it("Should not allow yield* followed by a newline in generators", function() { + var js = "function* test() {yield*\n123;}"; + var test = function() { + UglifyJS.parse(js); + } + var expect = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Unexpected token: operator (*)"; + } + assert.throws(test, expect); + }); + + it("Should be able to compress its expression", function() { + assert.strictEqual( + UglifyJS.minify("function *f() { yield 3-4; }", {fromString: true, compress: true}).code, + "function*f(){yield-1}" + ); + }); + + it("Should keep undefined after yield without compression if found in ast", function() { + assert.strictEqual( + UglifyJS.minify("function *f() { yield undefined; yield; yield* undefined; yield void 0}", {fromString: true, compress: false}).code, + "function*f(){yield undefined;yield;yield*undefined;yield void 0}" + ); + }); + + it("Should be able to drop undefined after yield if necessary with compression", function() { + assert.strictEqual( + UglifyJS.minify("function *f() { yield undefined; yield; yield* undefined; yield void 0}", {fromString: true, compress: true}).code, + "function*f(){yield,yield,yield*void 0,yield}" + ); + }); +}); \ No newline at end of file diff --git a/test/parser.js b/test/parser.js index 5f6f62f5..c91f1e7c 100644 --- a/test/parser.js +++ b/test/parser.js @@ -128,7 +128,7 @@ module.exports = function () { }); var generators_yield_def = UglifyJS.parse('function* fn() {\nyield remote();\}').body[0].body[0]; - ok.equal(generators_yield_def.body.operator, 'yield'); + ok.strictEqual(generators_yield_def.body.is_star, false); } // Run standalone From b0555a123a208b24f14d03add0d992a846c1dd89 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 11 Jun 2016 21:41:16 +0200 Subject: [PATCH 080/121] Fix newline handling after yield YieldExpressions can only be defined as: * `yield` * `yield` [no nlb] AssignmentExpression * `yield` [no nlb] `*` AssignmentExpression --- lib/parse.js | 24 +++++++++++++++--------- test/mocha/yield.js | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 14da6b01..da8cb506 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1174,17 +1174,23 @@ function parse($TEXT, options) { var has_expression = true; var tmp; - // Get expression behind yield, default to value `undefined` stored as `null` in ast - // Expression must start on same line, yield* has a mandatory expression - if (is("operator", "*")) { - star = true; - next(); - if (S.token.nlb) { - unexpected(S.prev); - } - } else if (can_insert_semicolon() || + // Attempt to get expression or star (and then the mandatory expression) + // behind yield on the same line. + // + // If nothing follows on the same line of the yieldExpression, + // it should default to the value `undefined` for yield to return. + // In that case, the `undefined` stored as `null` in ast. + // + // Note 1: It isn't allowed for yield* to close without an expression + // Note 2: If there is a nlb between yield and star, it is interpret as + // yield * + if (can_insert_semicolon() || (is("punc") && PUNC_AFTER_EXPRESSION(S.token.value))) { has_expression = false; + + } else if (is("operator", "*")) { + star = true; + next(); } return new AST_Yield({ diff --git a/test/mocha/yield.js b/test/mocha/yield.js index cd4cce52..b105ef60 100644 --- a/test/mocha/yield.js +++ b/test/mocha/yield.js @@ -20,8 +20,20 @@ describe("Yield", function() { assert.throws(test, expect); }); - it("Should not allow yield* followed by a newline in generators", function() { - var js = "function* test() {yield*\n123;}"; + it("Should not allow yield* followed by a semicolon in generators", function() { + var js = "function* test() {yield*\n;}"; + var test = function() { + UglifyJS.parse(js); + } + var expect = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Unexpected token: punc (;)"; + } + assert.throws(test, expect); + }); + + it("Should not allow yield with next token star on next line", function() { + var js = "function* test() {yield\n*123;}"; var test = function() { UglifyJS.parse(js); } From f9cab7ad61e16853ed342f88d8469d8cdc3f2bd5 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 11 Jun 2016 01:40:45 +0200 Subject: [PATCH 081/121] Allow expand in array literals --- lib/ast.js | 8 ++--- lib/output.js | 2 +- lib/parse.js | 9 +++-- lib/transform.js | 4 +++ test/compress/arrays.js | 73 +++++++++++++++++++++++++++++++++++++++++ test/parser.js | 2 +- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index dd5c6b8c..54111e58 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -371,15 +371,15 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); -var AST_Expansion = DEFNODE("Expansion", "symbol", { +var AST_Expansion = DEFNODE("Expansion", "expression", { $documentation: "An expandible argument, such as ...rest, a splat, such as [1,2,...all], or an expansion in a variable declaration, such as var [first, ...rest] = list", $propdoc: { - symbol: "AST_Symbol the thing to be expanded" + expression: "AST_Symbol the thing to be expanded" }, _walk: function(visitor) { var self = this; return visitor._visit(this, function(){ - self.symbol.walk(visitor); + self.expression.walk(visitor); }); } }); @@ -516,7 +516,7 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { out.push(node); } if (node instanceof AST_Expansion) { - out.push(node.symbol); + out.push(node.expression); } })); return out; diff --git a/lib/output.js b/lib/output.js index 09e0d776..99cd63f2 100644 --- a/lib/output.js +++ b/lib/output.js @@ -657,7 +657,7 @@ function OutputStream(options) { DEFPRINT(AST_Expansion, function (self, output) { output.print('...'); - self.symbol.print(output); + self.expression.print(output); }); DEFPRINT(AST_Destructuring, function (self, output) { diff --git a/lib/parse.js b/lib/parse.js index 14cdff77..9f7193ce 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1166,7 +1166,7 @@ function parse($TEXT, options) { next(); a.push(new AST_Expansion({ start: prev(), - symbol: as_symbol(AST_SymbolFunarg), + expression: as_symbol(AST_SymbolFunarg), end: S.token, })); } else { @@ -1385,7 +1385,7 @@ function parse($TEXT, options) { var symbol = _make_symbol(sym_type); children.push(new AST_Expansion({ start: prev(), - symbol: symbol, + expression: symbol, end: S.token })); next(); @@ -1586,6 +1586,9 @@ function parse($TEXT, options) { if (allow_trailing_comma && is("punc", closing)) break; if (is("punc", ",") && allow_empty) { a.push(new AST_Hole({ start: S.token, end: S.token })); + } else if (is("expand", "...")) { + next(); + a.push(new AST_Expansion({start: S.token, expression: expression(),end: S.token})); } else { a.push(expression(false)); } @@ -1956,7 +1959,7 @@ function parse($TEXT, options) { next(); args.push(new AST_Expansion({ start: prev(), - symbol: as_symbol(AST_SymbolFunarg) + expression: as_symbol(AST_SymbolFunarg) })); } else { args.push(expression(false)); diff --git a/lib/transform.js b/lib/transform.js index 6e3a9d96..ae839417 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -227,4 +227,8 @@ TreeTransformer.prototype = new TreeWalker; self.value = self.value.transform(tw); }); + _(AST_Expansion, function(self, tw){ + self.expression = self.expression.transform(tw); + }); + })(); diff --git a/test/compress/arrays.js b/test/compress/arrays.js index e636347f..bc573083 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -72,3 +72,76 @@ constant_join_2: { var f = "strstr" + variable + "foobarmoo" + foo; } } + +spread_with_variable_as_last_element: { + input: { + var values = [4, 5, 6]; + var a = [1, 2, 3, ...values]; + } + expect: { + var values = [4, 5, 6]; + var a = [1, 2, 3, ...values]; + } +} + +spread_with_variable_in_middle: { + input: { + var values = [4, 5, 6]; + var a = [1, 2, 3, ...values, 7,,,]; + } + expect: { + var values = [4, 5, 6]; + var a = [1, 2, 3, ...values, 7,,,]; + } +} + +spread_with_variable_at_front: { + input: { + var values = [1, 2, 3]; + var a = [...values, 4, 5, 6]; + } + expect: { + var values = [1, 2, 3]; + var a = [...values, 4, 5, 6]; + } +} + +spread_with_variable_at_front_after_elisions: { + input: { + var values = [1, 2, 3]; + var a = [,,,...values, 4, 5, 6]; + } + expect: { + var values = [1, 2, 3]; + var a = [,,,...values, 4, 5, 6]; + } +} + +spread_with_array_at_end: { + input: { + var a = [1, 2, ...[4, 5, 6]]; + } + expect: { + var a = [1, 2, ...[4, 5, 6]]; + } +} + +spread_with_logical_expression_at_end: { + options = { evaluate: true } + input: { + var a = [1, 2, 3, ...[2+2]] + } + expect: { + var a = [1, 2, 3, ...[4]] + } +} + +spread_with_logical_expression_at_middle: { + options = { evaluate: true } + input: { + var a = [1, 1, ...[1+1, 1+2, 2+3], 8] + } + expect: { + var a = [1, 1, ...[2, 3, 5], 8] + } +} \ No newline at end of file diff --git a/test/parser.js b/test/parser.js index c91f1e7c..84f8755c 100644 --- a/test/parser.js +++ b/test/parser.js @@ -117,7 +117,7 @@ module.exports = function () { ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); - ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); + ok.equal(expanding_def.name.names[1].expression.TYPE, 'SymbolVar'); // generators var generators_def = UglifyJS.parse('function* fn() {}').body[0]; From 6b03b800b3d77506f75ed3c789dc0d40ea140149 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 12 Jun 2016 22:40:07 +0200 Subject: [PATCH 082/121] Only last parameter between parentheses can have spread --- lib/parse.js | 4 ++ test/compress/arrow.js | 85 ++++++++++++++++++++++++++++++++++++++++++ test/mocha/arrow.js | 25 +++++++++++++ test/mocha/class.js | 26 +++++++++++++ test/mocha/function.js | 30 +++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 test/compress/arrow.js create mode 100644 test/mocha/arrow.js create mode 100644 test/mocha/class.js create mode 100644 test/mocha/function.js diff --git a/lib/parse.js b/lib/parse.js index 9f7193ce..6298419b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1163,12 +1163,16 @@ function parse($TEXT, options) { while (!is("punc", ")")) { if (first) first = false; else expect(","); if (is("expand", "...")) { + var spread_token = S.token; next(); a.push(new AST_Expansion({ start: prev(), expression: as_symbol(AST_SymbolFunarg), end: S.token, })); + if (!is("punc", ")")) { + unexpected(spread_token); + } } else { a.push(expression(false)); } diff --git a/test/compress/arrow.js b/test/compress/arrow.js new file mode 100644 index 00000000..c5838537 --- /dev/null +++ b/test/compress/arrow.js @@ -0,0 +1,85 @@ +arrow_functions_without_body: { + input: { + var a1 = () => 42; + var a2 = (p) => p; + var a3 = p => p; + var a4 = (...p) => p; + var a5 = (b, c) => b + c; + var a6 = (b, ...c) => b + c[0]; + var a7 = (...b) => b.join(); + } + expect: { + var a1 = () => 42; + var a2 = (p) => p; + var a3 = p => p; + var a4 = (...p) => p; + var a5 = (b, c) => b + c; + var a6 = (b, ...c) => b + c[0]; + var a7 = (...b) => b.join(); + } +} + +arrow_functions_with_body: { + input: { + var a1 = () => { + var a = 42 * Math.random(); + return a; + }; + var a2 = (p) => { + var a = Math.random() * p; + return a; + }; + var a3 = p => { + var a = Math.random() * p; + return a; + }; + var a4 = (...p) => { + var a = Math.random() * p; + return a; + }; + var a5 = (b, c) => { + var result = b * c + b / c; + return result + }; + var a6 = (b, ...c) => { + var result = b; + for (var i = 0; i < c.length; i++) + result += c[i]; + return result + }; + var a7 = (...b) => { + b.join(); + } + } + expect: { + var a1 = () => { + var a = 42 * Math.random(); + return a; + }; + var a2 = (p) => { + var a = Math.random() * p; + return a; + }; + var a3 = p => { + var a = Math.random() * p; + return a; + }; + var a4 = (...p) => { + var a = Math.random() * p; + return a; + }; + var a5 = (b, c) => { + var result = b * c + b / c; + return result + }; + var a6 = (b, ...c) => { + var result = b; + for (var i = 0; i < c.length; i++) + result += c[i]; + return result + }; + var a7 = (...b) => { + b.join(); + }; + } +} \ No newline at end of file diff --git a/test/mocha/arrow.js b/test/mocha/arrow.js new file mode 100644 index 00000000..19bbb429 --- /dev/null +++ b/test/mocha/arrow.js @@ -0,0 +1,25 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Arrow functions", function() { + it("Should not accept spread tokens on non-last parameters or without arguments parentheses", function() { + var tests = [ + "var a = ...a => {return a.join()}", + "var b = (a, ...b, c) => { return a + b.join() + c}", + "var c = (...a, b) => a.join()" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "Unexpected token: expand (...)"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); +}); \ No newline at end of file diff --git a/test/mocha/class.js b/test/mocha/class.js new file mode 100644 index 00000000..e0b1fbd1 --- /dev/null +++ b/test/mocha/class.js @@ -0,0 +1,26 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Class", function() { + it("Should not accept spread on non-last parameters in methods", function() { + var tests = [ + "class foo { bar(...a, b) { return a.join(b) } }", + "class foo { bar(a, b, ...c, d) { return c.join(a + b) + d } }", + "class foo { *bar(...a, b) { return a.join(b) } }", + "class foo { *bar(a, b, ...c, d) { return c.join(a + b) + d } }" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "Unexpected token: expand (...)"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); +}); \ No newline at end of file diff --git a/test/mocha/function.js b/test/mocha/function.js new file mode 100644 index 00000000..cdfb5cee --- /dev/null +++ b/test/mocha/function.js @@ -0,0 +1,30 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Function", function() { + it("Should not accept spread on non-last parameters", function() { + var tests = [ + "var a = function(...a, b) { return a.join(b) }", + "var b = function(a, b, ...c, d) { return c.join(a + b) + d }", + "function foo(...a, b) { return a.join(b) }", + "function bar(a, b, ...c, d) { return c.join(a + b) + d }", + "var a = function*(...a, b) { return a.join(b) }", + "var b = function*(a, b, ...c, d) { return c.join(a + b) + d }", + "function* foo(...a, b) { return a.join(b) }", + "function* bar(a, b, ...c, d) { return c.join(a + b) + d }" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "Unexpected token: expand (...)"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); +}); \ No newline at end of file From ca04508cd16f2ca8c6b6b7386df81b6afa7358f1 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 17 Jun 2016 20:39:03 +0200 Subject: [PATCH 083/121] Restrict yield outside generators in strict mode * Move some yield/generic tests from compress/harmony.js to compress/yield.js * Adjust error messages to conform ecmascript standards --- lib/parse.js | 26 ++++--- test/compress/harmony.js | 106 ----------------------------- test/compress/yield.js | 142 +++++++++++++++++++++++++++++++++++++++ test/mocha/arrow.js | 4 +- test/mocha/class.js | 4 +- test/mocha/function.js | 4 +- test/mocha/yield.js | 45 ++++++++++++- 7 files changed, 208 insertions(+), 123 deletions(-) create mode 100644 test/compress/yield.js diff --git a/lib/parse.js b/lib/parse.js index 6298419b..f13d9bc4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -795,14 +795,14 @@ function parse($TEXT, options) { function unexpected(token) { if (token == null) token = S.token; - token_error(token, "Unexpected token: " + token.type + " (" + token.value + ")"); + token_error(token, "SyntaxError: Unexpected token: " + token.type + " (" + token.value + ")"); }; function expect_token(type, val) { if (is(type, val)) { return next(); } - token_error(S.token, "Unexpected token " + S.token.type + " «" + S.token.value + "»" + ", expected " + type + " «" + val + "»"); + token_error(S.token, "SyntaxError: Unexpected token " + S.token.type + " «" + S.token.value + "»" + ", expected " + type + " «" + val + "»"); }; function expect(punc) { return expect_token("punc", punc); }; @@ -993,7 +993,7 @@ function parse($TEXT, options) { var label = as_symbol(AST_Label); if (label.name === "yield" && is_in_generator()) { // Ecma-262, 12.1.1 Static Semantics: Early Errors - croak("Yield cannot be used as label inside generators"); + token_error(S.prev, "SyntaxError: Yield cannot be used as label inside generators"); } if (find_if(function(l){ return l.name == label.name }, S.labels)) { // ECMA-262, 12.12: An ECMAScript program is considered @@ -1873,9 +1873,12 @@ function parse($TEXT, options) { expect("]"); return ex; } else unexpected(); - case "num": - case "string": case "name": + if (tmp.value === "yield" && S.input.has_directive("use strict") && !is_in_generator()) { + token_error(tmp, "SyntaxError: Unexpected yield identifier inside strict mode"); + } + case "string": + case "num": case "operator": case "keyword": case "atom": @@ -1915,6 +1918,9 @@ function parse($TEXT, options) { if (!noerror) croak("Name expected"); return null; } + if (is("name", "yield") && S.input.has_directive("use strict")) { + token_error(S.prev, "SyntaxError: Unexpected yield identifier inside strict mode"); + } var sym = _make_symbol(type); next(); return sym; @@ -2050,9 +2056,13 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; - if (start.type == "name" && start.value == "yield" && is_in_generator()) { - next(); - return _yield_expression(); + if (start.type == "name" && start.value == "yield") { + if (is_in_generator()) { + next(); + return _yield_expression(); + } else if (S.input.has_directive("use strict")) { + token_error(S.token, "SyntaxError: Unexpected yield identifier inside strict mode") + } } if (start.type == "punc" && start.value == "(" && peek().value == ")") { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 50fb47af..8093384b 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -449,110 +449,4 @@ regression_cannot_use_of: { x.of; foo(); /* Label statement missing? No prob. */ } -} - -generators: { - input: { - function* fn() {}; - } - expect_exact: "function*fn(){}" -} - -generators_yield: { - input: { - function* fn() { - yield remote(); - } - } - expect_exact: "function*fn(){yield remote()}" -} - -generators_yield_assign: { - input: { - function* fn() { - var x = {}; - x.prop = yield 5; - } - } - expect_exact: "function*fn(){var x={};x.prop=yield 5}" -} - -generator_yield_undefined: { - input: { - function* fn() { - yield; - } - } - expect_exact: "function*fn(){yield}" -} - -yield_optimize_expression: { - options = { - } - input: { - function* f1() { yield; } - function* f2() { yield undefined; } - function* f3() { yield null; } - function* f4() { yield* undefined; } - } - expect: { - function* f1() { yield } - function* f2() { yield; } - function* f3() { yield null; } - function* f4() { yield* void 0; } - } -} - -yield_statements: { - input: { - function* fn() { - var a = (yield 1) + (yield 2); - var b = (yield 3) === (yield 4); - var c = (yield 5) << (yield 6); - var d = yield 7; - var e = (yield 8) ? yield 9 : yield 10; - var f = -(yield 11); - } - } - expect_exact: "function*fn(){var a=(yield 1)+(yield 2);var b=(yield 3)===(yield 4);var c=(yield 5)<<(yield 6);var d=yield 7;var e=(yield 8)?yield 9:yield 10;var f=-(yield 11)}" -} - -yield_as_identifier_in_function_in_generator: { - input: { - var g = function*() { - function h() { - yield = 1; - } - }; - } - expect: { - var g = function*() { - function h() { - yield = 1; - } - }; - } -} - -yield_before_punctuators: { - input: { - iter = (function*() { - assignmentResult = [ x = yield ] = value; - })(); - function* g1() { (yield) } - function* g2() { [yield] } - function* g3() { return {yield} } // Added return to avoid {} drop - function* g4() { yield, yield; } - function* g5() { (yield) ? yield : yield; } - } - expect: { - iter = (function*() { - assignmentResult = [ x = yield ] = value; - })(); - function* g1() { (yield) } - function* g2() { [yield] } - function* g3() { return {yield} } - function* g4() { yield, yield; } - function* g5() { (yield) ? yield : yield; } - } } \ No newline at end of file diff --git a/test/compress/yield.js b/test/compress/yield.js new file mode 100644 index 00000000..9fe9f6c8 --- /dev/null +++ b/test/compress/yield.js @@ -0,0 +1,142 @@ +generators: { + input: { + function* fn() {}; + } + expect_exact: "function*fn(){}" +} + +generators_yield: { + input: { + function* fn() { + yield remote(); + } + } + expect_exact: "function*fn(){yield remote()}" +} + +generators_yield_assign: { + input: { + function* fn() { + var x = {}; + x.prop = yield 5; + } + } + expect_exact: "function*fn(){var x={};x.prop=yield 5}" +} + +generator_yield_undefined: { + input: { + function* fn() { + yield; + } + } + expect_exact: "function*fn(){yield}" +} + +yield_optimize_expression: { + options = { + } + input: { + function* f1() { yield; } + function* f2() { yield undefined; } + function* f3() { yield null; } + function* f4() { yield* undefined; } + } + expect: { + function* f1() { yield } + function* f2() { yield; } + function* f3() { yield null; } + function* f4() { yield* void 0; } + } +} + +yield_statements: { + input: { + function* fn() { + var a = (yield 1) + (yield 2); + var b = (yield 3) === (yield 4); + var c = (yield 5) << (yield 6); + var d = yield 7; + var e = (yield 8) ? yield 9 : yield 10; + var f = -(yield 11); + } + } + expect_exact: "function*fn(){var a=(yield 1)+(yield 2);var b=(yield 3)===(yield 4);var c=(yield 5)<<(yield 6);var d=yield 7;var e=(yield 8)?yield 9:yield 10;var f=-(yield 11)}" +} + +yield_as_identifier_in_function_in_generator: { + input: { + var g = function*() { + function h() { + yield = 1; + } + }; + } + expect: { + var g = function*() { + function h() { + yield = 1; + } + }; + } +} + +yield_before_punctuators: { + input: { + iter = (function*() { + assignmentResult = [ x = yield ] = value; + })(); + function* g1() { (yield) } + function* g2() { [yield] } + function* g3() { return {yield} } // Added return to avoid {} drop + function* g4() { yield, yield; } + function* g5() { (yield) ? yield : yield; } + } + expect: { + iter = (function*() { + assignmentResult = [ x = yield ] = value; + })(); + function* g1() { (yield) } + function* g2() { [yield] } + function* g3() { return {yield} } + function* g4() { yield, yield; } + function* g5() { (yield) ? yield : yield; } + } +} + +yield_as_identifier_outside_strict_mode: { + input: { + import yield from "bar"; + yield = 123; + while (true) { + yield: + for(;;) break yield; + + foo(); + } + while (true) + yield: for(;;) continue yield; + function yield(){} + function foo(...yield){} + try { new Error("") } catch (yield) {} + var yield = "foo"; + class yield {} + } + expect: { + import yield from "bar"; + yield = 123; + while (true) { + yield: + for(;;) break yield; + + foo(); + } + while (true) + yield: for(;;) continue yield; + function yield(){} + function foo(...yield){} + try { new Error("") } catch (yield) {} + var yield = "foo"; + class yield {} + } +} \ No newline at end of file diff --git a/test/mocha/arrow.js b/test/mocha/arrow.js index 19bbb429..a02286c2 100644 --- a/test/mocha/arrow.js +++ b/test/mocha/arrow.js @@ -15,11 +15,11 @@ describe("Arrow functions", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "Unexpected token: expand (...)"; + e.message === "SyntaxError: Unexpected token: expand (...)"; } for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } }); -}); \ No newline at end of file +}); diff --git a/test/mocha/class.js b/test/mocha/class.js index e0b1fbd1..0df67e6a 100644 --- a/test/mocha/class.js +++ b/test/mocha/class.js @@ -16,11 +16,11 @@ describe("Class", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "Unexpected token: expand (...)"; + e.message === "SyntaxError: Unexpected token: expand (...)"; } for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } }); -}); \ No newline at end of file +}); diff --git a/test/mocha/function.js b/test/mocha/function.js index cdfb5cee..34fc70b3 100644 --- a/test/mocha/function.js +++ b/test/mocha/function.js @@ -20,11 +20,11 @@ describe("Function", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "Unexpected token: expand (...)"; + e.message === "SyntaxError: Unexpected token: expand (...)"; } for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } }); -}); \ No newline at end of file +}); diff --git a/test/mocha/yield.js b/test/mocha/yield.js index b105ef60..1211ea9c 100644 --- a/test/mocha/yield.js +++ b/test/mocha/yield.js @@ -15,7 +15,7 @@ describe("Yield", function() { } var expect = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "Yield cannot be used as label inside generators"; + e.message === "SyntaxError: Yield cannot be used as label inside generators"; } assert.throws(test, expect); }); @@ -27,7 +27,7 @@ describe("Yield", function() { } var expect = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "Unexpected token: punc (;)"; + e.message === "SyntaxError: Unexpected token: punc (;)"; } assert.throws(test, expect); }); @@ -39,7 +39,7 @@ describe("Yield", function() { } var expect = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "Unexpected token: operator (*)"; + e.message === "SyntaxError: Unexpected token: operator (*)"; } assert.throws(test, expect); }); @@ -64,4 +64,43 @@ describe("Yield", function() { "function*f(){yield,yield,yield*void 0,yield}" ); }); + + it("Should not allow yield to be used as symbol, identifier or property outside generators in strict mode", function() { + var tests = [ + // Fail as as_symbol + '"use strict"; import yield from "bar";', + '"use strict"; yield = 123;', + '"use strict"; yield: "123";', + '"use strict"; for(;;){break yield;}', + '"use strict"; for(;;){continue yield;}', + '"use strict"; function yield(){}', + '"use strict"; function foo(...yield){}', + '"use strict"; try { new Error("")} catch (yield) {}', + '"use strict"; var yield = "foo";', + '"use strict"; class yield {}', + + // Fail as maybe_assign + '"use strict"; var foo = yield;', + '"use strict"; var foo = bar = yield', + + // Fail as as_property_name + '"use strict"; var foo = {yield};', + '"use strict"; var bar = {yield: "foo"};' + ]; + + var fail = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "SyntaxError: Unexpected yield identifier inside strict mode"; + } + + var test = function(input) { + return function() { + UglifyJS.parse(input); + } + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), fail, tests[i]); + } + }); }); \ No newline at end of file From dda58244b6dcc6270386ac40e2264e3d10a7f38d Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Mon, 20 Jun 2016 13:59:26 +0200 Subject: [PATCH 084/121] Fixes to prevent failing tests after merging master * Add missing quote properties to AST_ObjectKeyVal * Avoid test results being interpret as directives --- lib/parse.js | 2 ++ test/compress/harmony.js | 4 ++-- test/compress/issue-1001.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index f13d9bc4..4e15f7d6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1654,6 +1654,7 @@ function parse($TEXT, options) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectKeyVal({ start: start, + quote: start.quote, end: start, key: name, value: new AST_SymbolRef({ @@ -1667,6 +1668,7 @@ function parse($TEXT, options) { expect(":"); a.push(new AST_ObjectKeyVal({ start : start, + quote : start.quote, key : name, value : expression(false), end : prev() diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 8093384b..9d17f4f4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -73,9 +73,9 @@ typeof_arrow_functions: { evaluate: true } input: { - typeof (x) => null; + var foo = typeof (x) => null; } - expect_exact: "\"function\";" + expect_exact: "var foo=\"function\";" } template_strings: { diff --git a/test/compress/issue-1001.js b/test/compress/issue-1001.js index 89adf81c..432e953c 100644 --- a/test/compress/issue-1001.js +++ b/test/compress/issue-1001.js @@ -1,8 +1,8 @@ parenthesis_strings_in_parenthesis: { input: { - ('('); + var foo = ('('); a(')'); } - expect_exact: '"(";a(")");' + expect_exact: 'var foo="(";a(")");' } From 6eaeb19a4a5ecbb890fc025591bb24d466d3efe0 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 18 Jun 2016 00:25:18 +0200 Subject: [PATCH 085/121] Add exponentiation operator --- lib/compress.js | 1 + lib/output.js | 8 +++- lib/parse.js | 13 ++++-- test/compress/evaluate.js | 88 +++++++++++++++++++++++++++++++++++++ test/compress/expression.js | 52 ++++++++++++++++++++++ test/mocha/expression.js | 32 ++++++++++++++ 6 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 test/compress/expression.js create mode 100644 test/mocha/expression.js diff --git a/lib/compress.js b/lib/compress.js index 1c99dad3..0fbed00a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1088,6 +1088,7 @@ merge(Compressor.prototype, { case "^" : return ev(left, c) ^ ev(right, c); case "+" : return ev(left, c) + ev(right, c); case "*" : return ev(left, c) * ev(right, c); + case "**" : return Math.pow(ev(left, c), ev(right, c)); case "/" : return ev(left, c) / ev(right, c); case "%" : return ev(left, c) % ev(right, c); case "-" : return ev(left, c) - ev(right, c); diff --git a/lib/output.js b/lib/output.js index a048ce1a..1fc541a6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -538,7 +538,13 @@ function OutputStream(options) { PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this - || p instanceof AST_New; + || p instanceof AST_New + || p instanceof AST_Binary + && p.operator === "**" + && this instanceof AST_UnaryPrefix + && p.left === this + && this.operator !== "++" + && this.operator !== "--"; }); PARENS(AST_Seq, function(output){ diff --git a/lib/parse.js b/lib/parse.js index 4132340a..f1fbf9e8 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -80,6 +80,7 @@ var OPERATORS = makePredicate([ "|", "^", "*", + "**", "/", "%", ">>", @@ -99,6 +100,7 @@ var OPERATORS = makePredicate([ "-=", "/=", "*=", + "**=", "%=", ">>=", "<<=", @@ -694,7 +696,7 @@ var UNARY_PREFIX = makePredicate([ var UNARY_POSTFIX = makePredicate([ "--", "++" ]); -var ASSIGNMENT = makePredicate([ "=", "+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=" ]); +var ASSIGNMENT = makePredicate([ "=", "+=", "-=", "/=", "*=", "**=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=" ]); var PRECEDENCE = (function(a, ret){ for (var i = 0; i < a.length; ++i) { @@ -715,7 +717,8 @@ var PRECEDENCE = (function(a, ret){ ["<", ">", "<=", ">=", "in", "instanceof"], [">>", "<<", ">>>"], ["+", "-"], - ["*", "/", "%"] + ["*", "/", "%"], + ["**"] ], {} ); @@ -2015,8 +2018,12 @@ function parse($TEXT, options) { var expr_op = function(left, min_prec, no_in) { var op = is("operator") ? S.token.value : null; if (op == "in" && no_in) op = null; + if (op == "**" && left instanceof AST_UnaryPrefix + && left.end === S.prev /* unary token in front not allowed, but allowed if prev is for example `)` */ + && left.operator !== "--" && left.operator !== "++") + unexpected(left.start); var prec = op != null ? PRECEDENCE[op] : null; - if (prec != null && prec > min_prec) { + if (prec != null && (prec > min_prec || (op === "**" && min_prec === prec))) { next(); var right = expr_op(maybe_unary(true), prec, no_in); return expr_op(new AST_Binary({ diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index d27582f3..f2658c55 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -37,3 +37,91 @@ positive_zero: { ); } } + +pow: { + options = { evaluate: true } + input: { + var a = 5 ** 3; + } + expect: { + var a = 125; + } +} + +pow_sequence: { + options = { + evaluate: true + } + input: { + var a = 2 ** 3 ** 2; + } + expect: { + var a = 512; + } +} + +pow_mixed: { + options = { + evaluate: true + } + input: { + var a = 5 + 2 ** 3 + 5; + var b = 5 * 3 ** 2; + var c = 5 ** 3 * 2; + var d = 5 ** +3; + } + expect: { + var a = 18; + var b = 45; + var c = 250; + var d = 125; + } +} + +pow_with_right_side_evaluating_to_unary: { + options = { + evaluate: true + } + input: { + var a = (4 - 7) ** foo; + var b = ++bar ** 3; + var c = --baz ** 2; + } + expect_exact: "var a=(-3)**foo;var b=++bar**3;var c=--baz**2;" +} + +pow_with_number_constants: { + options = { + evaluate: true + } + input: { + var a = 5 ** NaN; /* NaN exponent results to NaN */ + var b = 42 ** +0; /* +0 exponent results to NaN */ + var c = 42 ** -0; /* -0 exponent results to NaN */ + var d = NaN ** 1; /* NaN with non-zero exponent is NaN */ + var e = 2 ** Infinity; /* abs(base) > 1 with Infinity as exponent is Infinity */ + var f = 2 ** -Infinity; /* abs(base) > 1 with -Infinity as exponent is +0 */ + var g = (-7) ** (0.5); + var h = 2324334 ** 34343443; + var i = (-2324334) ** 34343443; + var j = 2 ** (-3); + var k = 2.0 ** -3; + var l = 2.0 ** (5 - 7); + var m = 3 ** -10; // Result will be 0.000016935087808430286, which is too long + } + expect: { + var a = NaN; + var b = 1; + var c = 1; + var d = NaN; + var e = Infinity; + var f = 0; + var g = NaN; + var h = Infinity; + var i = -Infinity; + var j = .125; + var k = .125; + var l = .25; + var m = 3 ** -10; + } +} diff --git a/test/compress/expression.js b/test/compress/expression.js new file mode 100644 index 00000000..a3fe1a5a --- /dev/null +++ b/test/compress/expression.js @@ -0,0 +1,52 @@ +pow: { + input: { + var a = 2 ** 7; + var b = 3; + b **= 2; + } + expect: { + var a = 2 ** 7; + var b = 3; + b **= 2; + } +} + +pow_with_number_constants: { + input: { + var a = 5 ** NaN; + var b = 42 ** +0; + var c = 42 ** -0; + var d = NaN ** 1; + var e = 2 ** Infinity; + var f = 2 ** -Infinity; + } + expect: { + var a = 5 ** NaN; + var b = 42 ** +0; + var c = 42 ** -0; + var d = NaN ** 1; + var e = 2 ** (1/0); + var f = 2 ** -(1/0); + } +} + +pow_with_parentheses: { + input: { + var g = (-7) ** (0.5); + var h = 2324334 ** 34343443; + var i = (-2324334) ** 34343443; + var j = 2 ** (-3); + var k = 2.0 ** -3; + var l = 2.0 ** (5 - 7); + } + expect_exact: "var g=(-7)**.5;var h=2324334**34343443;var i=(-2324334)**34343443;var j=2**-3;var k=2**-3;var l=2**(5-7);" +} + +pow_with_unary_between_brackets: { + input: { + var a = (-(+5)) ** 3; + } + expect: { + var a = (-+5)**3; + } +} diff --git a/test/mocha/expression.js b/test/mocha/expression.js new file mode 100644 index 00000000..178cff22 --- /dev/null +++ b/test/mocha/expression.js @@ -0,0 +1,32 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Expression", function() { + it("Should not allow the first exponentiation operator to be prefixed with an unary operator", function() { + var tests = [ + "+5 ** 3", + "-5 ** 3", + "~5 ** 3", + "!5 ** 3", + "void 5 ** 3", + "typeof 5 ** 3", + "delete 5 ** 3", + "var a = -(5) ** 3;" + ]; + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error && + /^SyntaxError: Unexpected token: operator \((?:[!+~-]|void|typeof|delete)\)/.test(e.message); + } + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail, tests[i]); + } + }); +}); From 07785d0003e0c1669f60e348decac85b07502cdb Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 25 Jun 2016 19:32:09 +0200 Subject: [PATCH 086/121] Throw error if new.target is like new.foo --- lib/parse.js | 2 +- test/mocha/new.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index f1fbf9e8..0cabd453 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1448,7 +1448,7 @@ function parse($TEXT, options) { expect_token("operator", "new"); if (is("punc", ".")) { next(); - expect_token("name"); + expect_token("name", "target"); return subscripts(new AST_NewTarget({ start : start, end : prev() diff --git a/test/mocha/new.js b/test/mocha/new.js index 083b9964..cc193d3d 100644 --- a/test/mocha/new.js +++ b/test/mocha/new.js @@ -85,4 +85,11 @@ describe("New", function() { ); } }); + + it("Should check target in new.target", function() { + assert.throws(function() {uglify.parse("new.blah")}, function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: Unexpected token name «blah», expected name «target»"; + }); + }); }); \ No newline at end of file From 63c432f4fa0137b8dbdacdf51fbaa89d30aed646 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 22 Jun 2016 17:12:23 +0200 Subject: [PATCH 087/121] Extend unicode support * Support \u{xxxx} syntax * Add support for surrogate pairs * Allow identifiers to have unicode escape sequence --- lib/output.js | 21 +++-- lib/parse.js | 176 ++++++++++++++++++++++++++------------- test/compress/unicode.js | 26 ++++++ test/mocha/unicode.js | 133 +++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 64 deletions(-) create mode 100644 test/mocha/unicode.js diff --git a/lib/output.js b/lib/output.js index 1fc541a6..8ba98b69 100644 --- a/lib/output.js +++ b/lib/output.js @@ -77,9 +77,12 @@ function OutputStream(options) { var OUTPUT = ""; function to_ascii(str, identifier) { - return str.replace(/[\u0000-\u001f\u007f-\uffff]/g, function(ch) { - var code = ch.charCodeAt(0).toString(16); - if (code.length <= 2 && !identifier) { + return str.replace(/[\ud800-\udbff][\udc00-\udfff]|[\u0000-\u001f\u007f-\uffff]/g, function(ch) { + var code = get_full_char_code(ch, 0).toString(16); + + if ((identifier && code.length === 1) || code.length > 4) { + return "\\u{" + code + "}"; + } else if (code.length <= 2 && !identifier) { while (code.length < 2) code = "0" + code; return "\\x" + code; } else { @@ -107,7 +110,7 @@ function OutputStream(options) { case "\u2029": return "\\u2029"; case "\ufeff": return "\\ufeff"; case "\0": - return /[0-7]/.test(str.charAt(i+1)) ? "\\x00" : "\\0"; + return /[0-7]/.test(get_full_char(str, i+1)) ? "\\x00" : "\\0"; } return s; }); @@ -158,7 +161,13 @@ function OutputStream(options) { var last = null; function last_char() { - return last.charAt(last.length - 1); + var char = last.charAt(last.length - 1); + + if (is_surrogate_pair_tail(char)) { + return last.charAt(last.length - 2) + char; + } + + return char; }; function maybe_newline() { @@ -170,7 +179,7 @@ function OutputStream(options) { function print(str) { str = String(str); - var ch = str.charAt(0); + var ch = get_full_char(str, 0); if (might_need_semicolon) { might_need_semicolon = false; diff --git a/lib/parse.js b/lib/parse.js index 0cabd453..fc7d3054 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -57,6 +57,7 @@ KEYWORDS_ATOM = makePredicate(KEYWORDS_ATOM); var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); +var RE_NUM_LITERAL = /[0-9a-f]/i; var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; var RE_OCT_NUMBER = /^0[0-7]+$/; var RE_ES6_OCT_NUMBER = /^0o[0-7]+$/i; @@ -124,58 +125,74 @@ var REGEXP_MODIFIERS = makePredicate(characters("gmsiy")); /* -----[ Tokenizer ]----- */ -// regexps adapted from http://xregexp.com/plugins/#unicode +// surrogate safe regexps adapted from https://github.com/mathiasbynens/unicode-8.0.0/tree/89b412d8a71ecca9ed593d9e9fa073ab64acfebe/Binary_Property var UNICODE = { - letter: new RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0-\\u08B2\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F8\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA7AD\\uA7B0\\uA7B1\\uA7F7-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB5F\\uAB64\\uAB65\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]"), - digit: new RegExp("[\\u0030-\\u0039\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE6-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29\\u1040-\\u1049\\u1090-\\u1099\\u17E0-\\u17E9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19D9\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\uA620-\\uA629\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19]"), - non_spacing_mark: new RegExp("[\\u0300-\\u036F\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065E\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0900-\\u0902\\u093C\\u0941-\\u0948\\u094D\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09BC\\u09C1-\\u09C4\\u09CD\\u09E2\\u09E3\\u0A01\\u0A02\\u0A3C\\u0A41\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81\\u0A82\\u0ABC\\u0AC1-\\u0AC5\\u0AC7\\u0AC8\\u0ACD\\u0AE2\\u0AE3\\u0B01\\u0B3C\\u0B3F\\u0B41-\\u0B44\\u0B4D\\u0B56\\u0B62\\u0B63\\u0B82\\u0BC0\\u0BCD\\u0C3E-\\u0C40\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0CBC\\u0CBF\\u0CC6\\u0CCC\\u0CCD\\u0CE2\\u0CE3\\u0D41-\\u0D44\\u0D4D\\u0D62\\u0D63\\u0DCA\\u0DD2-\\u0DD4\\u0DD6\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F71-\\u0F7E\\u0F80-\\u0F84\\u0F86\\u0F87\\u0F90-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102D-\\u1030\\u1032-\\u1037\\u1039\\u103A\\u103D\\u103E\\u1058\\u1059\\u105E-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108D\\u109D\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B7-\\u17BD\\u17C6\\u17C9-\\u17D3\\u17DD\\u180B-\\u180D\\u18A9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193B\\u1A17\\u1A18\\u1A56\\u1A58-\\u1A5E\\u1A60\\u1A62\\u1A65-\\u1A6C\\u1A73-\\u1A7C\\u1A7F\\u1B00-\\u1B03\\u1B34\\u1B36-\\u1B3A\\u1B3C\\u1B42\\u1B6B-\\u1B73\\u1B80\\u1B81\\u1BA2-\\u1BA5\\u1BA8\\u1BA9\\u1C2C-\\u1C33\\u1C36\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE0\\u1CE2-\\u1CE8\\u1CED\\u1DC0-\\u1DE6\\u1DFD-\\u1DFF\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F\\uA67C\\uA67D\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA825\\uA826\\uA8C4\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA951\\uA980-\\uA982\\uA9B3\\uA9B6-\\uA9B9\\uA9BC\\uAA29-\\uAA2E\\uAA31\\uAA32\\uAA35\\uAA36\\uAA43\\uAA4C\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uABE5\\uABE8\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE26]"), - space_combining_mark: new RegExp("[\\u0903\\u093E-\\u0940\\u0949-\\u094C\\u094E\\u0982\\u0983\\u09BE-\\u09C0\\u09C7\\u09C8\\u09CB\\u09CC\\u09D7\\u0A03\\u0A3E-\\u0A40\\u0A83\\u0ABE-\\u0AC0\\u0AC9\\u0ACB\\u0ACC\\u0B02\\u0B03\\u0B3E\\u0B40\\u0B47\\u0B48\\u0B4B\\u0B4C\\u0B57\\u0BBE\\u0BBF\\u0BC1\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCC\\u0BD7\\u0C01-\\u0C03\\u0C41-\\u0C44\\u0C82\\u0C83\\u0CBE\\u0CC0-\\u0CC4\\u0CC7\\u0CC8\\u0CCA\\u0CCB\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D40\\u0D46-\\u0D48\\u0D4A-\\u0D4C\\u0D57\\u0D82\\u0D83\\u0DCF-\\u0DD1\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0F3E\\u0F3F\\u0F7F\\u102B\\u102C\\u1031\\u1038\\u103B\\u103C\\u1056\\u1057\\u1062-\\u1064\\u1067-\\u106D\\u1083\\u1084\\u1087-\\u108C\\u108F\\u109A-\\u109C\\u17B6\\u17BE-\\u17C5\\u17C7\\u17C8\\u1923-\\u1926\\u1929-\\u192B\\u1930\\u1931\\u1933-\\u1938\\u19B0-\\u19C0\\u19C8\\u19C9\\u1A19-\\u1A1B\\u1A55\\u1A57\\u1A61\\u1A63\\u1A64\\u1A6D-\\u1A72\\u1B04\\u1B35\\u1B3B\\u1B3D-\\u1B41\\u1B43\\u1B44\\u1B82\\u1BA1\\u1BA6\\u1BA7\\u1BAA\\u1C24-\\u1C2B\\u1C34\\u1C35\\u1CE1\\u1CF2\\uA823\\uA824\\uA827\\uA880\\uA881\\uA8B4-\\uA8C3\\uA952\\uA953\\uA983\\uA9B4\\uA9B5\\uA9BA\\uA9BB\\uA9BD-\\uA9C0\\uAA2F\\uAA30\\uAA33\\uAA34\\uAA4D\\uAA7B\\uABE3\\uABE4\\uABE6\\uABE7\\uABE9\\uABEA\\uABEC]"), - connector_punctuation: new RegExp("[\\u005F\\u203F\\u2040\\u2054\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF3F]") + ID_Start: /[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/, + ID_Continue: /[0-9A-Z_a-z\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/, }; -function is_letter(code) { - return (code >= 97 && code <= 122) - || (code >= 65 && code <= 90) - || (code >= 0xaa && UNICODE.letter.test(String.fromCharCode(code))); -}; +function get_full_char(str, pos) { + var char = str.charAt(pos); + if (char >= "\ud800" && char <= "\udbff") { + return char + str.charAt(pos + 1); + } + return char; +} + +function get_full_char_code(str, pos) { + // https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + if (is_surrogate_pair_head(str.charAt(pos))) { + return 0x10000 + (str.charCodeAt(pos) - 0xd800 << 10) + str.charCodeAt(pos + 1) - 0xdc00; + } + return str.charCodeAt(pos); +} + +function from_char_code(code) { + // Based on https://github.com/mathiasbynens/String.fromCodePoint/blob/master/fromcodepoint.js + if (code > 0xFFFF) { + code -= 0x10000; + return (String.fromCharCode((code >> 10) + 0xD800) + + String.fromCharCode((code % 0x400) + 0xDC00)); + } + return String.fromCharCode(code); +} + +function is_surrogate_pair_head(code) { + if (typeof code === "string") + code = code.charCodeAt(0); + + return code >= 0xd800 && code <= 0xdbff; +} + +function is_surrogate_pair_tail(code) { + if (typeof code === "string") + code = code.charCodeAt(0); + return code >= 0xdc00 && code <= 0xdfff; +} function is_digit(code) { return code >= 48 && code <= 57; }; -function is_alphanumeric_char(code) { - return is_digit(code) || is_letter(code); -}; - -function is_unicode_digit(code) { - return UNICODE.digit.test(String.fromCharCode(code)); -} - -function is_unicode_combining_mark(ch) { - return UNICODE.non_spacing_mark.test(ch) || UNICODE.space_combining_mark.test(ch); -}; - -function is_unicode_connector_punctuation(ch) { - return UNICODE.connector_punctuation.test(ch); -}; - function is_identifier(name) { - return !RESERVED_WORDS(name) && /^[a-z_$][a-z0-9_$]*$/i.test(name); + if (typeof name !== "string" || RESERVED_WORDS(name)) + return false; + + return true; }; -function is_identifier_start(code) { - return code == 36 || code == 95 || is_letter(code); +function is_identifier_start(ch) { + var code = ch.charCodeAt(0); + return UNICODE.ID_Start.test(ch) || code == 36 || code == 95; }; function is_identifier_char(ch) { var code = ch.charCodeAt(0); - return is_identifier_start(code) - || is_digit(code) + return UNICODE.ID_Continue.test(ch) + || code == 36 + || code == 95 || code == 8204 // \u200c: zero-width non-joiner || code == 8205 // \u200d: zero-width joiner (in my ECMA-262 PDF, this is also 200c) - || is_unicode_combining_mark(ch) - || is_unicode_connector_punctuation(ch) - || is_unicode_digit(code) ; }; @@ -241,10 +258,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { directive_stack : [] }; - function peek() { return S.text.charAt(S.pos); }; + function peek() { return get_full_char(S.text, S.pos); }; function next(signal_eof, in_string) { - var ch = S.text.charAt(S.pos++); + var ch = get_full_char(S.text, S.pos++); if (signal_eof && !ch) throw EX_EOF; if ("\r\n\u2028\u2029".indexOf(ch) >= 0) { @@ -257,6 +274,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { ch = "\n"; } } else { + if (is_surrogate_pair_head(ch)) { + ++S.pos; + ++S.col; + } ++S.col; } return ch; @@ -347,6 +368,9 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { var num = read_while(function(ch, i){ var code = ch.charCodeAt(0); switch (code) { + case 98: case 66: // bB + return (has_x = true); // Can occur in hex sequence, don't return false yet + case 111: case 79: // oO case 120: case 88: // xX return has_x ? false : (has_x = true); case 101: case 69: // eE @@ -358,7 +382,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { case (after_e = false, 46): // . return (!has_dot && !has_x && !has_e) ? (has_dot = true) : false; } - return is_alphanumeric_char(code); + return RE_NUM_LITERAL.test(ch); }); if (prefix) num = prefix + num; var valid = parse_js_number(num); @@ -380,7 +404,22 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { case 102 : return "\f"; case 48 : return "\0"; case 120 : return String.fromCharCode(hex_bytes(2)); // \x - case 117 : return String.fromCharCode(hex_bytes(4)); // \u + case 117 : // \u + if (peek() == "{") { + next(true); + if (peek() === "}") + parse_error("SyntaxError: Expecting hex-character between {}"); + while (peek() == "0") next(true); // No significance + var result, length = find("}", true) - S.pos; + // Avoid 32 bit integer overflow (1 << 32 === 1) + // We know first character isn't 0 and thus out of range anyway + if (length > 6 || (result = hex_bytes(length)) > 0x10FFFF) { + parse_error("SyntaxError: Unicode reference out of bounce"); + } + next(true); + return from_char_code(result); + } + return String.fromCharCode(hex_bytes(4)); case 10 : return ""; // newline case 13 : // \r if (peek() == "\n") { // DOS newline @@ -396,7 +435,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { for (; n > 0; --n) { var digit = parseInt(next(true), 16); if (isNaN(digit)) - parse_error("Invalid hex-character pattern in string"); + parse_error("SyntaxError: Invalid hex-character pattern in string"); num = (num << 4) | digit; } return num; @@ -471,24 +510,45 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { }); function read_name() { - var backslash = false, name = "", ch, escaped = false, hex; + var name = "", ch, escaped = false, hex; + var read_escaped_identifier_char = function() { + escaped = true; + next(); + if (peek() !== "u") { + parse_error("SyntaxError: Expecting UnicodeEscapeSequence -- uXXXX or u{XXXX}"); + } + return read_escaped_char(); + } + + // Read first character (ID_Start) + if ((name = peek()) === "\\") { + name = read_escaped_identifier_char(); + if (!is_identifier_start(name)) { + parse_error("SyntaxError: First identifier char is an invalid identifier char"); + } + } else if (is_identifier_start(name)){ + next(); + } else { + return ""; + } + + // Read ID_Continue while ((ch = peek()) != null) { - if (!backslash) { - if (ch == "\\") escaped = backslash = true, next(); - else if (is_identifier_char(ch)) name += next(); - else break; - } - else { - if (ch != "u") parse_error("Expecting UnicodeEscapeSequence -- uXXXX"); - ch = read_escaped_char(); - if (!is_identifier_char(ch)) parse_error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); - name += ch; - backslash = false; + if ((ch = peek()) === "\\") { + ch = read_escaped_identifier_char(); + if (!is_identifier_char(ch)) { + parse_error("SyntaxError: Invalid escaped identifier char"); + } + } else { + if (!is_identifier_char(ch)) { + break; + } + next(); } + name += ch; } if (KEYWORDS(name) && escaped) { - hex = name.charCodeAt(0).toString(16).toUpperCase(); - name = "\\u" + "0000".substr(hex.length) + hex + name.slice(1); + parse_error("SyntaxError: Escaped characters are not allowed in keywords"); } return name; }; @@ -626,7 +686,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { if (is_digit(code)) return read_num(); if (PUNC_CHARS(ch)) return token("punc", next()); if (OPERATOR_CHARS(ch)) return read_operator(); - if (code == 92 || is_identifier_start(code)) return read_word(); + if (code == 92 || is_identifier_start(ch)) return read_word(); if (shebang) { if (S.pos == 0 && looking_at("#!")) { forward(2); @@ -636,7 +696,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } break; } - parse_error("Unexpected character '" + ch + "'"); + parse_error("SyntaxError: Unexpected character '" + ch + "'"); }; next_token.next = next; @@ -1222,8 +1282,8 @@ function parse($TEXT, options) { function _yield_expression() { // Previous token must be keyword yield and not be interpret as an identifier if (!is_in_generator()) { - throw new JS_Parse_Error("Unexpected yield expression outside generator function", - S.prev.file, S.prev.line, S.prev.col, S.prev.pos); + croak("SyntaxError: Unexpected yield expression outside generator function", + S.prev.line, S.prev.col, S.prev.pos); } var star = false; var has_expression = true; diff --git a/test/compress/unicode.js b/test/compress/unicode.js index 9fb9ab8c..9651c6df 100644 --- a/test/compress/unicode.js +++ b/test/compress/unicode.js @@ -15,3 +15,29 @@ unicode_parse_variables: { var l০ = 3; } } + +unicode_escaped_identifier: { + input: { + var \u{61} = "foo"; + var \u{10000} = "bar"; + } + expect_exact: 'var a="foo";var \u{10000}="bar";'; +} + +unicode_identifier_ascii_only: { + beautify = {ascii_only: true} + input: { + var \u{0061} = "hi"; + var bar = "h\u{0065}llo"; + var \u{10000} = "testing \u{101111}"; + } + expect_exact: 'var a="hi";var bar="hello";var \\u{10000}="testing \\u{101111}";' +} + +unicode_string_literals: { + beautify = {ascii_only: true} + input: { + var a = "6 length unicode character: \u{101111}"; + } + expect_exact: 'var a="6 length unicode character: \\u{101111}";' +} diff --git a/test/mocha/unicode.js b/test/mocha/unicode.js new file mode 100644 index 00000000..a7cc12dd --- /dev/null +++ b/test/mocha/unicode.js @@ -0,0 +1,133 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Unicode", function() { + it("Should not accept invalid code ranges in unicode escape", function() { + var tests = [ + "\\u{110000}", // A bit over the unicode range + "\\u{100000061} = 'foo'", // 32-bit overflow resulting in "a" + "\\u{fffffffffff}", // A bit too much over the unicode range + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: Unicode reference out of bounce"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail); + } + }); + + it("Should not accept invalid unicode sequences", function() { + var tests = [ + "var foo = '\\u-111'", + "var bar = '\\u{-1}'", + "var baz = '\\ugggg'" + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: Invalid hex-character pattern in string"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail); + } + }); + + it("Should throw error if escaped first identifier char is not part of ID_start", function() { + var tests = [ + 'var \\u{0} = "foo";', + 'var \\u{10ffff} = "bar";', + 'var \\u000a = "what\'s up";' + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: First identifier char is an invalid identifier char"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail); + } + }); + + it("Should throw error if escaped non-first identifier char is not part of ID_start", function() { + var tests = [ + 'var a\\u{0} = "foo";', + 'var a\\u{10ffff} = "bar";', + 'var z\\u000a = "what\'s up";' + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: Invalid escaped identifier char"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail); + } + }); + + it("Should throw error if identifier is a keyword with a escape sequences", function() { + var tests = [ + 'var \\u0069\\u006e = "foo"', // in + 'var \\u0076\\u0061\\u0072 = "bar"', // var + 'var \\u{66}\\u{6f}\\u{72} = "baz"', // for + 'var \\u0069\\u{66} = "foobar"' // if + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + } + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && e.message === "SyntaxError: Escaped characters are not allowed in keywords"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail); + } + }); + + it("Should read strings containing surigates correctly", function() { + var tests = [ + ['var a = "\ud800\udc00";', 'var a="\\u{10000}";'], + ['var b = "\udbff\udfff";', 'var b="\\u{10ffff}";'] + ]; + + for (var i = 0; i < tests.length; i++) { + assert.strictEqual(uglify.minify(tests[i][0], { + fromString: true, output: { ascii_only: true} + }).code, tests[i][1]); + } + }); +}); From 54a783ba84892346213f61ccf524c3a48a7cb6e9 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 26 Jun 2016 19:44:22 +0200 Subject: [PATCH 088/121] Add ecma5 flag for codegen --- lib/output.js | 9 ++++-- test/compress/unicode.js | 63 ++++++++++++++++++++++++++++++++++++++-- test/mocha/unicode.js | 2 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/lib/output.js b/lib/output.js index 8ba98b69..91f7c504 100644 --- a/lib/output.js +++ b/lib/output.js @@ -67,7 +67,8 @@ function OutputStream(options) { screw_ie8 : false, preamble : null, quote_style : 0, - keep_quoted_props: false + keep_quoted_props: false, + ecma : 5, }, true); var indentation = 0; @@ -80,7 +81,11 @@ function OutputStream(options) { return str.replace(/[\ud800-\udbff][\udc00-\udfff]|[\u0000-\u001f\u007f-\uffff]/g, function(ch) { var code = get_full_char_code(ch, 0).toString(16); - if ((identifier && code.length === 1) || code.length > 4) { + if ((identifier && code.length === 1 && !options.es5) || code.length > 4) { + if (options.ecma < 6) { + return "\\u" + ch.charCodeAt(0).toString(16) + "\\u" + + ch.charCodeAt(1).toString(16); + } return "\\u{" + code + "}"; } else if (code.length <= 2 && !identifier) { while (code.length < 2) code = "0" + code; diff --git a/test/compress/unicode.js b/test/compress/unicode.js index 9651c6df..f08072e0 100644 --- a/test/compress/unicode.js +++ b/test/compress/unicode.js @@ -17,6 +17,7 @@ unicode_parse_variables: { } unicode_escaped_identifier: { + beautify = {ecma: 6} input: { var \u{61} = "foo"; var \u{10000} = "bar"; @@ -25,7 +26,7 @@ unicode_escaped_identifier: { } unicode_identifier_ascii_only: { - beautify = {ascii_only: true} + beautify = {ascii_only: true, ecma: 6} input: { var \u{0061} = "hi"; var bar = "h\u{0065}llo"; @@ -35,9 +36,67 @@ unicode_identifier_ascii_only: { } unicode_string_literals: { - beautify = {ascii_only: true} + beautify = {ascii_only: true, ecma: 6} input: { var a = "6 length unicode character: \u{101111}"; } expect_exact: 'var a="6 length unicode character: \\u{101111}";' } + +unicode_output_es5_surrogates: { + beautify = {ascii_only: true, ecma: 5} + input: { + var \u{10000} = "6 length unicode character: \u{10FFFF}"; + } + expect_exact: 'var \\ud800\\udc00="6 length unicode character: \\udbff\\udfff";' +} + +check_escape_style: { + beautify = {ascii_only: true, ecma: 6} + input: { + var a = "\x01"; + var \ua0081 = "\x10"; // \u0081 only in ID_Continue + var \u0100 = "\u0100"; + var \u1000 = "\u1000"; + var \u{10000} = "\u{10000}"; + var \u{2f800} = "\u{100000}"; + } + expect_exact: 'var a="\\x01";var \\ua0081="\\x10";var \\u0100="\\u0100";var \\u1000="\\u1000";var \\u{10000}="\\u{10000}";var \\u{2f800}="\\u{100000}";' +} + +check_escape_style_es5: { + beautify = {ascii_only: true, ecma: 5} + input: { + var a = "\x01"; + var \ua0081 = "\x10"; // \u0081 only in ID_Continue + var \u0100 = "\u0100"; + var \u1000 = "\u1000"; + var \u{10000} = "\u{10000}"; + var \u{2f800} = "\u{100000}"; + } + expect_exact: 'var a="\\x01";var \\ua0081="\\x10";var \\u0100="\\u0100";var \\u1000="\\u1000";var \\ud800\\udc00="\\ud800\\udc00";var \\ud87e\\udc00="\\udbc0\\udc00";' +} + +ID_continue_with_surrogate_pair: { + beautify = {ascii_only: true, ecma: 6} + input: { + var \u{2f800}\u{2f800}\u{2f800}\u{2f800} = "\u{100000}\u{100000}\u{100000}\u{100000}\u{100000}"; + } + expect_exact: 'var \\u{2f800}\\u{2f800}\\u{2f800}\\u{2f800}="\\u{100000}\\u{100000}\\u{100000}\\u{100000}\\u{100000}";' +} + +escape_non_escaped_identifier: { + beautify = {ascii_only: true, ecma: 6} + input: { + var µþ = "µþ"; + } + expect_exact: 'var \\u00b5\\u00fe="\\xb5\\xfe";' +} + +non_escape_2_non_escape: { + beautify = {ascii_only: false, ecma: 6} + input: { + var µþ = "µþ"; + } + expect_exact: 'var µþ="µþ";' +} \ No newline at end of file diff --git a/test/mocha/unicode.js b/test/mocha/unicode.js index a7cc12dd..d23b4a46 100644 --- a/test/mocha/unicode.js +++ b/test/mocha/unicode.js @@ -126,7 +126,7 @@ describe("Unicode", function() { for (var i = 0; i < tests.length; i++) { assert.strictEqual(uglify.minify(tests[i][0], { - fromString: true, output: { ascii_only: true} + fromString: true, output: { ascii_only: true, ecma: 6} }).code, tests[i][1]); } }); From fb2f8d1a512e5bb8f6b4281d09bbf71fc9a75f23 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Mon, 27 Jun 2016 01:17:53 +0200 Subject: [PATCH 089/121] Add reserved words to list unescapable keywords Additionals: * Update list reserved keywords --- lib/parse.js | 5 ++--- test/mocha/unicode.js | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index fc7d3054..7f306483 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -46,8 +46,7 @@ var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; 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' - + " " + KEYWORDS_ATOM + " " + KEYWORDS; +var RESERVED_WORDS = 'enum implements interface package private protected public static super this' + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case yield'; KEYWORDS = makePredicate(KEYWORDS); @@ -547,7 +546,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } name += ch; } - if (KEYWORDS(name) && escaped) { + if (RESERVED_WORDS(name) && escaped) { parse_error("SyntaxError: Escaped characters are not allowed in keywords"); } return name; diff --git a/test/mocha/unicode.js b/test/mocha/unicode.js index d23b4a46..332adab7 100644 --- a/test/mocha/unicode.js +++ b/test/mocha/unicode.js @@ -99,7 +99,8 @@ describe("Unicode", function() { 'var \\u0069\\u006e = "foo"', // in 'var \\u0076\\u0061\\u0072 = "bar"', // var 'var \\u{66}\\u{6f}\\u{72} = "baz"', // for - 'var \\u0069\\u{66} = "foobar"' // if + 'var \\u0069\\u{66} = "foobar"', // if + 'var \\u{73}uper' // super ]; var exec = function(test) { From d8d4e71b9ee022aeadc56821e086c641702009ee Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 1 Jul 2016 15:36:38 +0200 Subject: [PATCH 090/121] Fix uses_with/uses_eval/directives state in block scope --- lib/scope.js | 5 +++++ test/mocha/with.js | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index 7b8d38a0..11adbbfd 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -119,6 +119,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ scope.init_scope_vars(nesting); scope.parent_scope = save_scope; scope.is_block_scope = true; + if (!(node instanceof AST_Scope)) { + scope.uses_with = save_scope.uses_with; + scope.uses_eval = save_scope.uses_eval; + scope.directives = save_scope.directives; + } descend(); scope = save_scope; return true; diff --git a/test/mocha/with.js b/test/mocha/with.js index 2e758d1e..d1bfcc80 100644 --- a/test/mocha/with.js +++ b/test/mocha/with.js @@ -13,4 +13,11 @@ describe("With", function() { } assert.throws(test, error); }); -}); \ No newline at end of file + it("Should set uses_with for scopes involving With statements", function() { + var ast = uglify.parse("with(e) {f(1, 2)}"); + ast.figure_out_scope(); + assert.equal(ast.uses_with, true); + assert.equal(ast.body[0].expression.scope.uses_with, true); + assert.equal(ast.body[0].body.body[0].body.expression.scope.uses_with, true); + }); +}); From d9bc6f303c7649811dedd1b5646c83df89c24588 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 3 Jul 2016 20:17:03 +0200 Subject: [PATCH 091/121] Fix output arrow function with 1 param with default value Fixes #1090 --- lib/output.js | 2 +- test/compress/arrow.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index 60c8dad8..e309f5bd 100644 --- a/lib/output.js +++ b/lib/output.js @@ -909,7 +909,7 @@ function OutputStream(options) { parent instanceof AST_Unary || parent instanceof AST_Call; if (needs_parens) { output.print("(") } - if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol) { + if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol && !self.argnames[0].default) { self.argnames[0].print(output); } else { output.with_parens(function(){ diff --git a/test/compress/arrow.js b/test/compress/arrow.js index c5838537..1a8b17ed 100644 --- a/test/compress/arrow.js +++ b/test/compress/arrow.js @@ -82,4 +82,11 @@ arrow_functions_with_body: { b.join(); }; } -} \ No newline at end of file +} + +arrow_function_with_single_parameter_with_default: { + input: { + var foo = (a = 0) => doSomething(a); + } + expect_exact: "var foo=(a=0)=>doSomething(a);" +} From ff7f6139ba515aa289893955c5d1d7cb1d1ba7bc Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Mon, 4 Jul 2016 14:30:27 +0200 Subject: [PATCH 092/121] Improve multi-line comment parsing * Make sure comments are skipped correctly with surrogates * Fix regression in multiline comments with nlb --- lib/parse.js | 18 +++++++++++++++++- test/compress/html_comments.js | 9 +++++++++ test/mocha/comment.js | 5 +++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index f843bc4b..1c0482f3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -148,6 +148,21 @@ function get_full_char_code(str, pos) { return str.charCodeAt(pos); } +function get_full_char_length(str) { + var surrogates = 0; + + for (var i = 0; i < str.length; i++) { + if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) { + if (str.charCodeAt(i + 1) >= 0xdc00 && str.charCodeAt(i + 1) <= 0xdfff) { + surrogates++; + i++; + } + } + } + + return str.length - surrogates; +} + function from_char_code(code) { // Based on https://github.com/mathiasbynens/String.fromCodePoint/blob/master/fromcodepoint.js if (code > 0xFFFF) { @@ -493,8 +508,9 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { var i = find("*/", true); var text = S.text.substring(S.pos, i).replace(/\r\n|\r|\u2028|\u2029/g, '\n'); // update stream position - forward(text.length /* doesn't count \r\n as 2 char while S.pos - i does */ + 2); + forward(get_full_char_length(text) /* text length doesn't count \r\n as 2 char while S.pos - i does */ + 2); S.comments_before.push(token("comment2", text, true)); + S.newline_before = S.newline_before || text.indexOf("\n") >= 0; S.regex_allowed = regex_allowed; return next_token; }); diff --git a/test/compress/html_comments.js b/test/compress/html_comments.js index 8495b433..39973c3d 100644 --- a/test/compress/html_comments.js +++ b/test/compress/html_comments.js @@ -69,3 +69,12 @@ html_comment_in_string_literal: { } expect_exact: 'function f(){return"\\x3c!--HTML--\\x3ecomment in\\x3c!--string literal--\\x3e"}'; } + +html_comment_after_multiline_comment: { + input: { + var foo; /* +*/--> var bar; + var foobar; + } + expect_exact: "var foo;var foobar;" +} diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 69cdb3d5..d135bb0a 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -31,7 +31,8 @@ describe("Comment", function() { "/*Some comment 2\r\n\r\n\r\n*/\r\n>\n\n\n\n\n\n", "/*Some comment 3\r\r\r*/\r>\n\n\n\n\n\n", "/*Some comment 4\u2028\u2028\u2028*/\u2028>\n\n\n\n\n\n", - "/*Some comment 5\u2029\u2029\u2029*/\u2029>\n\n\n\n\n\n" + "/*Some comment 5\u2029\u2029\u2029*/\u2029>\n\n\n\n\n\n", + "/*Some comment 6\udbff\udfff\udbff\udfff\n\n\n*/\n>\n\n\n\n\n" ]; var fail = function(e) { @@ -43,7 +44,7 @@ describe("Comment", function() { for (var i = 0; i < tests.length; i++) { assert.throws(function() { - uglify.parse(tests[i], {fromString: true}) + uglify.parse(tests[i], {fromString: true}); }, fail, tests[i]); } }); From 0af42d1831ea4e187f889f03d812a08f949725ff Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 22 Jun 2016 19:24:03 +0200 Subject: [PATCH 093/121] Template fixes * Fixes #1147: template strings not obeying -b ascii_only true * Allow evaluation of template expressions by adding optimizers and walkers * Make sure tagged templates are never changed * Remove template tokenizer in parser, add template tokenizer in tokenizer. It is using a brace counter to track brace position of templates * Add tokens `template_head` and `template_substitution` but parsing tokens stays mostly the same * Do not output strings anymore in AST_TemplateString, instead use AST_TemplateSegment * Fix parsing tagged templates, allowing multiple templates behind as spec allows this These changes don't influence tagged templates because raw content may influence code execution, however they are safe to do in normal templates: * Allow basic string concatenation of templates where possible * Allow custom character escape style similar to strings, except in tagged templates Note that expressions are still compressed in tagged templates. Optional things that may be improved later: * Custom quote style for templates if it doesn't have expressions. Making it obey the quote_style option if this is the case. --- lib/ast.js | 10 +- lib/compress.js | 40 ++++ lib/output.js | 27 ++- lib/parse.js | 108 +++++++--- lib/transform.js | 12 ++ test/compress/harmony.js | 18 -- test/compress/template-string.js | 331 +++++++++++++++++++++++++++++++ test/mocha/template-string.js | 33 +++ 8 files changed, 528 insertions(+), 51 deletions(-) create mode 100644 test/compress/template-string.js create mode 100644 test/mocha/template-string.js diff --git a/lib/ast.js b/lib/ast.js index 54111e58..c08e6222 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -538,7 +538,7 @@ var AST_PrefixedTemplateString = DEFNODE("PrefixedTemplateString", "template_str var AST_TemplateString = DEFNODE("TemplateString", "segments", { $documentation: "A template string literal", $propdoc: { - segments: "[string|AST_Expression]* One or more segments. They can be the parts that are evaluated, or the raw string parts." + segments: "[AST_TemplateSegment|AST_Expression]* One or more segments, starting with AST_TemplateSegment. AST_Expression may follow AST_TemplateSegment, but each AST_Expression must be followed by AST_TemplateSegment." }, _walk: function(visitor) { return visitor._visit(this, function(){ @@ -551,6 +551,14 @@ var AST_TemplateString = DEFNODE("TemplateString", "segments", { } }); +var AST_TemplateSegment = DEFNODE("TemplateSegment", "value raw", { + $documentation: "A segment of a template string literal", + $propdoc: { + value: "Content of the segment", + raw: "Raw content of the segment" + } +}); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { diff --git a/lib/compress.js b/lib/compress.js index 2a22e7f1..47fb220a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -952,6 +952,9 @@ merge(Compressor.prototype, { (function (def){ def(AST_Node, function(){ return false }); def(AST_String, function(){ return true }); + def(AST_TemplateString, function(){ + return this.segments.length === 1; + }); def(AST_UnaryPrefix, function(){ return this.operator == "typeof"; }); @@ -1056,6 +1059,10 @@ merge(Compressor.prototype, { def(AST_Constant, function(){ return this.getValue(); }); + def(AST_TemplateString, function() { + if (this.segments.length !== 1) throw def; + return this.segments[0].value; + }); def(AST_UnaryPrefix, function(compressor){ var e = this.expression; switch (this.operator) { @@ -2988,4 +2995,37 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_TemplateString, function(self, compressor){ + if (!compressor.option("evaluate") + || compressor.parent() instanceof AST_PrefixedTemplateString) + return self; + + var segments = []; + for (var i = 0; i < self.segments.length; i++) { + if (self.segments[i] instanceof AST_Node) { + var result = self.segments[i].evaluate(compressor); + // No result[1] means nothing to stringify + if (result.length === 1) { + segments.push(result[0]); + continue; + } + // Evaluate length + if (result[0].print_to_string().length + 3 /* ${} */ < (result[1]+"").length) { + segments.push(result[0]); + continue; + } + // There should always be a previous and next segment if segment is a node + segments[segments.length - 1].value = segments[segments.length - 1].value + result[1] + self.segments[++i].value; + } else { + segments.push(self.segments[i]); + } + } + self.segments = segments; + + return self; + }); + + OPT(AST_PrefixedTemplateString, function(self, compressor){ + return self; + }); })(); diff --git a/lib/output.js b/lib/output.js index e309f5bd..f75f4243 100644 --- a/lib/output.js +++ b/lib/output.js @@ -125,7 +125,22 @@ function OutputStream(options) { function quote_double() { return '"' + str.replace(/\x22/g, '\\"') + '"'; } + function quote_template() { + if (!options.ascii_only) { + str = str.replace(/\\(n|r|u2028|u2029)/g, function(s, c) { + switch(c) { + case "n": return "\n"; + case "r": return "\r"; + case "u2028": return "\u2028"; + case "u2029": return "\u2029"; + } + return s; + }); + } + return '`' + str.replace(/`/g, '\\`') + '`'; + } if (options.ascii_only) str = to_ascii(str); + if (quote === "`") return quote_template(); switch (options.quote_style) { case 1: return quote_single(); @@ -387,6 +402,10 @@ function OutputStream(options) { } print(encoded); }, + print_template_string_chars: function(str) { + var encoded = encode_string(str, '`'); + return print(encoded.substr(1, encoded.length - 2)); + }, encode_string : encode_string, next_indent : next_indent, with_indent : with_indent, @@ -889,14 +908,18 @@ function OutputStream(options) { self.template_string.print(output); }); DEFPRINT(AST_TemplateString, function(self, output) { + var is_tagged = output.parent() instanceof AST_PrefixedTemplateString; + output.print("`"); for (var i = 0; i < self.segments.length; i++) { - if (typeof self.segments[i] !== "string") { + if (!(self.segments[i] instanceof AST_TemplateSegment)) { output.print("${"); self.segments[i].print(output); output.print("}"); + } else if (is_tagged) { + output.print(self.segments[i].raw); } else { - output.print(self.segments[i]); + output.print_template_string_chars(self.segments[i].value); } } output.print("`"); diff --git a/lib/parse.js b/lib/parse.js index 1c0482f3..34e42e7f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -120,7 +120,7 @@ var PUNC_AFTER_EXPRESSION = makePredicate(characters(";]),:")); var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); -var PUNC_CHARS = makePredicate(characters("[]{}(),;:`")); +var PUNC_CHARS = makePredicate(characters("[]{}(),;:")); var REGEXP_MODIFIERS = makePredicate(characters("gmsiy")); @@ -269,6 +269,8 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { tokcol : 0, newline_before : false, regex_allowed : false, + brace_counter : 0, + template_braces : [], comments_before : [], directives : {}, directive_stack : [] @@ -487,6 +489,40 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return tok; }); + var read_template_characters = with_eof_error("SyntaxError: Unterminated template", function(begin){ + if (begin) { + S.template_braces.push(S.brace_counter); + } + var content = "", raw = "", ch, tok; + next(); + while ((ch = next(true)) !== "`") { + if (ch === "$" && peek() === "{") { + next(); + S.brace_counter++; + tok = token(begin ? "template_head" : "template_substitution", content); + tok.begin = begin; + tok.raw = raw; + tok.end = false; + return tok; + } + + raw += ch; + if (ch === "\\") { + var tmp = S.pos; + ch = read_escaped_char(); + raw += S.text.substr(tmp, S.pos - tmp); + } + + content += ch; + } + S.template_braces.pop(); + tok = token(begin ? "template_head" : "template_substitution", content); + tok.begin = begin; + tok.raw = raw; + tok.end = true; + return tok; + }); + function skip_line_comment(type) { var regex_allowed = S.regex_allowed; var i = find_eol(), ret; @@ -688,6 +724,16 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return tok; } case 61: return handle_eq_sign(); + case 96: return read_template_characters(true); + case 123: + S.brace_counter++; + break; + case 125: + S.brace_counter--; + if (S.template_braces.length > 0 + && S.template_braces[S.template_braces.length - 1] === S.brace_counter) + return read_template_characters(false); + break; } if (is_digit(code)) return read_num(); if (PUNC_CHARS(ch)) return token("punc", next()); @@ -939,6 +985,7 @@ function parse($TEXT, options) { }); } return stat; + case "template_head": case "num": case "regexp": case "operator": @@ -960,7 +1007,6 @@ function parse($TEXT, options) { }); case "[": case "(": - case "`": return simple_statement(); case ";": S.in_directives = false; @@ -1600,8 +1646,6 @@ function parse($TEXT, options) { return subscripts(array_(), allow_calls); case "{": return subscripts(object_or_object_destructuring_(), allow_calls); - case "`": - return subscripts(template_string(), allow_calls); } unexpected(); } @@ -1619,6 +1663,9 @@ function parse($TEXT, options) { cls.end = prev(); return subscripts(cls, allow_calls); } + if (is("template_head")) { + return subscripts(template_string(), allow_calls); + } if (ATOMIC_START_TOKEN[S.token.type]) { return subscripts(as_atom_node(), allow_calls); } @@ -1626,28 +1673,29 @@ function parse($TEXT, options) { }; function template_string() { - var tokenizer_S = S.input, start = S.token, segments = [], segment = "", ch; + var segments = [], start = S.token; - while ((ch = tokenizer_S.next()) !== "`") { - if (ch === "$" && tokenizer_S.peek() === "{") { - segments.push(segment); segment = ""; - tokenizer_S.next(); - next(); - segments.push(expression()); - if (!is("punc", "}")) { - // force error message - expect("}"); - } - continue; - } - segment += ch; - if (ch === "\\") { - segment += tokenizer_S.next(); + segments.push(new AST_TemplateSegment({ + start: S.token, + raw: S.token.raw, + value: S.token.value, + end: S.token + })); + while (S.token.end === false) { + next(); + segments.push(expression()); + + if (!is_token("template_substitution")) { + unexpected(); } + + segments.push(new AST_TemplateSegment({ + start: S.token, + raw: S.token.raw, + value: S.token.value, + end: S.token + })); } - - segments.push(segment); - next(); return new AST_TemplateString({ @@ -2033,6 +2081,13 @@ function parse($TEXT, options) { end : prev() }), true); } + if (is("template_head")) { + return subscripts(new AST_PrefixedTemplateString({ + start: start, + prefix: expr, + template_string: template_string() + }), allow_calls); + } return expr; }; @@ -2189,13 +2244,6 @@ function parse($TEXT, options) { }); return arrow_function(expr); } - if ((expr instanceof AST_SymbolRef || expr instanceof AST_PropAccess) && is("punc", "`")) { - return new AST_PrefixedTemplateString({ - start: start, - prefix: expr, - template_string: template_string() - }) - } if (commas && is("punc", ",")) { next(); return new AST_Seq({ diff --git a/lib/transform.js b/lib/transform.js index ae839417..05e04853 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -231,4 +231,16 @@ TreeTransformer.prototype = new TreeWalker; self.expression = self.expression.transform(tw); }); + _(AST_TemplateString, function(self, tw) { + for (var i = 0; i < self.segments.length; i++) { + if (!(self.segments[i] instanceof AST_TemplateSegment)) { + self.segments[i] = self.segments[i].transform(tw); + } + } + }); + + _(AST_PrefixedTemplateString, function(self, tw) { + self.template_string = self.template_string.transform(tw); + }); + })(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 9d17f4f4..838501ab 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -78,24 +78,6 @@ typeof_arrow_functions: { expect_exact: "var foo=\"function\";" } -template_strings: { - input: { - ``; - `xx\`x`; - `${ foo + 2 }`; - ` foo ${ bar + `baz ${ qux }` }`; - } - expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; -} - -template_string_prefixes: { - input: { - String.raw`foo`; - foo `bar`; - } - expect_exact: "String.raw`foo`;foo`bar`;"; -} - destructuring_arguments: { input: { (function ( a ) { }); diff --git a/test/compress/template-string.js b/test/compress/template-string.js new file mode 100644 index 00000000..df4ff897 --- /dev/null +++ b/test/compress/template-string.js @@ -0,0 +1,331 @@ +template_strings: { + beautify = { + quote_style: 3 + } + input: { + ``; + `xx\`x`; + `${ foo + 2 }`; + ` foo ${ bar + `baz ${ qux }` }`; + } + expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; +} + +template_string_prefixes: { + beautify = { + quote_style: 3 + } + input: { + String.raw`foo`; + foo `bar`; + } + expect_exact: "String.raw`foo`;foo`bar`;"; +} + +template_strings_ascii_only: { + beautify = { + ascii_only: true, + quote_style: 3 + } + input: { + var foo = `foo + bar + ↂωↂ`; + var bar = `\``; + } + expect_exact: "var foo=`foo\\n bar\\n \\u2182\\u03c9\\u2182`;var bar=`\\``;" +} + +template_strings_without_ascii_only: { + beautify = { + quote_style: 3 + } + input: { + var foo = `foo + bar + ↂωↂ` + } + expect_exact: "var foo=`foo\n bar\n ↂωↂ`;" +} + +template_string_with_constant_expression: { + options = { + evaluate: true + } + beautify = { + quote_style: 3 + } + input: { + var foo = `${4 + 4} equals 4 + 4`; + } + expect: { + var foo = `8 equals 4 + 4`; + } +} + +template_string_with_predefined_constants: { + options = { + evaluate: true + } + beautify = { + quote_style: 3 + } + input: { + var foo = `This is ${undefined}`; + var bar = `This is ${NaN}`; + var baz = `This is ${null}`; + var foofoo = `This is ${Infinity}`; + var foobar = "This is ${1/0}"; + var foobaz = 'This is ${1/0}'; + var barfoo = "This is ${NaN}"; + var bazfoo = "This is ${null}"; + var bazbaz = `This is ${1/0}`; + var barbar = `This is ${0/0}`; + var barbar = "This is ${0/0}"; + var barber = 'This is ${0/0}'; + + var a = `${4**11}`; // 8 in template vs 7 chars - 4194304 + var b = `${4**12}`; // 8 in template vs 8 chars - 16777216 + var c = `${4**14}`; // 8 in template vs 9 chars - 268435456 + } + expect: { + var foo = `This is undefined`; + var bar = `This is NaN`; + var baz = `This is null`; + var foofoo = `This is ${1/0}`; + var foobar = "This is ${1/0}"; + var foobaz = 'This is ${1/0}'; + var barfoo = "This is ${NaN}"; + var bazfoo = "This is ${null}"; + var bazbaz = `This is ${1/0}`; + var barbar = `This is NaN`; + var barbar = "This is ${0/0}"; + var barber = 'This is ${0/0}'; + + var a = `4194304`; + var b = `16777216`; // Potential for further concatentation + var c = `${4**14}`; // Not worth converting + } +} + +template_string_evaluate_with_many_segments: { + options = { + evaluate: true + } + beautify = { + quote_style: 3 + } + input: { + var foo = `Hello ${guest()}, welcome to ${location()}${"."}`; + var bar = `${1}${2}${3}${4}${5}${6}${7}${8}${9}${0}`; + var baz = `${foobar()}${foobar()}${foobar()}${foobar()}`; + var buzz = `${1}${foobar()}${2}${foobar()}${3}${foobar()}`; + } + expect: { + var foo = `Hello ${guest()}, welcome to ${location()}.`; + var bar = `1234567890`; + var baz = `${foobar()}${foobar()}${foobar()}${foobar()}`; + var buzz = `1${foobar()}2${foobar()}3${foobar()}`; + } +} + +template_string_with_many_segments: { + beautify = { + quote_style: 3 + } + input: { + var foo = `Hello ${guest()}, welcome to ${location()}${"."}`; + var bar = `${1}${2}${3}${4}${5}${6}${7}${8}${9}${0}`; + var baz = `${foobar()}${foobar()}${foobar()}${foobar()}`; + var buzz = `${1}${foobar()}${2}${foobar()}${3}${foobar()}`; + } + expect: { + var foo = `Hello ${guest()}, welcome to ${location()}${"."}`; + var bar = `${1}${2}${3}${4}${5}${6}${7}${8}${9}${0}`; + var baz = `${foobar()}${foobar()}${foobar()}${foobar()}`; + var buzz = `${1}${foobar()}${2}${foobar()}${3}${foobar()}`; + } +} + +template_string_to_normal_string: { + options = { + evaluate: true + } + beautify = { + quote_style: 0 + } + input: { + var foo = `This is ${undefined}`; + var bar = "Decimals " + `${1}${2}${3}${4}${5}${6}${7}${8}${9}${0}`; + } + expect: { + var foo = `This is undefined`; + var bar = "Decimals 1234567890"; + } +} + +template_concattenating_string: { + options = { + evaluate: true + } + beautify = { + quote_style: 3 // Yes, keep quotes + } + input: { + var foo = "Have a nice " + `day. ${`day. ` + `day.`}`; + var bar = "Have a nice " + `${day()}`; + } + expect: { + var foo = "Have a nice day. day. day."; + var bar = "Have a nice " + `${day()}`; + } +} + +evaluate_nested_templates: { + options = { + evaluate: true + } + beautify = { + quote_style: 0 + } + input: { + var baz = `${`${`${`foo`}`}`}`; + } + expect: { + var baz = `foo`; + } +} + +enforce_double_quotes: { + beautify = { + quote_style: 1 + } + input: { + var foo = `Hello world`; + var bar = `Hello ${'world'}`; + var baz = `Hello ${world()}`; + } + expect: { + var foo = `Hello world`; + var bar = `Hello ${"world"}`; + var baz = `Hello ${world()}`; + } +} + +enforce_single_quotes: { + beautify = { + quote_style: 2 + } + input: { + var foo = `Hello world`; + var bar = `Hello ${"world"}`; + var baz = `Hello ${world()}`; + } + expect: { + var foo = `Hello world`; + var bar = `Hello ${'world'}`; + var baz = `Hello ${world()}`; + } +} + +enforce_double_quotes_and_evaluate: { + beautify = { + quote_style: 1 + } + options = { + evaluate: true + } + input: { + var foo = `Hello world`; + var bar = `Hello ${'world'}`; + var baz = `Hello ${world()}`; + } + expect: { + var foo = `Hello world`; + var bar = `Hello world`; + var baz = `Hello ${world()}`; + } +} + +enforce_single_quotes_and_evaluate: { + beautify = { + quote_style: 2 + } + options = { + evaluate: true + } + input: { + var foo = `Hello world`; + var bar = `Hello ${"world"}`; + var baz = `Hello ${world()}`; + } + expect: { + var foo = `Hello world`; + var bar = `Hello world`; + var baz = `Hello ${world()}`; + } +} + +respect_inline_script: { + beautify = { + inline_script: true, + quote_style: 3 + } + input: { + var foo = `${content}`; + var bar = ``; + } + expect_exact: "var foo=`<\\/script>${content}`;var bar=`\\x3c!--`;var baz=`--\\x3e`;"; +} + +do_not_optimize_tagged_template_1: { + beautify = { + quote_style: 0 + } + options = { + evaluate: true + } + input: { + var foo = tag`Shall not be optimized. ${"But " + "this " + "is " + "fine."}`; + var bar = tag`Don't even mind changing my quotes!`; + } + expect_exact: + 'var foo=tag`Shall not be optimized. ${"But this is fine."}`;var bar=tag`Don\'t even mind changing my quotes!`;'; +} + +do_not_optimize_tagged_template_2: { + options = { + evaluate: true + } + input: { + var foo = tag`test` + " something out"; + } + expect_exact: 'var foo=tag`test`+" something out";'; +} + +keep_raw_content_in_tagged_template: { + options = { + evaluate: true + } + input: { + var foo = tag`\u0020\u{20}\u{00020}\x20\40\040 `; + } + expect_exact: "var foo=tag`\\u0020\\u{20}\\u{00020}\\x20\\40\\040 `;"; +} + +allow_chained_templates: { + input: { + var foo = tag`a``b``c``d`; + } + expect: { + var foo = tag`a``b``c``d`; + } +} + +check_escaped_chars: { + input: { + var foo = `\u0020\u{20}\u{00020}\x20\40\040 `; + } + expect_exact: "var foo=` `;"; +} diff --git a/test/mocha/template-string.js b/test/mocha/template-string.js new file mode 100644 index 00000000..cc6ba7ab --- /dev/null +++ b/test/mocha/template-string.js @@ -0,0 +1,33 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Template string", function() { + it("Should not accept invalid sequences", function() { + var tests = [ + // Stress invalid expression + "var foo = `Hello ${]}`", + "var foo = `Test 123 ${>}`", + "var foo = `Blah ${;}`", + + // Stress invalid template_substitution after expression + "var foo = `Blablabla ${123 456}`", + "var foo = `Blub ${123;}`", + "var foo = `Bleh ${a b}`" + ]; + + var exec = function(test) { + return function() { + uglify.parse(test); + } + }; + + var fail = function(e) { + return e instanceof uglify.JS_Parse_Error + && /^SyntaxError: Unexpected token: /.test(e.message); + }; + + for (var i = 0; i < tests.length; i++) { + assert.throws(exec(tests[i]), fail, tests[i]); + } + }); +}); From 842ac27efbc8f81b3bc4140a396957d6cde89d9c Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 17 Jul 2016 12:59:38 -0400 Subject: [PATCH 094/121] [ES6] Get compress and global_defs working for AST_Class --- lib/compress.js | 6 +++ lib/transform.js | 6 +++ test/compress/issue-1212.js | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 test/compress/issue-1212.js diff --git a/lib/compress.js b/lib/compress.js index 47fb220a..21ec8e5c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2988,6 +2988,12 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_Class, function(self, compressor){ + // HACK to avoid compress failure. + // AST_Class is not really an AST_Scope/AST_Block as it lacks a body. + return self; + }); + OPT(AST_Yield, function(self, compressor){ if (!self.is_star && self.expression instanceof AST_Undefined) { self.expression = null; diff --git a/lib/transform.js b/lib/transform.js index 05e04853..c5df875b 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -227,6 +227,12 @@ TreeTransformer.prototype = new TreeWalker; self.value = self.value.transform(tw); }); + _(AST_Class, function(self, tw){ + if (self.name) self.name = self.name.transform(tw); + if (self.extends) self.extends = self.extends.transform(tw); + self.properties = do_list(self.properties, tw); + }); + _(AST_Expansion, function(self, tw){ self.expression = self.expression.transform(tw); }); diff --git a/test/compress/issue-1212.js b/test/compress/issue-1212.js new file mode 100644 index 00000000..72d6ffe0 --- /dev/null +++ b/test/compress/issue-1212.js @@ -0,0 +1,76 @@ +issue_1212_debug_false: { + options = { + global_defs : { DEBUG: false }, + sequences : true, + properties : true, + dead_code : true, + conditionals : true, + comparisons : true, + evaluate : true, + booleans : true, + loops : true, + unused : true, + hoist_funs : true, + keep_fargs : true, + if_return : true, + join_vars : true, + cascade : true, + side_effects : true, + } + input: { + class foo { + bar() { + if (DEBUG) + console.log("DEV"); + else + console.log("PROD"); + } + } + new foo().bar(); + } + expect: { + class foo{ + bar() { console.log("PROD") } + } + (new foo).bar(); + } +} + +issue_1212_debug_true: { + options = { + global_defs : { DEBUG: true }, + sequences : true, + properties : true, + dead_code : true, + conditionals : true, + comparisons : true, + evaluate : true, + booleans : true, + loops : true, + unused : true, + hoist_funs : true, + keep_fargs : true, + if_return : true, + join_vars : true, + cascade : true, + side_effects : true, + } + input: { + class foo { + bar() { + if (DEBUG) + console.log("DEV"); + else + console.log("PROD"); + } + } + new foo().bar(); + } + expect: { + class foo{ + bar() { console.log("DEV") } + } + (new foo).bar(); + } +} + From 766fafda8baefe6d150df27dfad7e99857c08557 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Tue, 19 Jul 2016 18:03:08 +0200 Subject: [PATCH 095/121] Don't remove empty generators passed as parameter --- lib/compress.js | 1 + test/compress/yield.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 21ec8e5c..21d8b50b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2282,6 +2282,7 @@ merge(Compressor.prototype, { } if (compressor.option("side_effects")) { if (self.expression instanceof AST_Function + && !self.expression.is_generator && self.args.length == 0 && !AST_Block.prototype.has_side_effects.call(self.expression, compressor)) { return make_node(AST_Undefined, self).transform(compressor); diff --git a/test/compress/yield.js b/test/compress/yield.js index 9fe9f6c8..c29ce926 100644 --- a/test/compress/yield.js +++ b/test/compress/yield.js @@ -139,4 +139,30 @@ yield_as_identifier_outside_strict_mode: { var yield = "foo"; class yield {} } +} + +empty_generator_as_parameter_with_side_effects: { + options = { + side_effects: true + } + input: { + var GeneratorPrototype = Object.getPrototypeOf( + Object.getPrototypeOf(function*() {}()) + ); + evaluate(GeneratorPrototype); + } + expect_exact: "var GeneratorPrototype=Object.getPrototypeOf(Object.getPrototypeOf(function*(){}()));evaluate(GeneratorPrototype);" +} + +empty_generator_as_parameter_without_side_effects: { + options = { + side_effects: false + } + input: { + var GeneratorPrototype = Object.getPrototypeOf( + Object.getPrototypeOf(function*() {}()) + ); + evaluate(GeneratorPrototype); + } + expect_exact: "var GeneratorPrototype=Object.getPrototypeOf(Object.getPrototypeOf(function*(){}()));evaluate(GeneratorPrototype);" } \ No newline at end of file From 72a9d799b6fce12ed39502eb552fc8c09ff2cc63 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 6 Jul 2016 00:40:28 +0200 Subject: [PATCH 096/121] Various property fixes * Implement getter/setter with computed value * Fix parsing getter/setter after static or generator token * Allow storing expressions for computed expression in AST_SymbolMethod * Allow get and set in shorthand properties in object literals Fixes #1094, #1146 and #1221 --- lib/output.js | 15 +- lib/parse.js | 76 +++++++---- test/compress/harmony.js | 113 ++++------------ test/compress/object.js | 263 ++++++++++++++++++++++++++++++++++++ test/mocha/getter-setter.js | 89 ------------ test/mocha/object.js | 113 +++++++++++++++- 6 files changed, 462 insertions(+), 207 deletions(-) create mode 100644 test/compress/object.js delete mode 100644 test/mocha/getter-setter.js diff --git a/lib/output.js b/lib/output.js index f75f4243..53c9a4ce 100644 --- a/lib/output.js +++ b/lib/output.js @@ -887,8 +887,12 @@ function OutputStream(options) { output.space(); } } - if (self.name) { + if (self.name instanceof AST_Symbol) { self.name.print(output); + } else if (nokeyword && self.name instanceof AST_Node) { + output.with_square(function() { + self.name.print(output); // Computed method name + }); } output.with_parens(function(){ self.argnames.forEach(function(arg, i){ @@ -1489,6 +1493,15 @@ function OutputStream(options) { self.default.print(output) } }); + DEFPRINT(AST_SymbolMethod, function(self, output) { + if (self.name instanceof AST_Node) { + output.with_square(function() { + self.name.print(output); + }); + } else { + self._do_print(output); + } + }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index 34e42e7f..07f984f1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1738,7 +1738,7 @@ function parse($TEXT, options) { if (!options.strict && is("punc", "}")) // allow trailing comma break; - var start = S.token; + start = S.token; var type = start.type; var name = as_property_name(); if (type != "string" && type != "num" && !is("punc", ":")) { @@ -1747,6 +1747,8 @@ function parse($TEXT, options) { a.push(concise); continue; } + } else if (start.value === "*") { + unexpected(prev()); } if (type == "punc" && start.value == "[") { @@ -1802,7 +1804,7 @@ function parse($TEXT, options) { }); function class_(KindOfClass) { - var start, method, class_name, name, extends_, a = []; + var start, method, class_name, extends_, a = []; if (S.token.type == "name" && S.token.value != "extends") { class_name = as_symbol(KindOfClass === AST_DefClass ? AST_SymbolDefClass : AST_SymbolClass); @@ -1822,8 +1824,7 @@ function parse($TEXT, options) { if (is("punc", ";")) { next(); } // Leading semicolons are okay in class bodies. while (!is("punc", "}")) { start = S.token; - name = as_property_name(); - method = concise_method_or_getset(name, start, true); + method = concise_method_or_getset(as_property_name(), start, true); if (!method) { unexpected(); } a.push(method); if (is("punc", ";")) { next(); } @@ -1841,46 +1842,64 @@ function parse($TEXT, options) { } function concise_method_or_getset(name, start, is_class) { + var get_ast = function(name, token) { + if (typeof name === "string" || typeof name === "number") { + if (name === "*") { + unexpected(token); + } + return new AST_SymbolMethod({ + start: token, + name: name, + end: prev() + }); + } + return name; + } var is_static = false; var is_generator = false; if (is_class && name === "static" && !is("punc", "(")) { is_static = true; - name = S.token.value; - next(); + name = as_property_name(); } if (name === "*") { is_generator = true; - name = S.token.value; - next(); + name = as_property_name(); } if (is("punc", "(")) { + name = get_ast(name, start); return new AST_ConciseMethod({ is_generator: is_generator, start : start, static : is_static, - name : new AST_SymbolMethod({ name: name }), + name : name, argnames : params_or_seq_().as_params(croak), body : _function_body(true, is_generator), end : prev() }); } if (name == "get") { - return new AST_ObjectGetter({ - start : start, - static: is_static, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - }); + if (!is("punc") || is("punc", "[")) { + name = get_ast(as_property_name(), prev()); + return new AST_ObjectGetter({ + start : start, + static: is_static, + key : name, + value : function_(AST_Accessor), + end : prev() + }); + } } - if (name == "set") { - return new AST_ObjectSetter({ - start : start, - static: is_static, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - }); + else if (name == "set") { + if (!is("punc") || is("punc", "[")) { + name = get_ast(as_property_name(), prev()); + return new AST_ObjectSetter({ + start : start, + static: is_static, + key : name, + value : function_(AST_Accessor), + end : prev() + }); + } } } @@ -1996,19 +2015,22 @@ function parse($TEXT, options) { var ex = expression(false); expect("]"); return ex; - } else unexpected(); + } else unexpected(tmp); + case "operator": + if (["*", "delete", "in", "instanceof", "new", "typeof", "void"].indexOf(tmp.value) === -1) { + unexpected(tmp); + } case "name": if (tmp.value === "yield" && S.input.has_directive("use strict") && !is_in_generator()) { token_error(tmp, "SyntaxError: Unexpected yield identifier inside strict mode"); } case "string": case "num": - case "operator": case "keyword": case "atom": return tmp.value; default: - unexpected(); + unexpected(tmp); } }; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 838501ab..dd153413 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -47,27 +47,6 @@ regression_assign_arrow_functions: { } } -computed_property_names: { - input: { - obj({ ["x" + "x"]: 6 }); - } - expect_exact: 'obj({["x"+"x"]:6});' -} - -shorthand_properties: { - mangle = true; - input: (function() { - var prop = 1; - const value = {prop}; - return value; - })(); - expect: (function() { - var n = 1; - const r = {prop:n}; - return r; - })(); -} - typeof_arrow_functions: { options = { evaluate: true @@ -158,72 +137,6 @@ default_values_in_destructurings: { expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" } -concise_methods: { - input: { - x = { - foo(a, b) { - return x; - } - } - y = { - foo([{a}]) { - return a; - }, - bar(){} - } - } - expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" -} - -concise_methods_and_mangle_props: { - mangle_props = { - regex: /_/ - }; - input: { - function x() { - obj = { - _foo() { return 1; } - } - } - } - expect: { - function x() { - obj = { - a() { return 1; } - } - } - } -} - -concise_generators: { - input: { - x = { - *foo(a, b) { - return x; - } - } - y = { - *foo([{a}]) { - yield a; - }, - bar(){} - } - } - expect_exact: "x={*foo(a,b){return x}};y={*foo([{a}]){yield a},bar(){}};" -} - -concise_methods_and_keyword_names: { - input: { - x = { - catch() {}, - throw() {} - } - } - expect: { - x={catch(){},throw(){}}; - } -} - classes: { input: { class SomeClass { @@ -309,6 +222,32 @@ classes_can_have_generators: { } } +classes_can_have_computed_generators: { + input: { + class C4 { + *['constructor']() {} + } + } + expect: { + class C4 { + *['constructor']() {} + } + } +} + +classes_can_have_computed_static: { + input: { + class C4 { + static ['constructor']() {} + } + } + expect: { + class C4 { + static ['constructor']() {} + } + } +} + new_target: { input: { new.target; diff --git a/test/compress/object.js b/test/compress/object.js new file mode 100644 index 00000000..ede3ea0f --- /dev/null +++ b/test/compress/object.js @@ -0,0 +1,263 @@ +getter_setter: { + input: { + var get = "bar"; + var a = { + get, + set: "foo", + get bar() { + return this.get; + }, + get 5() { + return "five"; + }, + get 0xf55() { + return "f five five"; + }, + get "five"() { + return 5; + }, + set one(value) { + this._one = value; + }, + set 9(value) { + this._nine = value; + }, + set 0b1010(value) { + this._ten = value; + }, + set "eleven"(value) { + this._eleven = value; + } + }; + var b = { + get() { return "gift"; }, + set: function(code) { return "Storing code " + code; } + }; + var c = { + ["get"]: "foo", + ["set"]: "bar" + }; + var d = { + get: "foo", + set: "bar" + }; + } + expect: { + var get = "bar"; + var a = { + get, + set: "foo", + get bar() { + return this.get; + }, + get 5() { + return "five"; + }, + get 0xf55() { + return "f five five"; + }, + get "five"() { + return 5; + }, + set one(value) { + this._one = value; + }, + set 9(value) { + this._nine = value; + }, + set 0b1010(value) { + this._ten = value; + }, + set "eleven"(value) { + this._eleven = value; + } + }; + var b = { + get() { return "gift"; }, + set: function(code) { return "Storing code " + code; } + }; + var c = { + ["get"]: "foo", + ["set"]: "bar" + }; + var d = { + get: "foo", + set: "bar" + }; + } +} + +getter_setter_mangler: { + mangle = {} + input: { + function f(get,set) { + return { + get, + set, + get g(){}, + set s(n){}, + c, + a:1, + m(){} + }; + } + } + expect_exact: "function f(t,e){return{get:t,set:e,get g(){},set s(t){},c,a:1,m(){}}}" +} + +computed_property_names: { + input: { + obj({ ["x" + "x"]: 6 }); + } + expect_exact: 'obj({["x"+"x"]:6});' +} + +shorthand_properties: { + mangle = true; + input: (function() { + var prop = 1; + const value = {prop}; + return value; + })(); + expect: (function() { + var n = 1; + const r = {prop:n}; + return r; + })(); +} + +concise_methods: { + input: { + x = { + foo(a, b) { + return x; + } + } + y = { + foo([{a}]) { + return a; + }, + bar(){} + } + } + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" +} + +concise_methods_with_computed_property: { + options = { + evaluate: true + } + input: { + var foo = { + [Symbol.iterator]() { + return { /* stuff */ } + }, + [1 + 2]() { + return 3; + } + } + } + expect: { + var foo = { + [Symbol.iterator]() { + return { /* stuff */ } + }, + [3]() { + return 3; + } + } + } +} + +concise_methods_and_mangle_props: { + mangle_props = { + regex: /_/ + }; + input: { + function x() { + obj = { + _foo() { return 1; } + } + } + } + expect: { + function x() { + obj = { + a() { return 1; } + } + } + } +} + +concise_generators: { + input: { + x = { + *foo(a, b) { + return x; + } + } + y = { + *foo([{a}]) { + yield a; + }, + bar(){} + } + } + expect_exact: "x={*foo(a,b){return x}};y={*foo([{a}]){yield a},bar(){}};" +} + +concise_methods_and_keyword_names: { + input: { + x = { + catch() {}, + throw() {} + } + } + expect: { + x={catch(){},throw(){}}; + } +} + + +getter_setter_with_computed_value: { + input: { + class C { + get ['a']() { + return 'A'; + } + set ['a'](value) { + do_something(a); + } + } + var x = { + get [a.b]() { + return 42; + } + }; + class MyArray extends Array { + get [Symbol.species]() { + return Array; + } + } + } + expect: { + class C { + get ['a']() { + return 'A'; + } + set ['a'](value) { + do_something(a); + } + } + var x = { + get [a.b]() { + return 42; + } + }; + class MyArray extends Array { + get [Symbol.species]() { + return Array; + } + } + } +} diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js deleted file mode 100644 index a292fa00..00000000 --- a/test/mocha/getter-setter.js +++ /dev/null @@ -1,89 +0,0 @@ -var UglifyJS = require('../../'); -var assert = require("assert"); - -describe("Getters and setters", function() { - it("Should not accept operator symbols as getter/setter name", function() { - var illegalOperators = [ - "++", - "--", - "+", - "-", - "!", - "~", - "&", - "|", - "^", - "*", - "/", - "%", - ">>", - "<<", - ">>>", - "<", - ">", - "<=", - ">=", - "==", - "===", - "!=", - "!==", - "?", - "=", - "+=", - "-=", - "/=", - "*=", - "%=", - ">>=", - "<<=", - ">>>=", - "|=", - "^=", - "&=", - "&&", - "||" - ]; - var generator = function() { - var results = []; - - for (var i in illegalOperators) { - results.push({ - code: "var obj = { get " + illegalOperators[i] + "() { return test; }};", - operator: illegalOperators[i], - method: "get" - }); - results.push({ - code: "var obj = { set " + illegalOperators[i] + "(value) { test = value}};", - operator: illegalOperators[i], - method: "set" - }); - } - - return results; - }; - - var testCase = function(data) { - return function() { - UglifyJS.parse(data.code); - }; - }; - - var fail = function(data) { - return function (e) { - return e instanceof UglifyJS.JS_Parse_Error && - e.message === "SyntaxError: Invalid getter/setter name: " + data.operator; - }; - }; - - var errorMessage = function(data) { - return "Expected but didn't get a syntax error while parsing following line:\n" + data.code; - }; - - var tests = generator(); - for (var i = 0; i < tests.length; i++) { - var test = tests[i]; - assert.throws(testCase(test), fail(test), errorMessage(test)); - } - }); - -}); diff --git a/test/mocha/object.js b/test/mocha/object.js index a9373747..f5cd1a62 100644 --- a/test/mocha/object.js +++ b/test/mocha/object.js @@ -2,12 +2,12 @@ var Uglify = require("../../"); var assert = require("assert"); describe("Object", function() { - it ("Should allow objects to have a methodDefinition as property", function() { + it("Should allow objects to have a methodDefinition as property", function() { var code = "var a = {test() {return true;}}"; assert.equal(Uglify.minify(code, {fromString: true}).code, "var a={test(){return!0}};"); }); - it ("Should not allow objects to use static keywords like in classes", function() { + it("Should not allow objects to use static keywords like in classes", function() { var code = "{static test() {}}"; var parse = function() { Uglify.parse(code); @@ -17,4 +17,111 @@ describe("Object", function() { } assert.throws(parse, expect); }); -}); \ No newline at end of file + + it("Should not allow objects to have static computed properties like in classes", function() { + var code = "var foo = {static [123](){}}"; + var parse = function() { + console.log(Uglify.parse(code).body[0].body[0]); + } + var expect = function(e) { + return e instanceof Uglify.JS_Parse_Error; + } + assert.throws(parse, expect); + }); + it("Should not accept operator tokens as property/getter/setter name", function() { + var illegalOperators = [ + "++", + "--", + "+", + "-", + "!", + "~", + "&", + "|", + "^", + "*", + "/", + "%", + ">>", + "<<", + ">>>", + "<", + ">", + "<=", + ">=", + "==", + "===", + "!=", + "!==", + "?", + "=", + "+=", + "-=", + "/=", + "*=", + "%=", + ">>=", + "<<=", + ">>>=", + "|=", + "^=", + "&=", + "&&", + "||" + ]; + var generator = function() { + var results = []; + + for (var i in illegalOperators) { + results.push({ + code: "var obj = { get " + illegalOperators[i] + "() { return test; }};", + operator: illegalOperators[i], + method: "get" + }); + results.push({ + code: "var obj = { set " + illegalOperators[i] + "(value) { test = value}};", + operator: illegalOperators[i], + method: "set" + }); + results.push({ + code: "var obj = { " + illegalOperators[i] + ': "123"};', + operator: illegalOperators[i], + method: "key" + }); + results.push({ + code: "var obj = { " + illegalOperators[i] + "(){ return test; }};", + operator: illegalOperators[i], + method: "method" + }); + } + + return results; + }; + + var testCase = function(data) { + return function() { + Uglify.parse(data.code); + }; + }; + + var fail = function(data) { + return function (e) { + return e instanceof Uglify.JS_Parse_Error && ( + e.message === "SyntaxError: Unexpected token: operator (" + data.operator + ")" || + (e.message === "SyntaxError: Unterminated regular expression" && data.operator[0] === "/") || + (e.message === "SyntaxError: Unexpected token: punc (()" && data.operator === "*") + ); + }; + }; + + var errorMessage = function(data) { + return "Expected but didn't get a syntax error while parsing following line:\n" + data.code; + }; + + var tests = generator(); + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + assert.throws(testCase(test), fail(test), errorMessage(test)); + } + }); +}); From 88384cf351b33017ef4aa5253357cb2ff115fd2f Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 16 Jul 2016 18:56:53 +0200 Subject: [PATCH 097/121] Add more globals, whereof most defined after es5.1 Also do not pollute env with mocks replacing standard globals --- lib/propmangle.js | 21 ++++++++++++++++----- test/mocha/builtins.js | 10 +++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/propmangle.js b/lib/propmangle.js index c8bb772e..acdd4b5b 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -45,15 +45,26 @@ function find_builtins() { - // Compatibility fix for es5.1 and earlier where Symbol isn't defined - if (!global.Symbol) { - global.Symbol = new Function(); - } + // Compatibility fix for some standard defined globals not defined on every js environment + var new_globals = ["Symbol", "Map", "Promise", "Proxy", "Reflect", "Set", "WeakMap", "WeakSet"]; + var objects = {}; + + new_globals.forEach(function (new_global) { + objects[new_global] = global[new_global] || new Function(); + }); var a = []; [ Object, Array, Function, Number, String, Boolean, Error, Math, - Date, RegExp, Symbol + Date, RegExp, objects.Symbol, ArrayBuffer, + DataView, decodeURI, decodeURIComponent, + encodeURI, encodeURIComponent, eval, EvalError, + Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, isFinite, isNaN, JSON, objects.Map, parseFloat, + parseInt, objects.Promise, objects.Proxy, RangeError, ReferenceError, + objects.Reflect, objects.Set, SyntaxError, TypeError, Uint8Array, + Uint8ClampedArray, Uint16Array, Uint32Array, URIError, + objects.WeakMap, objects.WeakSet ].forEach(function(ctor){ Object.getOwnPropertyNames(ctor).map(add); if (ctor.prototype) { diff --git a/test/mocha/builtins.js b/test/mocha/builtins.js index 2782830a..427a32ee 100644 --- a/test/mocha/builtins.js +++ b/test/mocha/builtins.js @@ -4,7 +4,7 @@ var assert = require("assert"); describe("builtins", function() { it ("Should not mangle builtins", function() { var test = "function foo(something){\n" + - " return [Object,Array,Function,Number,String,Boolean,Error,Math,Date,RegExp,Symbol,something];\n" + + " return [Object,Array,Function,Number,String,Boolean,Error,Math,Date,RegExp,Symbol,Map,Promise,Proxy,Reflect,Set,WeakMap,WeakSet,Float32Array,something];\n" + "};"; var result = UglifyJS.minify(test, {fromString: true, parse: {bare_returns: true}}).code; @@ -22,5 +22,13 @@ describe("builtins", function() { assert.notEqual(result.indexOf("Date"), -1); assert.notEqual(result.indexOf("RegExp"), -1); assert.notEqual(result.indexOf("Symbol"), -1); + assert.notEqual(result.indexOf("Promise"), -1); + assert.notEqual(result.indexOf("Proxy"), -1); + assert.notEqual(result.indexOf("Reflect"), -1); + assert.notEqual(result.indexOf("Set"), -1); + assert.notEqual(result.indexOf("WeakMap"), -1); + assert.notEqual(result.indexOf("WeakSet"), -1); + assert.notEqual(result.indexOf("Map"), -1); + assert.notEqual(result.indexOf("Float32Array"), -1); }); }); From 3f8fc3a316a60b67acf09b2b2cf887f0209c7d71 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Thu, 21 Jul 2016 18:02:32 +0200 Subject: [PATCH 098/121] Fix computed getters + cleanup AST --- lib/ast.js | 4 ++-- lib/output.js | 25 ++++++++++++++----------- test/compress/object.js | 21 +-------------------- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index c08e6222..4a9e20e4 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -449,7 +449,7 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments is_generator", $documentation: "Base class for functions", $propdoc: { is_generator: "is generatorFn or not", - name: "[AST_SymbolDeclaration?] the name of this function", + name: "[AST_SymbolDeclaration?|AST_Node] the name of this function or computed expression", argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, @@ -996,7 +996,7 @@ var AST_Object = DEFNODE("Object", "properties", { var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { - key: "[string] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.", + key: "[string|AST_Node] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.", value: "[AST_Node] property value. For setters and getters this is an AST_Function." }, _walk: function(visitor) { diff --git a/lib/output.js b/lib/output.js index 53c9a4ce..2046fc92 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1457,7 +1457,13 @@ function OutputStream(options) { } output.print("set"); output.space(); - self.key.print(output); + if (self.key instanceof AST_SymbolMethod) { + self.key.print(output); + } else { + output.with_square(function() { + self.key.print(output); + }); + } self.value._do_print(output, true); }); DEFPRINT(AST_ObjectGetter, function(self, output){ @@ -1467,7 +1473,13 @@ function OutputStream(options) { } output.print("get"); output.space(); - self.key.print(output); + if (self.key instanceof AST_SymbolMethod) { + self.key.print(output); + } else { + output.with_square(function() { + self.key.print(output); + }); + } self.value._do_print(output, true); }); DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { @@ -1493,15 +1505,6 @@ function OutputStream(options) { self.default.print(output) } }); - DEFPRINT(AST_SymbolMethod, function(self, output) { - if (self.name instanceof AST_Node) { - output.with_square(function() { - self.name.print(output); - }); - } else { - self._do_print(output); - } - }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/test/compress/object.js b/test/compress/object.js index ede3ea0f..2e7d37a3 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -240,24 +240,5 @@ getter_setter_with_computed_value: { } } } - expect: { - class C { - get ['a']() { - return 'A'; - } - set ['a'](value) { - do_something(a); - } - } - var x = { - get [a.b]() { - return 42; - } - }; - class MyArray extends Array { - get [Symbol.species]() { - return Array; - } - } - } + expect_exact: 'class C{get["a"](){return"A"}set["a"](value){do_something(a)}}var x={get[a.b](){return 42}};class MyArray extends Array{get[Symbol.species](){return Array}}' } From 110a1ac885ba224cbc677e42695e252068edd267 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 22 Jul 2016 11:46:30 +0200 Subject: [PATCH 099/121] Make distinction between * method and * operator Also add quotes to properties when necessary, this might be the case if the name isn't a valid identifier --- lib/output.js | 34 +++++++-------- lib/parse.js | 17 +++++--- test/compress/object.js | 97 +++++++++++++++++++++++++++++++++++++++++ test/mocha/cli.js | 2 +- 4 files changed, 125 insertions(+), 25 deletions(-) diff --git a/lib/output.js b/lib/output.js index 2046fc92..57920e02 100644 --- a/lib/output.js +++ b/lib/output.js @@ -888,7 +888,11 @@ function OutputStream(options) { } } if (self.name instanceof AST_Symbol) { - self.name.print(output); + if (typeof self.name.name === "string" && !is_identifier_string(self.name.name)) { + output.print_string(self.name.name); + } else { + self.name.print(output); + } } else if (nokeyword && self.name instanceof AST_Node) { output.with_square(function() { self.name.print(output); // Computed method name @@ -1450,15 +1454,19 @@ function OutputStream(options) { output.colon(); self.value.print(output); }); - DEFPRINT(AST_ObjectSetter, function(self, output){ + AST_ObjectProperty.DEFMETHOD("_print_getter_setter", function(type, self, output) { if (self.static) { output.print("static"); output.space(); } - output.print("set"); + output.print(type); output.space(); if (self.key instanceof AST_SymbolMethod) { - self.key.print(output); + if (typeof self.key.name === "string" && !is_identifier_string(self.key.name)) { + output.print_string(self.key.name); + } else { + self.key.print(output); + } } else { output.with_square(function() { self.key.print(output); @@ -1466,21 +1474,11 @@ function OutputStream(options) { } self.value._do_print(output, true); }); + DEFPRINT(AST_ObjectSetter, function(self, output){ + self._print_getter_setter("set", self, output); + }); DEFPRINT(AST_ObjectGetter, function(self, output){ - if (self.static) { - output.print("static"); - output.space(); - } - output.print("get"); - output.space(); - if (self.key instanceof AST_SymbolMethod) { - self.key.print(output); - } else { - output.with_square(function() { - self.key.print(output); - }); - } - self.value._do_print(output, true); + self._print_getter_setter("get", self, output); }); DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { output.print("["); diff --git a/lib/parse.js b/lib/parse.js index 07f984f1..b933dde3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1747,7 +1747,7 @@ function parse($TEXT, options) { a.push(concise); continue; } - } else if (start.value === "*") { + } else if (name === null) { unexpected(prev()); } @@ -1844,14 +1844,13 @@ function parse($TEXT, options) { function concise_method_or_getset(name, start, is_class) { var get_ast = function(name, token) { if (typeof name === "string" || typeof name === "number") { - if (name === "*") { - unexpected(token); - } return new AST_SymbolMethod({ start: token, name: name, end: prev() }); + } else if (name === null) { + unexpected(); } return name; } @@ -1861,9 +1860,12 @@ function parse($TEXT, options) { is_static = true; name = as_property_name(); } - if (name === "*") { + if (name === null) { is_generator = true; name = as_property_name(); + if (name === null) { + unexpected(); + } } if (is("punc", "(")) { name = get_ast(name, start); @@ -2017,7 +2019,10 @@ function parse($TEXT, options) { return ex; } else unexpected(tmp); case "operator": - if (["*", "delete", "in", "instanceof", "new", "typeof", "void"].indexOf(tmp.value) === -1) { + if (tmp.value === "*") { + return null; + } + if (["delete", "in", "instanceof", "new", "typeof", "void"].indexOf(tmp.value) === -1) { unexpected(tmp); } case "name": diff --git a/test/compress/object.js b/test/compress/object.js index 2e7d37a3..d13678a4 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -242,3 +242,100 @@ getter_setter_with_computed_value: { } expect_exact: 'class C{get["a"](){return"A"}set["a"](value){do_something(a)}}var x={get[a.b](){return 42}};class MyArray extends Array{get[Symbol.species](){return Array}}' } + +property_with_operator_value: { + input: { + var foo = { + "*": 1, + get "*"() { + return 2; + }, + *"*"() { + return 3; + }, + "%": 1, + get "%"() { + return 2; + }, + *"%"() { + return 3; + } + } + class bar { + get "*"() { + return 1 + } + *"*"() { + return 2; + } + get "%"() { + return 1 + } + *"%"() { + return 2; + } + } + } + expect_exact: 'var foo={"*":1,get"*"(){return 2},*"*"(){return 3},"%":1,get"%"(){return 2},*"%"(){return 3}};class bar{get"*"(){return 1}*"*"(){return 2}get"%"(){return 1}*"%"(){return 2}}' +} + +property_with_unprintable: { + input: { + var foo = { + "\x00\x01": "foo", + get "\x00\x01"() { + return "bar"; + }, + set "\x00\x01"(foo) { + save(foo); + }, + *"\x00\x01"() { + return "foobar"; + } + } + class bar { + get "\x00\x01"() { + return "bar" + } + set "\x00\x01"(foo) { + save(foo); + } + *"\x00\x01"() { + return "foobar"; + } + } + } + expect_exact: 'var foo={"\\0\x01":"foo",get"\\0\x01"(){return"bar"},set"\\0\x01"(foo){save(foo)},*"\\0\x01"(){return"foobar"}};class bar{get"\\0\x01"(){return"bar"}set"\\0\x01"(foo){save(foo)}*"\\0\x01"(){return"foobar"}}' +} + +property_with_unprintable_ascii_only: { + beautify = { + ascii_only: true, + } + input: { + var foo = { + "\x00\x01": "foo", + get "\x00\x01"() { + return "bar"; + }, + set "\x00\x01"(foo) { + save(foo); + }, + *"\x00\x01"() { + return "foobar"; + } + } + class bar { + get "\x00\x01"() { + return "bar" + } + set "\x00\x01"(foo) { + save(foo); + } + *"\x00\x01"() { + return "foobar"; + } + } + } + expect_exact: 'var foo={"\\0\\x01":"foo",get"\\0\\x01"(){return"bar"},set"\\0\\x01"(foo){save(foo)},*"\\0\\x01"(){return"foobar"}};class bar{get"\\0\\x01"(){return"bar"}set"\\0\\x01"(foo){save(foo)}*"\\0\\x01"(){return"foobar"}}' +} diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 38b57cd7..006566d0 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -3,7 +3,7 @@ var exec = require("child_process").exec; describe("bin/uglifyjs", function () { it("should produce a functional build when using --self", function (done) { - this.timeout(5000); + this.timeout(15000); var uglifyjs = '"' + process.argv[0] + '" bin/uglifyjs'; var command = uglifyjs + ' --self -cm --wrap WrappedUglifyJS'; From 27d3669800a2a0ddb79f652b9bc7da0d64c091b9 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 22 Jul 2016 17:33:24 +0200 Subject: [PATCH 100/121] Don't allow escaped surrogated identifiers + introduce ascii_identifiers Don't use 2 characters for surrogates in identifiers because there is support for the \u{} syntax when escaped identifiers were introduced. Also catch eof errors while reading identifier names Introduce ascii_identifiers: By setting ascii_identifiers to undefined (default value), ascii_identifiers will print identifiers using the same setting as ascii_only within the limits of the ecmascript 6 grammar. ascii_identifiers accept true and false, allowing identifiers to be printed under different settings than strings with the ascii_only setting. --- lib/output.js | 10 ++++++++-- lib/parse.js | 4 ++-- test/compress/unicode.js | 26 ++++++++++++++++++++++---- test/mocha/eof.js | 36 ++++++++++++++++++++++++++++++++++++ test/mocha/unicode.js | 6 +++++- 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 test/mocha/eof.js diff --git a/lib/output.js b/lib/output.js index 57920e02..d4c9fb34 100644 --- a/lib/output.js +++ b/lib/output.js @@ -53,6 +53,7 @@ function OutputStream(options) { quote_keys : false, space_colon : true, ascii_only : false, + ascii_identifiers: undefined, unescape_regexps : false, inline_script : false, width : 80, @@ -70,6 +71,8 @@ function OutputStream(options) { keep_quoted_props: false, ecma : 5, }, true); + if (typeof options.ascii_identifiers === 'undefined') + options.ascii_identifiers = options.ascii_only; var indentation = 0; var current_col = 0; @@ -81,8 +84,11 @@ function OutputStream(options) { return str.replace(/[\ud800-\udbff][\udc00-\udfff]|[\u0000-\u001f\u007f-\uffff]/g, function(ch) { var code = get_full_char_code(ch, 0).toString(16); - if ((identifier && code.length === 1 && !options.es5) || code.length > 4) { + if ((identifier && code.length === 1 && options.ecma >= 6) || code.length > 4) { if (options.ecma < 6) { + if (identifier) { + return ch; // no \u{} support + } return "\\u" + ch.charCodeAt(0).toString(16) + "\\u" + ch.charCodeAt(1).toString(16); } @@ -165,7 +171,7 @@ function OutputStream(options) { function make_name(name) { name = name.toString(); - if (options.ascii_only) + if (options.ascii_identifiers) name = to_ascii(name, true); return name; }; diff --git a/lib/parse.js b/lib/parse.js index b933dde3..a34ae4ad 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -551,7 +551,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return next_token; }); - function read_name() { + var read_name = with_eof_error("SyntaxError: Unterminated identifier name", function() { var name = "", ch, escaped = false, hex; var read_escaped_identifier_char = function() { escaped = true; @@ -593,7 +593,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { parse_error("SyntaxError: Escaped characters are not allowed in keywords"); } return name; - }; + }); var read_regexp = with_eof_error("SyntaxError: Unterminated regular expression", function(regexp){ var prev_backslash = false, ch, in_class = false; diff --git a/test/compress/unicode.js b/test/compress/unicode.js index f08072e0..d5853cc6 100644 --- a/test/compress/unicode.js +++ b/test/compress/unicode.js @@ -43,12 +43,13 @@ unicode_string_literals: { expect_exact: 'var a="6 length unicode character: \\u{101111}";' } +// Don't escape identifiers below es6 (or in this case double escaped in expect_exact) unicode_output_es5_surrogates: { beautify = {ascii_only: true, ecma: 5} input: { var \u{10000} = "6 length unicode character: \u{10FFFF}"; } - expect_exact: 'var \\ud800\\udc00="6 length unicode character: \\udbff\\udfff";' + expect_exact: 'var \u{10000}="6 length unicode character: \\udbff\\udfff";' } check_escape_style: { @@ -64,6 +65,7 @@ check_escape_style: { expect_exact: 'var a="\\x01";var \\ua0081="\\x10";var \\u0100="\\u0100";var \\u1000="\\u1000";var \\u{10000}="\\u{10000}";var \\u{2f800}="\\u{100000}";' } +// Don't escape identifiers below es6, no escaped identifiers support and no \u{} syntax check_escape_style_es5: { beautify = {ascii_only: true, ecma: 5} input: { @@ -71,10 +73,10 @@ check_escape_style_es5: { var \ua0081 = "\x10"; // \u0081 only in ID_Continue var \u0100 = "\u0100"; var \u1000 = "\u1000"; - var \u{10000} = "\u{10000}"; - var \u{2f800} = "\u{100000}"; + var \u{10000} = "\u{10000}"; // Identifier won't be escaped in es 5.1 + var \u{2f800} = "\u{100000}"; // Same } - expect_exact: 'var a="\\x01";var \\ua0081="\\x10";var \\u0100="\\u0100";var \\u1000="\\u1000";var \\ud800\\udc00="\\ud800\\udc00";var \\ud87e\\udc00="\\udbc0\\udc00";' + expect_exact: 'var a="\\x01";var \\ua0081="\\x10";var \\u0100="\\u0100";var \\u1000="\\u1000";var \ud800\udc00="\\ud800\\udc00";var \ud87e\udc00="\\udbc0\\udc00";' } ID_continue_with_surrogate_pair: { @@ -99,4 +101,20 @@ non_escape_2_non_escape: { var µþ = "µþ"; } expect_exact: 'var µþ="µþ";' +} + +non_escape_2_half_escape1: { + beautify = {ascii_only: false, ascii_identifiers: true, ecma: 6} + input: { + var µþ = "µþ"; + } + expect_exact: 'var \\u00b5\\u00fe="µþ";' +} + +non_escape_2_half_escape2: { + beautify = {ascii_only: true, ascii_identifiers: false, ecma: 6} + input: { + var µþ = "µþ"; + } + expect_exact: 'var µþ="\\xb5\\xfe";' } \ No newline at end of file diff --git a/test/mocha/eof.js b/test/mocha/eof.js new file mode 100644 index 00000000..d5a245eb --- /dev/null +++ b/test/mocha/eof.js @@ -0,0 +1,36 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("EOF", function() { + it("Should test code for at least throwing syntax error when incomplete", function() { + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + /^SyntaxError: /.test(e.message); + } + var parse = function(test) { + return function() { + uglify.parse(test); + } + } + // Chops off 1 char at a time until limit or start of string is reached + // The passed code must still be valid when unchopped + var test_eol = function(input, chopLimit) { + if (chopLimit === undefined) { + chopLimit = input.length - 1; + } + + assert.doesNotThrow(parse(input), "Expected valid code for \n" + input); + + for (var i = input.length - 1; chopLimit > 0; chopLimit--, i--) { + var code = input.substr(0, i); + assert.throws(parse(code), error, code); + } + } + + test_eol("var \\u1234", 7); // Incomplete identifier + test_eol("'Incomplete string'"); + test_eol("/Unterminated regex/"); + test_eol("` Unterminated template string`"); + test_eol("/* Unfinishing multiline comment */"); + }); +}); diff --git a/test/mocha/unicode.js b/test/mocha/unicode.js index 332adab7..eb29e4a5 100644 --- a/test/mocha/unicode.js +++ b/test/mocha/unicode.js @@ -52,7 +52,11 @@ describe("Unicode", function() { var tests = [ 'var \\u{0} = "foo";', 'var \\u{10ffff} = "bar";', - 'var \\u000a = "what\'s up";' + 'var \\u000a = "what\'s up";', + // Valid ID_Start, but using up 2 escaped characters and not fitting in IdentifierStart + 'var \\ud800\\udc00 = "Hello";', + 'var \\udbff\\udfff = "Unicode";', // Same as previous test + 'var \\ud800\udc01 = "Weird unicode";', // Same as above, but mixed escaped with unicode chars ]; var exec = function(test) { From d224d71b8dd9a113ac360f63895a7d6a7e2ef813 Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 26 Jul 2016 09:50:35 -0400 Subject: [PATCH 101/121] [ES6] Fix handling of semicolons in export parse. --- lib/parse.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index a34ae4ad..a2b23ede 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1101,7 +1101,7 @@ function parse($TEXT, options) { return tmp = import_(), semicolon(), tmp; case "export": - return tmp = export_(), semicolon(), tmp; + return export_(); default: unexpected(); @@ -1997,6 +1997,7 @@ function parse($TEXT, options) { exported_definition = statement(); } else { exported_value = expression(); + semicolon(); } return new AST_Export({ From 7fa0dbdeb97d01eb40cb2ab492531eac3c0cd820 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 15 Aug 2016 09:54:15 +0200 Subject: [PATCH 102/121] Ignore default reserved properties in compress test --- test/compress/properties.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/compress/properties.js b/test/compress/properties.js index f1680808..7ef6a014 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -99,7 +99,8 @@ mangle_unquoted_properties: { properties: false } mangle_props = { - ignore_quoted: true + ignore_quoted: true, + reserved: [] } beautify = { beautify: false, From 1c15d0db456ce32f1b9b507aad97e5ee5c8285f7 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 29 Jul 2016 03:18:21 +0200 Subject: [PATCH 103/121] Fix quoting of properties - Make AST_ConciseMethod child of AST_ObjectProperty. - Fix some typos. --- lib/ast.js | 39 ++++++------ lib/output.js | 57 +++++++++-------- lib/parse.js | 34 ++++++---- lib/transform.js | 3 + test/compress/harmony.js | 20 ++++++ test/compress/object.js | 131 ++++++++++++++++++++++++++++++++++++++- test/mocha/class.js | 31 +++++++++ 7 files changed, 257 insertions(+), 58 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 4a9e20e4..a3a894c5 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -448,8 +448,8 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments is_generator", { $documentation: "Base class for functions", $propdoc: { - is_generator: "is generatorFn or not", - name: "[AST_SymbolDeclaration?|AST_Node] the name of this function or computed expression", + is_generator: "[boolean] is generatorFn or not", + name: "[AST_SymbolDeclaration?] the name of this function", argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, @@ -487,14 +487,6 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); -var AST_ConciseMethod = DEFNODE("ConciseMethod", "is_generator static", { - $propdoc: { - is_generator: "is generatorFn or not", - static: "[boolean] whether this method is static (classes only)", - }, - $documentation: "An ES6 concise method inside an object or class" -}, AST_Lambda); - var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); @@ -996,11 +988,13 @@ var AST_Object = DEFNODE("Object", "properties", { var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { - key: "[string|AST_Node] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.", - value: "[AST_Node] property value. For setters and getters this is an AST_Function." + key: "[string|AST_Node] the property name converted to a string for ObjectKeyVal. For setters, getters and computed property this is an arbitrary AST_Node", + value: "[AST_Node] property value. For setters and getters this is an AST_Function." }, _walk: function(visitor) { return visitor._visit(this, function(){ + if (this.key instanceof AST_Node) + this.key._walk(visitor); this.value._walk(visitor); }); } @@ -1016,28 +1010,33 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote shorthand", { var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { $documentation: "An object property whose key is computed. Like `[Symbol.iterator]: function...` or `[routes.homepage]: renderHomepage`", - _walk: function(visitor) { - return visitor._visit(this, function(){ - this.key._walk(visitor); - this.value._walk(visitor); - }); - } }, AST_ObjectProperty); -var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { +var AST_ObjectSetter = DEFNODE("ObjectSetter", "quote static", { $propdoc: { + quote: "[string|undefined] the original quote character, if any", static: "[boolean] whether this is a static setter (classes only)" }, $documentation: "An object setter property", }, AST_ObjectProperty); -var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { +var AST_ObjectGetter = DEFNODE("ObjectGetter", "quote static", { $propdoc: { + quote: "[string|undefined] the original quote character, if any", static: "[boolean] whether this is a static getter (classes only)" }, $documentation: "An object getter property", }, AST_ObjectProperty); +var AST_ConciseMethod = DEFNODE("ConciseMethod", "quote static is_generator", { + $propdoc: { + quote: "[string|undefined] the original quote character, if any", + static: "[boolean] whether this method is static (classes only)", + is_generator: "[boolean] is generatorFn or not", + }, + $documentation: "An ES6 concise method inside an object or class" +}, AST_ObjectProperty); + var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { name: "[AST_SymbolClass|AST_SymbolDefClass?] optional class name.", diff --git a/lib/output.js b/lib/output.js index c0d3632a..d0238da8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -966,16 +966,6 @@ function OutputStream(options) { } if (needs_parens) { output.print(")") } }); - DEFPRINT(AST_ConciseMethod, function(self, output){ - if (self.static) { - output.print("static"); - output.space(); - } - if (self.is_generator) { - output.print("*"); - } - self._do_print(output, true /* do not print "function" */); - }); /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ @@ -1430,17 +1420,7 @@ function OutputStream(options) { DEFPRINT(AST_NewTarget, function(self, output) { output.print("new.target"); }); - DEFPRINT(AST_ObjectKeyVal, function(self, output){ - var key = self.key; - var quote = self.quote; - var print_as_shorthand = self.shorthand && - self.value instanceof AST_Symbol && - self.key == self.value.print_to_string(); - - if (print_as_shorthand) { - output.print_name(key); - return; - } + AST_ObjectProperty.DEFMETHOD("print_property_name", function(key, quote, output) { if (output.option("quote_keys")) { output.print_string(key + ""); } else if ((typeof key == "number" @@ -1457,6 +1437,17 @@ function OutputStream(options) { } else { output.print_string(key, quote); } + }); + DEFPRINT(AST_ObjectKeyVal, function(self, output){ + var print_as_shorthand = self.shorthand && + self.value instanceof AST_Symbol && + self.key == self.value.print_to_string(); + + if (print_as_shorthand) { + output.print_name(self.key); + return; + } + self.print_property_name(self.key, self.quote, output); output.colon(); self.value.print(output); }); @@ -1468,11 +1459,7 @@ function OutputStream(options) { output.print(type); output.space(); if (self.key instanceof AST_SymbolMethod) { - if (typeof self.key.name === "string" && !is_identifier_string(self.key.name)) { - output.print_string(self.key.name); - } else { - self.key.print(output); - } + self.print_property_name(self.key.name, self.quote, output); } else { output.with_square(function() { self.key.print(output); @@ -1486,6 +1473,24 @@ function OutputStream(options) { DEFPRINT(AST_ObjectGetter, function(self, output){ self._print_getter_setter("get", self, output); }); + DEFPRINT(AST_ConciseMethod, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } + if (self.is_generator) { + output.print("*"); + } + output.space(); + if (self.key instanceof AST_SymbolMethod) { + self.print_property_name(self.key.name, self.quote, output); + } else { + output.with_square(function() { + self.key.print(output); + }); + } + self.value._do_print(output, true); + }); DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { output.print("["); self.key.print(output); diff --git a/lib/parse.js b/lib/parse.js index 28c722fb..80691da4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1253,7 +1253,7 @@ function parse($TEXT, options) { }); }; - var function_ = function(ctor) { + var function_ = function(ctor, is_generator_property) { var start = S.token var in_statement = ctor === AST_Defun; @@ -1267,7 +1267,7 @@ function parse($TEXT, options) { unexpected(); var args = params_or_seq_().as_params(croak); - var body = _function_body(true, is_generator); + var body = _function_body(true, is_generator || is_generator_property); return new ctor({ start : args.start, end : body.end, @@ -1744,7 +1744,7 @@ function parse($TEXT, options) { start = S.token; var type = start.type; var name = as_property_name(); - if (type != "string" && type != "num" && !is("punc", ":")) { + if (!is("punc", ":")) { var concise = concise_method_or_getset(name, start); if (concise) { a.push(concise); @@ -1757,8 +1757,10 @@ function parse($TEXT, options) { if (type == "punc" && start.value == "[") { expect(":"); a.push(new AST_ObjectComputedKeyVal({ + start: start, key: name, - value: expression(false) + value: expression(false), + end: prev() })); continue; } @@ -1859,12 +1861,15 @@ function parse($TEXT, options) { } var is_static = false; var is_generator = false; + var property_token = start; if (is_class && name === "static" && !is("punc", "(")) { is_static = true; + property_token = S.token; name = as_property_name(); } if (name === null) { is_generator = true; + property_token = S.token; name = as_property_name(); if (name === null) { unexpected(); @@ -1872,23 +1877,28 @@ function parse($TEXT, options) { } if (is("punc", "(")) { name = get_ast(name, start); - return new AST_ConciseMethod({ - is_generator: is_generator, + var node = new AST_ConciseMethod({ start : start, static : is_static, - name : name, - argnames : params_or_seq_().as_params(croak), - body : _function_body(true, is_generator), + is_generator: is_generator, + key : name, + quote : name instanceof AST_SymbolMethod ? + property_token.quote : undefined, + value : function_(AST_Accessor, is_generator), end : prev() }); + return node; } + property_token = S.token; if (name == "get") { if (!is("punc") || is("punc", "[")) { - name = get_ast(as_property_name(), prev()); + name = get_ast(as_property_name(), start); return new AST_ObjectGetter({ start : start, static: is_static, key : name, + quote : name instanceof AST_SymbolMethod ? + property_token.quote : undefined, value : function_(AST_Accessor), end : prev() }); @@ -1896,11 +1906,13 @@ function parse($TEXT, options) { } else if (name == "set") { if (!is("punc") || is("punc", "[")) { - name = get_ast(as_property_name(), prev()); + name = get_ast(as_property_name(), start); return new AST_ObjectSetter({ start : start, static: is_static, key : name, + quote : name instanceof AST_SymbolMethod ? + property_token.quote : undefined, value : function_(AST_Accessor), end : prev() }); diff --git a/lib/transform.js b/lib/transform.js index c5df875b..8237c1e8 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -224,6 +224,9 @@ TreeTransformer.prototype = new TreeWalker; }); _(AST_ObjectProperty, function(self, tw){ + if (self.key instanceof AST_Node) { + self.key = self.key.transform(tw); + } self.value = self.value.transform(tw); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index dd153413..122069dd 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -248,6 +248,26 @@ classes_can_have_computed_static: { } } +class_methods_and_getters_with_keep_quoted_props_enabled: { + beautify = { + quote_style: 3, + keep_quoted_props: true, + } + input: { + class clss { + a() {} + "b"() {} + get c() { return "c"} + get "d"() { return "d"} + set e(a) { doSomething(a); } + set 'f'(a) { doSomething(b); } + static g() {} + static "h"() {} + } + } + expect_exact: 'class clss{a(){}"b"(){}get c(){return"c"}get"d"(){return"d"}set e(a){doSomething(a)}set\'f\'(a){doSomething(b)}static g(){}static"h"(){}}' +} + new_target: { input: { new.target; diff --git a/test/compress/object.js b/test/compress/object.js index d13678a4..b493204a 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -112,6 +112,35 @@ computed_property_names: { expect_exact: 'obj({["x"+"x"]:6});' } +computed_property_names_evaluated_1: { + options = { + evaluate: true + } + input: { + obj({ + [1 + 1]: 2, + ["x" + "x"]: 6 + }); + } + expect_exact: 'obj({[2]:2,["xx"]:6});' +} + +computed_property_names_evaluated_2: { + options = { + evaluate: true + } + input: { + var foo = something(); + + var obj = { + [foo]() { + return "blah"; + } + } + } + expect_exact: 'var foo=something();var obj={[foo](){return"blah"}};' +} + shorthand_properties: { mangle = true; input: (function() { @@ -154,6 +183,9 @@ concise_methods_with_computed_property: { }, [1 + 2]() { return 3; + }, + ["1" + "4"]() { + return 14; } } } @@ -164,11 +196,74 @@ concise_methods_with_computed_property: { }, [3]() { return 3; + }, + ["14"]() { + return 14; } } } } +concise_methods_with_computed_property2: { + options = { + evaluate: true + } + input: { + var foo = { + [[1]](){ + return "success"; + } + }; + doSomething(foo[[1]]()); + } + expect_exact: { + 'var foo={[[1]](){return"success"}};doSomething(foo[[1]]());' + } +} + +concise_methods_with_various_property_names: { + input: { + var get = "bar"; + var a = { + bar() { + return this.get; + }, + 5() { + return "five"; + }, + 0xf55() { + return "f five five"; + }, + "five"() { + return 5; + }, + 0b1010(value) { + this._ten = value; + } + }; + } + expect: { + var get = "bar"; + var a = { + bar() { + return this.get; + }, + 5() { + return "five"; + }, + 0xf55() { + return "f five five"; + }, + "five"() { + return 5; + }, + 0b1010(value) { + this._ten = value; + } + }; + } +} + concise_methods_and_mangle_props: { mangle_props = { regex: /_/ @@ -218,7 +313,6 @@ concise_methods_and_keyword_names: { } } - getter_setter_with_computed_value: { input: { class C { @@ -339,3 +433,38 @@ property_with_unprintable_ascii_only: { } expect_exact: 'var foo={"\\0\\x01":"foo",get"\\0\\x01"(){return"bar"},set"\\0\\x01"(foo){save(foo)},*"\\0\\x01"(){return"foobar"}};class bar{get"\\0\\x01"(){return"bar"}set"\\0\\x01"(foo){save(foo)}*"\\0\\x01"(){return"foobar"}}' } + +property_with_unprintable_ascii_only_static: { + beautify = { + ascii_only: true + } + input: { + class foo { + static get "\x02\x03"() { + return "bar"; + } + static set "\x04\x05"(foo) { + save(foo); + } + } + } + expect_exact: 'class foo{static get"\\x02\\x03"(){return"bar"}static set"\\x04\\x05"(foo){save(foo)}}' +} + +methods_and_getters_with_keep_quoted_props_enabled: { + beautify = { + quote_style: 3, + keep_quoted_props: true, + } + input: { + var obj = { + a() {}, + "b"() {}, + get c() { return "c"}, + get "d"() { return "d"}, + set e(a) { doSomething(a); }, + set f(a) { doSomething(b); } + } + } + expect_exact: 'var obj={a(){},"b"(){},get c(){return"c"},get"d"(){return"d"},set e(a){doSomething(a)},set f(a){doSomething(b)}};' +} diff --git a/test/mocha/class.js b/test/mocha/class.js index 0df67e6a..6100afb9 100644 --- a/test/mocha/class.js +++ b/test/mocha/class.js @@ -23,4 +23,35 @@ describe("Class", function() { assert.throws(test(tests[i]), error); } }); + + it("Should return the correct token for class methods", function() { + var tests = [ + { + code: "class foo{static test(){}}", + token_value_start: "static", + token_value_end: "}" + }, + { + code: "class bar{*procedural(){}}", + token_value_start: "*", + token_value_end: "}" + }, + { + code: "class foobar{aMethod(){}}", + token_value_start: "aMethod", + token_value_end: "}" + }, + { + code: "class foobaz{get something(){}}", + token_value_start: "get", + token_value_end: "}" + } + ]; + + for (var i = 0; i < tests.length; i++) { + var ast = uglify.parse(tests[i].code); + assert.strictEqual(ast.body[0].properties[0].start.value, tests[i].token_value_start); + assert.strictEqual(ast.body[0].properties[0].end.value, tests[i].token_value_end); + } + }); }); From 1db50c3b169ee4195e1935013d6721628eb5b4bd Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Thu, 1 Sep 2016 09:35:31 +0200 Subject: [PATCH 104/121] Don't parenthesize arrow functions in parameter lists --- lib/output.js | 3 ++- test/compress/harmony.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index d0238da8..81712d21 100644 --- a/lib/output.js +++ b/lib/output.js @@ -597,6 +597,7 @@ function OutputStream(options) { || p instanceof AST_ObjectProperty // { foo: (1, 2) }.foo ==> 2 || p instanceof AST_Conditional /* (false, true) ? (a = 10, b = 20) : (c = 30) * ==> 20 (side effect, set a := 10 and b := 20) */ + || p instanceof AST_Arrow // x => (x, x) ; }); @@ -944,7 +945,7 @@ function OutputStream(options) { var parent = output.parent(); var needs_parens = parent instanceof AST_Binary || parent instanceof AST_Unary || - parent instanceof AST_Call; + (parent instanceof AST_Call && self === parent.expression); if (needs_parens) { output.print("(") } if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol && !self.argnames[0].default) { self.argnames[0].print(output); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 122069dd..e13b27e0 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -390,4 +390,15 @@ regression_cannot_use_of: { x.of; foo(); /* Label statement missing? No prob. */ } -} \ No newline at end of file +} + +fat_arrow_as_param: { + input: { + foo(x => x); + foo(x => x, y => y); + + foo(x => (x, x)); + foo(x => (x, x), y => (y, y)); + } + expect_exact: "foo(x=>x);foo(x=>x,y=>y);foo(x=>(x,x));foo(x=>(x,x),y=>(y,y));" +} From 13ed445607125a045e979df8abaa3d02fee9eb24 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 20 Aug 2016 22:32:29 +0200 Subject: [PATCH 105/121] Improve support for binding pattern Including improvements for parameters, variable assignment and catch parameter. --- lib/ast.js | 3 +- lib/output.js | 16 ++ lib/parse.js | 320 ++++++++++++++++++++++++++++----- test/compress/destructuring.js | 57 +++++- test/compress/harmony.js | 116 ------------ test/compress/parameters.js | 159 ++++++++++++++++ test/compress/try-catch.js | 9 + test/mocha/class.js | 2 +- test/mocha/function.js | 162 ++++++++++++++++- test/mocha/lhs-expressions.js | 29 +++ test/mocha/try.js | 22 +++ test/mocha/yield.js | 2 +- test/parser.js | 138 -------------- test/run-tests.js | 4 - 14 files changed, 727 insertions(+), 312 deletions(-) create mode 100644 test/compress/parameters.js create mode 100644 test/compress/try-catch.js create mode 100644 test/mocha/lhs-expressions.js create mode 100644 test/mocha/try.js delete mode 100644 test/parser.js diff --git a/lib/ast.js b/lib/ast.js index a3a894c5..d117f320 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -989,7 +989,8 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { key: "[string|AST_Node] the property name converted to a string for ObjectKeyVal. For setters, getters and computed property this is an arbitrary AST_Node", - value: "[AST_Node] property value. For setters and getters this is an AST_Function." + value: "[AST_Node] property value. For setters and getters this is an AST_Function.", + default: "[AST_Expression] The default for this parameter, only used when nested inside a binding pattern" }, _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index 81712d21..232723aa 100644 --- a/lib/output.js +++ b/lib/output.js @@ -595,6 +595,10 @@ function OutputStream(options) { || p instanceof AST_PropAccess // (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2 || p instanceof AST_Array // [ 1, (2, 3), 4 ] ==> [ 1, 3, 4 ] || p instanceof AST_ObjectProperty // { foo: (1, 2) }.foo ==> 2 + || (p instanceof AST_SymbolConst && p.default === this) // const { xCover = (0, function() {}) } + || (p instanceof AST_SymbolLet && p.default === this) // let { xCover = (0, function() {}) } + || (p instanceof AST_SymbolVar && p.default === this) // var { xCover = (0, function() {}) } + || (p instanceof AST_SymbolCatch && p.default === this) // } catch (xCover = (0, function() {}) ) { || p instanceof AST_Conditional /* (false, true) ? (a = 10, b = 20) : (c = 30) * ==> 20 (side effect, set a := 10 and b := 20) */ || p instanceof AST_Arrow // x => (x, x) @@ -1451,6 +1455,12 @@ function OutputStream(options) { self.print_property_name(self.key, self.quote, output); output.colon(); self.value.print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output); + } }); AST_ObjectProperty.DEFMETHOD("_print_getter_setter", function(type, self, output) { if (self.static) { @@ -1498,6 +1508,12 @@ function OutputStream(options) { output.print("]:"); output.space(); self.value.print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output); + } }); AST_Symbol.DEFMETHOD("_do_print", function(output){ var def = this.definition(); diff --git a/lib/parse.js b/lib/parse.js index 80691da4..442a4164 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -46,7 +46,7 @@ var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'enum implements interface package private protected public static super this' + KEYWORDS_ATOM + " " + KEYWORDS; +var RESERVED_WORDS = 'enum implements interface package private protected public static super this ' + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case yield'; KEYWORDS = makePredicate(KEYWORDS); @@ -1266,7 +1266,7 @@ function parse($TEXT, options) { if (in_statement && !name) unexpected(); - var args = params_or_seq_().as_params(croak); + var args = parameters(); var body = _function_body(true, is_generator || is_generator_property); return new ctor({ start : args.start, @@ -1278,6 +1278,276 @@ function parse($TEXT, options) { }); }; + function track_used_binding_identifiers(is_parameter, strict) { + var parameters = {}; + var duplicate = false; + var default_assignment = false; + var spread = false; + var strict_mode = !!strict; + var tracker = { + add_parameter: function(token) { + if (parameters["$" + token.value] !== undefined) { + if (duplicate === false) { + duplicate = token; + } + tracker.check_strict(); + } else { + parameters["$" + token.value] = true; + if (is_parameter) { + switch (token.value) { + case "arguments": + case "eval": + case "yield": + if (strict_mode) { + token_error(token, "SyntaxError: Unexpected " + token.value + " identifier as parameter inside strict mode"); + } + break; + default: + if (RESERVED_WORDS(token.value)) { + unexpected(); + } + } + } + } + }, + mark_default_assignment: function(token) { + if (default_assignment === false) { + default_assignment = token; + } + }, + mark_spread: function(token) { + if (spread === false) { + spread = token; + } + }, + mark_strict_mode: function() { + strict_mode = true; + }, + is_strict: function() { + return default_assignment !== false || spread !== false || strict_mode + }, + check_strict: function() { + if (tracker.is_strict() && duplicate !== false) { + token_error(duplicate, "SyntaxError: Parameter " + duplicate.value + " was used already"); + } + } + }; + + return tracker; + } + + function parameters() { + var start = S.token; + var first = true; + var params = []; + var used_parameters = track_used_binding_identifiers(true, S.input.has_directive("use strict")); + + expect("("); + + while (!is("punc", ")")) { + if (first) { + first = false; + } else { + expect(","); + } + + var param = parameter(used_parameters); + params.push(param); + + if (param instanceof AST_Expansion) { + break; + } + } + + next(); + return params; + } + + function parameter(used_parameters, symbol_type) { + var param; + var expand = false; + if (used_parameters === undefined) { + used_parameters = track_used_binding_identifiers(true, S.input.has_directive("use strict")); + } + if (is("expand", "...")) { + expand = S.token; + used_parameters.mark_spread(S.token); + next(); + } + param = binding_element(used_parameters, symbol_type); + + if (is("operator", "=") && expand === false) { + used_parameters.mark_default_assignment(S.token); + next(); + param.default = expression(false); + } + + if (expand !== false) { + if (!is("punc", ")")) { + unexpected(); + } + param = new AST_Expansion({ + start: expand, + expression: param, + end: expand + }); + } + used_parameters.check_strict(); + + return param; + } + + function binding_element(used_parameters, symbol_type) { + var elements = []; + var first = true; + var is_expand = false; + var expand_token; + var first_token = S.token; + if (used_parameters === undefined) { + used_parameters = track_used_binding_identifiers(false, S.input.has_directive("use strict")); + } + symbol_type = symbol_type === undefined ? AST_SymbolFunarg : symbol_type; + if (is("punc", "[")) { + next(); + while (!is("punc", "]")) { + if (first) { + first = false; + } else { + expect(","); + } + + if (is("expand", "...")) { + is_expand = true; + expand_token = S.token; + used_parameters.mark_spread(S.token); + next(); + } + if (is("punc")) { + switch (S.token.value) { + case ",": + case "]": // Last element + elements.push(new AST_Hole({ + start: S.token, + end: S.token + })); + continue; + case "[": + case "{": + elements.push(binding_element(used_parameters, symbol_type)); + break; + default: + unexpected(); + } + } else if (is("name")) { + used_parameters.add_parameter(S.token); + elements.push(new symbol_type({ + start: S.token, + name: S.token.value, + end: S.token + })); + next(); + } else { + croak("SyntaxError: Invalid function parameter"); + } + if (is("operator", "=") && is_expand === false) { + used_parameters.mark_default_assignment(S.token); + next(); + elements[elements.length - 1].default = expression(false); + } + if (is_expand) { + if (!is("punc", "]")) { + unexpected(); // Must be last element + } + elements[elements.length - 1] = new AST_Expansion({ + start: expand_token, + expression: elements[elements.length - 1], + end: expand_token + }); + } + } + expect("]"); + used_parameters.check_strict(); + return new AST_Destructuring({ + start: first_token, + names: elements, + is_array: true, + end: prev() + }); + } else if (is("punc", "{")) { + next(); + while (!is("punc", "}")) { + if (first) { + first = false; + } else { + expect(","); + } + if (is("name") && (is_token(peek(), "punc") || is_token(peek(), "operator")) && [",", "}", "="].indexOf(peek().value) !== -1) { + used_parameters.add_parameter(S.token); + elements.push(new symbol_type({ + start: S.token, + name: S.token.value, + end: S.token + })); + next(); + } else if (is("punc", "}")) { + continue; // Allow trailing hole + } else { + var property_token = S.token; + var property = as_property_name(); + if (property === null) { + unexpected(prev()); + } else if (prev().type === "name" && !is("punc", ":")) { + elements.push(new AST_SymbolFunarg({ + start: prev(), + name: property_token, + end: prev() + })); + } else if (property instanceof AST_Node) { + expect(":"); + elements.push(new AST_ObjectComputedKeyVal({ + start: property_token, + key: property, + value: binding_element(used_parameters, symbol_type), + end: prev() + })); + } else { + expect(":"); + elements.push(new AST_ObjectKeyVal({ + start: property_token, + quote: property_token.quote, + end: prev(), + key: property, + value: binding_element(used_parameters, symbol_type) + })); + } + } + if (is("operator", "=")) { + used_parameters.mark_default_assignment(S.token); + next(); + elements[elements.length - 1].default = expression(false); + } + } + expect("}"); + used_parameters.check_strict(); + return new AST_Destructuring({ + start: first_token, + names: elements, + is_array: false, + end: prev() + }); + } else if (is("name")) { + used_parameters.add_parameter(S.token); + next(); + return new symbol_type({ + start: prev(), + name: prev().value, + end: prev() + }); + } else { + croak("SyntaxError: Invalid function parameter"); + } + } + function params_or_seq_() { var start = S.token expect("("); @@ -1434,7 +1704,7 @@ function parse($TEXT, options) { var start = S.token; next(); expect("("); - var name = as_symbol(AST_SymbolCatch); + var name = parameter(undefined, AST_SymbolCatch); expect(")"); bcatch = new AST_Catch({ start : start, @@ -1472,7 +1742,7 @@ function parse($TEXT, options) { if (is("punc", "{") || is("punc", "[")) { def = new AST_VarDef({ start: S.token, - name: destructuring_(sym_type), + name: binding_element(undefined ,sym_type), value: is("operator", "=") ? (expect_token("operator", "="), expression(false, no_in)) : null, end: prev() }); @@ -1492,48 +1762,6 @@ function parse($TEXT, options) { return a; }; - var destructuring_ = embed_tokens(function (sym_type) { - var is_array = is("punc", "["); - var closing = is_array ? ']' : '}'; - var sym_type = sym_type || AST_SymbolRef; - - next(); - - var first = true, children = []; - while (!is("punc", closing)) { - if (first) first = false; else expect(","); - if (is("punc", closing)) break; - if (is("punc", ",")) { - children.push(new AST_Hole({ start: S.token, end: S.token })); - } else if (is("punc", "[") || is("punc", "{")) { - children.push(destructuring_(sym_type)); - } else if (is("expand", "...")) { - next(); - var symbol = _make_symbol(sym_type); - children.push(new AST_Expansion({ - start: prev(), - expression: symbol, - end: S.token - })); - next(); - } else if (is("name")) { - children.push(new (sym_type)({ - name : String(S.token.value), - start : S.token, - default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, - end : S.token - })); - } else { - children.push(expression()); - } - } - next() - return new AST_Destructuring({ - names: children, - is_array: is_array - }) - }); - var var_ = function(no_in) { return new AST_Var({ start : prev(), diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 6ea54ac1..35be0a2c 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -1,27 +1,75 @@ - destructuring_arrays: { input: { + {const [aa, bb] = cc;} + {const [aa, [bb, cc]] = dd;} + {let [aa, bb] = cc;} + {let [aa, [bb, cc]] = dd;} var [aa, bb] = cc; + var [aa, [bb, cc]] = dd; + var [,[,,,,,],,,zz,] = xx; } expect: { - var[aa,bb]=cc; + {const [aa, bb] = cc;} + {const [aa, [bb, cc]] = dd;} + {let [aa, bb] = cc;} + {let [aa, [bb, cc]] = dd;} + var [aa, bb] = cc; + var [aa, [bb, cc]] = dd; + var [,[,,,,,],,,zz,] = xx; } } destructuring_objects: { input: { + {const {aa, bb} = {aa:1, bb:2};} + {const {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}};} + {let {aa, bb} = {aa:1, bb:2};} + {let {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}};} var {aa, bb} = {aa:1, bb:2}; + var {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}}; } expect: { - var{aa,bb}={aa:1,bb:2}; + {const {aa, bb} = {aa:1, bb:2};} + {const {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}};} + {let {aa, bb} = {aa:1, bb:2};} + {let {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}};} + var {aa, bb} = {aa:1, bb:2}; + var {aa, bb: {cc, dd}} = {aa:1, bb: {cc:2, dd: 3}}; } } +destructuring_objects_trailing_elision: { + input: { + var {cc,} = foo; + } + expect_exact: "var{cc}=foo;" +} + nested_destructuring_objects: { input: { + const [{a},b] = c; + let [{a},b] = c; var [{a},b] = c; } - expect_exact: 'var[{a},b]=c;'; + expect_exact: 'const[{a},b]=c;let[{a},b]=c;var[{a},b]=c;'; +} + +destructuring_constdef_in_loops: { + input: { + for (const [x,y] in pairs); + for (const [a] = 0;;); + for (const {c} of cees); + } + expect_exact: "for(const[x,y]in pairs);for(const[a]=0;;);for(const{c}of cees);" +} + +destructuring_letdef_in_loops: { + input: { + for (let [x,y] in pairs); + for (let [a] = 0;;); + for (let {c} of cees); + } + expect_exact: "for(let[x,y]in pairs);for(let[a]=0;;);for(let{c}of cees);" } destructuring_vardef_in_loops: { @@ -32,6 +80,7 @@ destructuring_vardef_in_loops: { } expect_exact: "for(var[x,y]in pairs);for(var[a]=0;;);for(var{c}of cees);" } + destructuring_expressions: { input: { ({a, b}); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e13b27e0..dc74b36e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -1,17 +1,3 @@ -arrow_functions: { - input: { - (a) => b; // 1 args - (a, b) => c; // n args - () => b; // 0 args - (a) => (b) => c; // func returns func returns func - (a) => ((b) => c); // So these parens are dropped - () => (b,c) => d; // func returns func returns func - a=>{return b;} - a => 'lel'; // Dropping the parens - } - expect_exact: "a=>b;(a,b)=>c;()=>b;a=>b=>c;a=>b=>c;()=>(b,c)=>d;a=>{return b};a=>\"lel\";" -} - arrow_function_parens: { input: { something && (() => {}); @@ -25,28 +11,6 @@ arrow_function_parens_2: { expect_exact: "(()=>null)();" } -regression_arrow_functions_and_hoist: { - options = { - hoist_vars: true, - hoist_funs: true - } - input: { - (a) => b; - } - expect_exact: "a=>b;" -} - -regression_assign_arrow_functions: { - input: { - oninstall = e => false; - oninstall = () => false; - } - expect: { - oninstall=e=>false; - oninstall=()=>false; - } -} - typeof_arrow_functions: { options = { evaluate: true @@ -57,86 +21,6 @@ typeof_arrow_functions: { expect_exact: "var foo=\"function\";" } -destructuring_arguments: { - input: { - (function ( a ) { }); - (function ( [ a ] ) { }); - (function ( [ a, b ] ) { }); - (function ( [ [ a ] ] ) { }); - (function ( [ [ a, b ] ] ) { }); - (function ( [ a, [ b ] ] ) { }); - (function ( [ [ b ], a ] ) { }); - - (function ( { a } ) { }); - (function ( { a, b } ) { }); - - (function ( [ { a } ] ) { }); - (function ( [ { a, b } ] ) { }); - (function ( [ a, { b } ] ) { }); - (function ( [ { b }, a ] ) { }); - - ( [ a ] ) => { }; - ( [ a, b ] ) => { }; - - ( { a } ) => { }; - ( { a, b, c, d, e } ) => { }; - - ( [ a ] ) => b; - ( [ a, b ] ) => c; - - ( { a } ) => b; - ( { a, b } ) => c; - } - expect: { - (function(a){}); - (function([a]){}); - (function([a,b]){}); - (function([[a]]){}); - (function([[a,b]]){}); - (function([a,[b]]){}); - (function([[b],a]){}); - - (function({a}){}); - (function({a,b}){}); - - (function([{a}]){}); - (function([{a,b}]){}); - (function([a,{b}]){}); - (function([{b},a]){}); - - ([a])=>{}; - ([a,b])=>{}; - - ({a})=>{}; - ({a,b,c,d,e})=>{}; - - ([a])=>b; - ([a,b])=>c; - - ({a})=>b; - ({a,b})=>c; - } -} - -default_arguments: { - input: { - function x(a = 6) { } - function x(a = (6 + 5)) { } - function x({ foo } = {}, [ bar ] = [ 1 ]) { } - } - expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" -} - -default_values_in_destructurings: { - input: { - function x({a=(4), b}) {} - function x([b, c=(12)]) {} - var { x = (6), y } = x; - var [ x, y = (6) ] = x; - } - expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" -} - classes: { input: { class SomeClass { diff --git a/test/compress/parameters.js b/test/compress/parameters.js new file mode 100644 index 00000000..fd0149b0 --- /dev/null +++ b/test/compress/parameters.js @@ -0,0 +1,159 @@ +arrow_functions: { + input: { + (a) => b; // 1 args + (a, b) => c; // n args + () => b; // 0 args + (a) => (b) => c; // func returns func returns func + (a) => ((b) => c); // So these parens are dropped + () => (b,c) => d; // func returns func returns func + a=>{return b;} + a => 'lel'; // Dropping the parens + } + expect_exact: "a=>b;(a,b)=>c;()=>b;a=>b=>c;a=>b=>c;()=>(b,c)=>d;a=>{return b};a=>\"lel\";" +} + +regression_arrow_functions_and_hoist: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + (a) => b; + } + expect_exact: "a=>b;" +} + +regression_assign_arrow_functions: { + input: { + oninstall = e => false; + oninstall = () => false; + } + expect: { + oninstall=e=>false; + oninstall=()=>false; + } +} + +destructuring_arguments_1: { + input: { + (function ( a ) { }); + (function ( [ a ] ) { }); + (function ( [ a, b ] ) { }); + (function ( [ [ a ] ] ) { }); + (function ( [ [ a, b ] ] ) { }); + (function ( [ a, [ b ] ] ) { }); + (function ( [ [ b ], a ] ) { }); + + (function ( { a } ) { }); + (function ( { a, b } ) { }); + + (function ( [ { a } ] ) { }); + (function ( [ { a, b } ] ) { }); + (function ( [ a, { b } ] ) { }); + (function ( [ { b }, a ] ) { }); + + ( [ a ] ) => { }; + ( [ a, b ] ) => { }; + + ( { a } ) => { }; + ( { a, b, c, d, e } ) => { }; + + ( [ a ] ) => b; + ( [ a, b ] ) => c; + + ( { a } ) => b; + ( { a, b } ) => c; + } + expect: { + (function(a){}); + (function([a]){}); + (function([a,b]){}); + (function([[a]]){}); + (function([[a,b]]){}); + (function([a,[b]]){}); + (function([[b],a]){}); + + (function({a}){}); + (function({a,b}){}); + + (function([{a}]){}); + (function([{a,b}]){}); + (function([a,{b}]){}); + (function([{b},a]){}); + + ([a])=>{}; + ([a,b])=>{}; + + ({a})=>{}; + ({a,b,c,d,e})=>{}; + + ([a])=>b; + ([a,b])=>c; + + ({a})=>b; + ({a,b})=>c; + } +} + +destructuring_arguments_2: { + input: { + (function([]) {}); + (function({}) {}); + (function([,,,,,]) {}); + (function ([a, {b: c}]) {}); + (function ([...args]) {}); + (function ({x,}) {}); + class a { *method({ [thrower()]: x } = {}) {}}; + (function(a, b, c, d, [{e: [...f]}]){})(1, 2, 3, 4, [{e: [1, 2, 3]}]); + } + expect: { + (function([]) {}); + (function({}) {}); + (function([,,,,,]) {}); + (function ([a, {b: c}]) {}); + (function ([...args]) {}); + (function ({x,}) {}); + class a { *method({ [thrower()]: x } = {}) {}}; + (function(a, b, c, d, [{e: [...f]}]){})(1, 2, 3, 4, [{e: [1, 2, 3]}]); + } +} + +destructuring_arguments_3: { + input: { + function fn3({x: {y: {z: {} = 42}}}) {} + const { cover = (function () {}), xCover = (0, function() {}) } = {}; + let { cover = (function () {}), xCover = (0, function() {}) } = {}; + var { cover = (function () {}), xCover = (0, function() {}) } = {}; + } + expect_exact: "function fn3({x:{y:{z:{}=42}}}){}const{cover=function(){},xCover=(0,function(){})}={};let{cover=function(){},xCover=(0,function(){})}={};var{cover=function(){},xCover=(0,function(){})}={};" +} + +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" +} + +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + +accept_duplicated_parameters_in_non_strict_without_spread_or_default_assignment: { + input: { + function a(b, b){} + function b({c: test, c: test}){} + } + expect: { + function a(b, b){} + function b({c: test, c: test}){} + } +} diff --git a/test/compress/try-catch.js b/test/compress/try-catch.js new file mode 100644 index 00000000..52926614 --- /dev/null +++ b/test/compress/try-catch.js @@ -0,0 +1,9 @@ +catch_destructuring_with_sequence: { + input: { + try { + throw {}; + } catch ({xCover = (0, function() {})} ) { + } + } + expect_exact: "try{throw{}}catch({xCover=(0,function(){})}){}" +} diff --git a/test/mocha/class.js b/test/mocha/class.js index 6100afb9..a81d631a 100644 --- a/test/mocha/class.js +++ b/test/mocha/class.js @@ -16,7 +16,7 @@ describe("Class", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Unexpected token: expand (...)"; + e.message.substr(0, 31) === "SyntaxError: Unexpected token: "; } for (var i = 0; i < tests.length; i++) { diff --git a/test/mocha/function.js b/test/mocha/function.js index 34fc70b3..94c518ae 100644 --- a/test/mocha/function.js +++ b/test/mocha/function.js @@ -2,6 +2,101 @@ var assert = require("assert"); var uglify = require("../../"); describe("Function", function() { + it ("Should parse binding patterns correctly", function() { + // Function argument nodes are correct + function get_args(args) { + return args.map(function (arg) { + return [arg.TYPE, arg.name]; + }); + } + + // Destructurings as arguments + var destr_fun1 = uglify.parse('(function ({a, b}) {})').body[0].body; + var destr_fun2 = uglify.parse('(function ([a, [b]]) {})').body[0].body; + + assert.equal(destr_fun1.argnames.length, 1); + assert.equal(destr_fun2.argnames.length, 1); + + var destr_fun1 = uglify.parse('({a, b}) => null').body[0].body; + var destr_fun2 = uglify.parse('([a, [b]]) => null').body[0].body; + + assert.equal(destr_fun1.argnames.length, 1); + assert.equal(destr_fun2.argnames.length, 1); + + var destruct1 = destr_fun1.argnames[0]; + var destruct2 = destr_fun2.argnames[0]; + + assert(destruct1 instanceof uglify.AST_Destructuring); + assert(destruct2 instanceof uglify.AST_Destructuring); + assert(destruct2.names[1] instanceof uglify.AST_Destructuring); + + assert.equal(destruct1.start.value, '{'); + assert.equal(destruct1.end.value, '}'); + assert.equal(destruct2.start.value, '['); + assert.equal(destruct2.end.value, ']'); + + assert.equal(destruct1.is_array, false); + assert.equal(destruct2.is_array, true); + + var aAndB = [ + ['SymbolFunarg', 'a'], + ['SymbolFunarg', 'b'] + ]; + + assert.deepEqual( + [ + destruct1.names[0].TYPE, + destruct1.names[0].name], + aAndB[0]); + + assert.deepEqual( + [ + destruct2.names[1].names[0].TYPE, + destruct2.names[1].names[0].name + ], + aAndB[1]); + + assert.deepEqual( + get_args(destr_fun1.args_as_names()), + aAndB); + assert.deepEqual( + get_args(destr_fun2.args_as_names()), + aAndB); + + // Making sure we don't accidentally accept things which + // Aren't argument destructurings + + assert.throws(function () { + uglify.parse('(function ( { a, [ b ] } ) { })') + }); + + assert.throws(function () { + uglify.parse('(function (1) { })'); + }, /Invalid function parameter/); + + assert.throws(function () { + uglify.parse('(function (this) { })'); + }); + + assert.throws(function () { + uglify.parse('(function ([1]) { })'); + }, /Invalid function parameter/); + + assert.throws(function () { + uglify.parse('(function [a] { })'); + }); + + // generators + var generators_def = uglify.parse('function* fn() {}').body[0]; + assert.equal(generators_def.is_generator, true); + + assert.throws(function () { + uglify.parse('function* (){ }'); + }); + + var generators_yield_def = uglify.parse('function* fn() {\nyield remote();\}').body[0].body[0]; + assert.strictEqual(generators_yield_def.body.is_star, false); + }); it("Should not accept spread on non-last parameters", function() { var tests = [ "var a = function(...a, b) { return a.join(b) }", @@ -20,11 +115,76 @@ describe("Function", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Unexpected token: expand (...)"; + e.message.substr(0, 31) === "SyntaxError: Unexpected token: "; } for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } }); + it("Should not accept empty parameters after elision", function() { + var tests = [ + "(function(,){})()", + "(function(a,){})()", + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "SyntaxError: Invalid function parameter"; + } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); + it("Should not accept an initializer when parameter is a rest parameter", function() { + var tests = [ + "(function(...a = b){})()", + "(function(a, ...b = [c, d]))" + ]; + var test = function(code) { + return function () { + uglify.parse(code, {fromString: true}); + } + } + var error = function (e) { + return e instanceof uglify.JS_Parse_Error; + } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error, tests[i]); + } + }); + it("Shoult not accept duplicated identifiers inside parameters in strict mode or when using default assigment or spread", function() { + // From: ES2016 9.2.12 FunctionDeclarationInstantiation (func, argumentsList) + // NOTE Early errors ensure that duplicate parameter names can only occur + // in non-strict functions that do not have parameter default values or + // rest parameters. + var tests = [ + "(function(a = 1, a){})()", + "(function(a, [a = 3]){})()", + "(function(a, b, c, d, [{e: [...a]}]){})()", + "'use strict'; (function(a, a){})", + "(function({a, a = b}))", + "(function(a, [...a]){})", + "(function(a, ...a){})", + "(function(a, [a, ...b]){})", + "(function(a, {b: a, c: [...d]}){})", + "(function(a, a, {b: [...c]}){})" + ]; + var test = function(code) { + return function () { + uglify.parse(code, {fromString: true}); + } + } + var error = function (e) { + return e instanceof uglify.JS_Parse_Error && + /^SyntaxError: Parameter [a-zA-Z]+ was used already$/.test(e.message); + } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error, tests[i]); + } + }); }); diff --git a/test/mocha/lhs-expressions.js b/test/mocha/lhs-expressions.js new file mode 100644 index 00000000..1edd44a2 --- /dev/null +++ b/test/mocha/lhs-expressions.js @@ -0,0 +1,29 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Left-hand side expressions", function () { + it("Should parse destructuring with const/let/var correctly", function () { + var decls = uglify.parse('var {a,b} = foo, { c, d } = bar'); + + assert.equal(decls.body[0].TYPE, 'Var'); + assert.equal(decls.body[0].definitions.length, 2); + assert.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); + assert.equal(decls.body[0].definitions[0].value.TYPE, 'SymbolRef'); + + var nested_def = uglify.parse('var [{x}] = foo').body[0].definitions[0]; + + assert.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar'); + assert.equal(nested_def.name.names[0].names[0].name, 'x'); + + var holey_def = uglify.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; + + assert.equal(holey_def.name.names[0].TYPE, 'Hole'); + assert.equal(holey_def.name.names[2].TYPE, 'SymbolConst'); + + var expanding_def = uglify.parse('var [first, ...rest] = [1,2,3]').body[0].definitions[0]; + + assert.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); + assert.equal(expanding_def.name.names[1].TYPE, 'Expansion'); + assert.equal(expanding_def.name.names[1].expression.TYPE, 'SymbolVar'); + }); +}); diff --git a/test/mocha/try.js b/test/mocha/try.js new file mode 100644 index 00000000..a447907d --- /dev/null +++ b/test/mocha/try.js @@ -0,0 +1,22 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Try", function() { + it("Should not allow catch with an empty parameter", function() { + var tests = [ + "try {} catch() {}" + ]; + + var test = function(code) { + return function () { + uglify.parse(code, {fromString: true}); + } + } + var error = function (e) { + return e instanceof uglify.JS_Parse_Error; + } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error, tests[i]); + } + }); +}); diff --git a/test/mocha/yield.js b/test/mocha/yield.js index 1211ea9c..676ed832 100644 --- a/test/mocha/yield.js +++ b/test/mocha/yield.js @@ -90,7 +90,7 @@ describe("Yield", function() { var fail = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "SyntaxError: Unexpected yield identifier inside strict mode"; + /SyntaxError: Unexpected yield identifier (?:as parameter )?inside strict mode/.test(e.message); } var test = function(input) { diff --git a/test/parser.js b/test/parser.js deleted file mode 100644 index 84f8755c..00000000 --- a/test/parser.js +++ /dev/null @@ -1,138 +0,0 @@ - -var UglifyJS = require(".."); -var ok = require('assert'); - -module.exports = function () { - console.log("--- Parser tests"); - - // Destructuring arguments - - // Function argument nodes are correct - function get_args(args) { - return args.map(function (arg) { - return [arg.TYPE, arg.name]; - }); - } - - // Destructurings as arguments - var destr_fun1 = UglifyJS.parse('(function ({a, b}) {})').body[0].body; - var destr_fun2 = UglifyJS.parse('(function ([a, [b]]) {})').body[0].body; - - ok.equal(destr_fun1.argnames.length, 1); - ok.equal(destr_fun2.argnames.length, 1); - - var destr_fun1 = UglifyJS.parse('({a, b}) => null').body[0].body; - var destr_fun2 = UglifyJS.parse('([a, [b]]) => null').body[0].body; - - ok.equal(destr_fun1.argnames.length, 1); - ok.equal(destr_fun2.argnames.length, 1); - - var destruct1 = destr_fun1.argnames[0]; - var destruct2 = destr_fun2.argnames[0]; - - ok(destruct1 instanceof UglifyJS.AST_Destructuring); - ok(destruct2 instanceof UglifyJS.AST_Destructuring); - ok(destruct2.names[1] instanceof UglifyJS.AST_Destructuring); - - ok.equal(destruct1.start.value, '{'); - ok.equal(destruct1.end.value, '}'); - ok.equal(destruct2.start.value, '['); - ok.equal(destruct2.end.value, ']'); - - ok.equal(destruct1.is_array, false); - ok.equal(destruct2.is_array, true); - - var aAndB = [ - ['SymbolFunarg', 'a'], - ['SymbolFunarg', 'b'] - ]; - - ok.deepEqual( - [ - destruct1.names[0].TYPE, - destruct1.names[0].name], - aAndB[0]); - - ok.deepEqual( - [ - destruct2.names[1].names[0].TYPE, - destruct2.names[1].names[0].name - ], - aAndB[1]); - - ok.deepEqual( - get_args(destr_fun1.args_as_names()), - aAndB) - ok.deepEqual( - get_args(destr_fun2.args_as_names()), - aAndB) - - // Making sure we don't accidentally accept things which - // Aren't argument destructurings - - ok.throws(function () { - UglifyJS.parse('(function ([]) {})'); - }, /Invalid destructuring function parameter/); - - ok.throws(function () { - UglifyJS.parse('(function ( { a, [ b ] } ) { })') - }); - - ok.throws(function () { - UglifyJS.parse('(function (1) { })'); - }, /Invalid function parameter/); - - ok.throws(function () { - UglifyJS.parse('(function (this) { })'); - }); - - ok.throws(function () { - UglifyJS.parse('(function ([1]) { })'); - }, /Invalid function parameter/); - - ok.throws(function () { - UglifyJS.parse('(function [a] { })'); - }); - - // Destructuring variable declaration - - var decls = UglifyJS.parse('var {a,b} = foo, { c, d } = bar'); - - ok.equal(decls.body[0].TYPE, 'Var'); - ok.equal(decls.body[0].definitions.length, 2); - ok.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); - ok.equal(decls.body[0].definitions[0].value.TYPE, 'SymbolRef'); - - var nested_def = UglifyJS.parse('var [{x}] = foo').body[0].definitions[0]; - - ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar'); - ok.equal(nested_def.name.names[0].names[0].name, 'x'); - - var holey_def = UglifyJS.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; - - ok.equal(holey_def.name.names[0].TYPE, 'Hole'); - ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst'); - - var expanding_def = UglifyJS.parse('var [first, ...rest] = [1,2,3]').body[0].definitions[0]; - - ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); - ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); - ok.equal(expanding_def.name.names[1].expression.TYPE, 'SymbolVar'); - - // generators - var generators_def = UglifyJS.parse('function* fn() {}').body[0]; - ok.equal(generators_def.is_generator, true); - - ok.throws(function () { - UglifyJS.parse('function* (){ }'); - }); - - var generators_yield_def = UglifyJS.parse('function* fn() {\nyield remote();\}').body[0].body[0]; - ok.strictEqual(generators_yield_def.body.is_star, false); -} - -// Run standalone -if (module.parent === null) { - module.exports(); -} - diff --git a/test/run-tests.js b/test/run-tests.js index dae179e6..4063870a 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -30,10 +30,6 @@ run_ast_conversion_tests({ iterations: 1000 }); -var run_parser_tests = require('./parser.js'); - -run_parser_tests(); - /* -----[ utils ]----- */ function tmpl() { From 4198095a9c755fb9051292d0ac74eb92b7c5271e Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 29 Sep 2016 13:25:17 -0400 Subject: [PATCH 106/121] [ES6] fix parsing spread arguments that are expressions --- lib/parse.js | 2 +- test/compress/expansions.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 442a4164..5093812f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2371,7 +2371,7 @@ function parse($TEXT, options) { next(); args.push(new AST_Expansion({ start: prev(), - expression: as_symbol(AST_SymbolFunarg) + expression: expression(false) })); } else { args.push(expression(false)); diff --git a/test/compress/expansions.js b/test/compress/expansions.js index a6537547..8879e67f 100644 --- a/test/compress/expansions.js +++ b/test/compress/expansions.js @@ -7,6 +7,17 @@ expand_arguments: { expect_exact: "func(a,...rest);func(...all);" } +expand_expression_arguments: { + input: { + f(...a.b); + f(...a.b()); + f(...(a)); + f(...(a.b)); + f(...a[i]); + } + expect_exact: "f(...a.b);f(...a.b());f(...a);f(...a.b);f(...a[i]);" +} + expand_parameters: { input: { (function (a, ...b){}); From 88f6ff38d156fac213191e9e5cbe735819686a08 Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 21 Sep 2016 08:39:29 -0400 Subject: [PATCH 107/121] [ES6] fix template string escaping of \${...} --- lib/output.js | 2 +- test/compress/template-string.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/output.js b/lib/output.js index 232723aa..cacd5f2f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -409,7 +409,7 @@ function OutputStream(options) { print(encoded); }, print_template_string_chars: function(str) { - var encoded = encode_string(str, '`'); + var encoded = encode_string(str, '`').replace(/\${/g, "\\${"); return print(encoded.substr(1, encoded.length - 2)); }, encode_string : encode_string, diff --git a/test/compress/template-string.js b/test/compress/template-string.js index df4ff897..20f7f0c4 100644 --- a/test/compress/template-string.js +++ b/test/compress/template-string.js @@ -329,3 +329,15 @@ check_escaped_chars: { } expect_exact: "var foo=` `;"; } + +escape_dollar_curly: { + options = { + evaluate: true + } + input: { + console.log(`\$\{ beep \}`) + console.log(`${1-0}\${2-0}$\{3-0}${4-0}`) + console.log(`$${""}{not an expression}`) + } + expect_exact: "console.log(`\\${ beep }`);console.log(`1\\${2-0}\\${3-0}4`);console.log(`\\${not an expression}`);" +} From 947b8750e8a47b552a51ec38e3954a1bbb15db19 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 11 Sep 2016 15:06:10 +0200 Subject: [PATCH 108/121] Make classes implicitly strict mode --- lib/ast.js | 5 +++- lib/parse.js | 5 ++++ test/compress/directives.js | 10 +++++++ test/compress/yield.js | 2 -- test/mocha/directives.js | 54 +++++++++++++++++++++++++++++++++++-- 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 test/compress/directives.js diff --git a/lib/ast.js b/lib/ast.js index d117f320..2042332d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1292,12 +1292,15 @@ TreeWalker.prototype = { this.directives = Object.create(this.directives); } else if (node instanceof AST_Directive) { this.directives[node.value] = this.directives[node.value] ? "up" : true; + } else if (node instanceof AST_Class) { + this.directives = Object.create(this.directives); + this.directives["use strict"] = this.directives["use strict"] ? "up" : true; } this.stack.push(node); }, pop: function(node) { this.stack.pop(); - if (node instanceof AST_Lambda) { + if (node instanceof AST_Lambda || node instanceof AST_Class) { this.directives = Object.getPrototypeOf(this.directives); } }, diff --git a/lib/parse.js b/lib/parse.js index 5093812f..7c9b1cd5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2039,6 +2039,9 @@ function parse($TEXT, options) { function class_(KindOfClass) { var start, method, class_name, extends_, a = []; + S.input.push_directives_stack(); // Push directive stack, but not scope stack + S.input.add_directive("use strict"); + if (S.token.type == "name" && S.token.value != "extends") { class_name = as_symbol(KindOfClass === AST_DefClass ? AST_SymbolDefClass : AST_SymbolClass); } @@ -2063,6 +2066,8 @@ function parse($TEXT, options) { if (is("punc", ";")) { next(); } } + S.input.pop_directives_stack(); + next(); return new KindOfClass({ diff --git a/test/compress/directives.js b/test/compress/directives.js new file mode 100644 index 00000000..51587b95 --- /dev/null +++ b/test/compress/directives.js @@ -0,0 +1,10 @@ +class_directives_compression: { + input: { + class foo { + foo() { + "use strict"; + } + } + } + expect_exact: "class foo{foo(){}}" +} diff --git a/test/compress/yield.js b/test/compress/yield.js index c29ce926..4f28b647 100644 --- a/test/compress/yield.js +++ b/test/compress/yield.js @@ -120,7 +120,6 @@ yield_as_identifier_outside_strict_mode: { function foo(...yield){} try { new Error("") } catch (yield) {} var yield = "foo"; - class yield {} } expect: { import yield from "bar"; @@ -137,7 +136,6 @@ yield_as_identifier_outside_strict_mode: { function foo(...yield){} try { new Error("") } catch (yield) {} var yield = "foo"; - class yield {} } } diff --git a/test/mocha/directives.js b/test/mocha/directives.js index 82594758..f53ebc1a 100644 --- a/test/mocha/directives.js +++ b/test/mocha/directives.js @@ -62,10 +62,10 @@ describe("Directives", function() { it("Should know which strings are directive and which ones are not", function() { var test_directive = function(tokenizer, test) { test.directives.map(function(directive) { - assert.strictEqual(tokenizer.has_directive(directive), true, directive + " in " + test.input); + assert.strictEqual(tokenizer.has_directive(directive), true, "Didn't found directive `" + directive + "` at the end of `" + test.input + '`'); }); test.non_directives.map(function(fake_directive) { - assert.strictEqual(tokenizer.has_directive(fake_directive), false, fake_directive + " in " + test.input); + assert.strictEqual(tokenizer.has_directive(fake_directive), false, "Unexpectedly found directive `" + fake_directive + "` at the end of `" + test.input + '`'); }); } @@ -156,6 +156,16 @@ describe("Directives", function() { input: '"use strict";try{"use asm";', directives: ["use strict"], non_directives: ["use\nstrict", "use \nstrict", "use asm"] + }, + { + input: 'class foo {', + directives: ["use strict"], + non_directives: ["use\nstrict", "use asm"] + }, + { + input: 'class foo {}', + directives: [], + non_directives: ["use strict", "use asm", "use\nstrict"] } ]; @@ -367,4 +377,44 @@ describe("Directives", function() { ); } }); + it("Should be detect implicit usages of strict mode from tree walker", function() { + var tests = [ + { + input: 'class foo {bar(){_check_}}', + directives: ["use strict"], + non_directives: ["use bar"] + }, + { + input: 'class foo {bar(){}}_check_', + directives: [], + non_directives: ["use strict", "use bar"] + } + ]; + + var i = 0; + var checked; + var checkWalker = new uglify.TreeWalker(function(node, descend) { + if (node instanceof uglify.AST_Symbol && node.name === "_check_") { + checked = true; + for (var j = 0; j < tests[i].directives.length; j++) { + assert.equal(checkWalker.has_directive(tests[i].directives[j]), true, + "Did not found directive '" + tests[i].directives[j] + "' in test " + tests[i].input) + } + for (var k = 0; k < tests[i].non_directives.length; k++) { + assert.equal(checkWalker.has_directive(tests[i].non_directives[k]), undefined, + "Found directive '" + tests[i].non_directives[k] + "' in test " + tests[i].input) + } + } + }); + + for (; i < tests.length; i++) { + // Do tests - iterate the ast in each test - check only when _check_ occurs - fail when no _check_ has been found + checked = false; + var ast = uglify.parse(tests[i].input); + ast.walk(checkWalker); + if (!checked) { + throw "No _check_ symbol found in " + tests[i].input; + } + } + }); }); From 32c2cc33bbb05a70cd67106afb6913d4517d00de Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 7 Sep 2016 01:00:07 +0200 Subject: [PATCH 109/121] Improve binding patterns for arrow functions --- README.md | 3 ++ lib/ast.js | 49 +++++++++++++++------------- lib/output.js | 39 +++++++++++++++------- lib/parse.js | 15 +++++---- lib/scope.js | 8 +++-- test/compress/arrays.js | 2 ++ test/compress/arrow.js | 36 +++++++++++++++++++++ test/compress/destructuring.js | 18 +++++++++-- test/compress/object.js | 25 +++++++++++++- test/mocha/arrow.js | 39 ++++++++++++++++++++++ test/mocha/function.js | 59 +++++++++++++++++++++++++++++----- test/mocha/object.js | 4 +++ 12 files changed, 243 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 4f5b21a0..df7eda44 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,9 @@ can pass additional arguments that control the code output: - `3` -- always use the original quotes - `keep_quoted_props` (default `false`) -- when turned on, prevents stripping quotes from property names in object literals. +- `es` (default `5`) -- set output printing mode. This will only change the + output in direct control of the beautifier. Non-compatible features in the + abstract syntax tree will still be outputted as is. ### Keeping copyright notices or other comments diff --git a/lib/ast.js b/lib/ast.js index 2042332d..68ef352a 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -394,8 +394,6 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { var root = this; return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { - if (ex.properties.length == 0) - croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); return new AST_Destructuring({ start: ex.start, end: ex.end, @@ -403,12 +401,16 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.properties.map(to_fun_args) }); - } else if (ex instanceof AST_ObjectKeyVal && ex.shorthand) { - return new AST_SymbolFunarg({ - name: ex.key, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_ObjectKeyVal || + ex instanceof AST_ObjectComputedKeyVal + ) { + if (ex.key instanceof AST_SymbolRef) { + ex.key = to_fun_args(ex.key, 0, [ex.key], ex.default); + } + ex.value = to_fun_args(ex.value, 0, [ex.key], ex.default); + return ex; + } else if (ex instanceof AST_Hole) { + return ex; } else if (ex instanceof AST_Destructuring) { if (ex.names.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -424,8 +426,6 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_Expansion) { return ex; } else if (ex instanceof AST_Array) { - if (ex.elements.length === 0) - croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); return new AST_Destructuring({ start: ex.start, end: ex.end, @@ -494,6 +494,11 @@ var AST_Defun = DEFNODE("Defun", null, { /* -----[ DESTRUCTURING ]----- */ var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", + $propdoc: { + "names": "[AST_Destructuring|AST_Expansion|AST_Hole|AST_ObjectKeyVal|AST_Symbol] Array of properties or elements", + "is_array": "[Boolean] Whether the destructuring represents an object or array", + "default": "[AST_Node?] Default assign value" + }, _walk: function(visitor) { return visitor._visit(this, function(){ this.names.forEach(function(name){ @@ -1001,11 +1006,11 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote shorthand", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote default", { $documentation: "A key: value object property", $propdoc: { quote: "[string] the original quote character", - shorthand: "[boolean] whether this is a shorthand key:value pair, expressed as just the key." + default: "[AST_Expression] The default parameter value, only used when nested inside a binding pattern" } }, AST_ObjectProperty); @@ -1068,13 +1073,19 @@ var AST_ClassExpression = DEFNODE("ClassExpression", null, { $documentation: "A class expression." }, AST_Class); -var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { +var AST_Symbol = DEFNODE("Symbol", "scope name thedef default", { $propdoc: { name: "[string] name of this symbol", scope: "[AST_Scope/S] the current scope (not necessarily the definition scope)", - thedef: "[SymbolDef/S] the definition of this symbol" + thedef: "[SymbolDef/S] the definition of this symbol", + default: "[AST_Expression] The default parameter value, only used when nested inside a binding pattern" }, $documentation: "Base class for all symbols", + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }); var AST_NewTarget = DEFNODE("NewTarget", null, { @@ -1085,17 +1096,11 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration.", - default: "[AST_Expression] The default for this parameter. For example, `= 6`" + init: "[AST_Node*/S] array of initializers for this declaration." }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_Symbol); var AST_SymbolVar = DEFNODE("SymbolVar", null, { diff --git a/lib/output.js b/lib/output.js index cacd5f2f..d176d491 100644 --- a/lib/output.js +++ b/lib/output.js @@ -69,11 +69,16 @@ function OutputStream(options) { preamble : null, quote_style : 0, keep_quoted_props: false, + shorthand : undefined, ecma : 5, }, true); + if (typeof options.ascii_identifiers === 'undefined') options.ascii_identifiers = options.ascii_only; + if (options.shorthand === undefined) + options.shorthand = options.ecma > 5; + var indentation = 0; var current_col = 0; var current_line = 1; @@ -728,9 +733,15 @@ function OutputStream(options) { DEFPRINT(AST_Destructuring, function (self, output) { output.print(self.is_array ? "[" : "{"); var first = true; - self.names.forEach(function (name) { + var len = self.names.length; + self.names.forEach(function (name, i) { if (first) first = false; else { output.comma(); output.space(); } name.print(output); + // If the final element is a hole, we need to make sure it + // doesn't look like a trailing comma, by inserting an actual + // trailing comma. + if (i === len - 1 && name instanceof AST_Hole) + output.comma(); }) output.print(self.is_array ? "]" : "}"); if (self.default) { @@ -739,7 +750,7 @@ function OutputStream(options) { output.space(); self.default.print(output) } - }) + }); DEFPRINT(AST_Debugger, function(self, output){ output.print("debugger"); @@ -1444,17 +1455,21 @@ function OutputStream(options) { } }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ - var print_as_shorthand = self.shorthand && - self.value instanceof AST_Symbol && - self.key == self.value.print_to_string(); - - if (print_as_shorthand) { - output.print_name(self.key); - return; + function get_name(self) { + var def = self.value.definition(); + return def ? def.mangled_name || def.name : self.value.name; + } + if (output.option("shorthand") && + self.value instanceof AST_Symbol && + is_identifier_string(self.key) && + get_name(self) === self.key + ) { + self.print_property_name(self.key, self.quote, output); + } else { + self.print_property_name(self.key, self.quote, output); + output.colon(); + self.value.print(output); } - self.print_property_name(self.key, self.quote, output); - output.colon(); - self.value.print(output); if (self.default) { output.space(); output.print('='); diff --git a/lib/parse.js b/lib/parse.js index 7c9b1cd5..aa1bda45 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1232,6 +1232,10 @@ function parse($TEXT, options) { }; var arrow_function = function(args) { + if (S.token.nlb) { + croak("SyntaxError: Unexpected newline before arrow (=>)"); + } + expect_token("arrow", "=>"); var argnames; @@ -1425,12 +1429,13 @@ function parse($TEXT, options) { if (is("punc")) { switch (S.token.value) { case ",": - case "]": // Last element elements.push(new AST_Hole({ start: S.token, end: S.token })); continue; + case "]": // Trailing comma after last element + break; case "[": case "{": elements.push(binding_element(used_parameters, symbol_type)); @@ -1560,7 +1565,7 @@ function parse($TEXT, options) { next(); a.push(new AST_Expansion({ start: prev(), - expression: as_symbol(AST_SymbolFunarg), + expression: binding_element(undefined, AST_SymbolFunarg), end: S.token, })); if (!is("punc", ")")) { @@ -2011,15 +2016,13 @@ function parse($TEXT, options) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectKeyVal({ start: start, - quote: start.quote, - end: start, key: name, value: new AST_SymbolRef({ start: start, - end: start, + end: prev(), name: name }), - shorthand: true, + end: start, })); } else { expect(":"); diff --git a/lib/scope.js b/lib/scope.js index d2712256..056131db 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -567,10 +567,12 @@ AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ else if (node instanceof AST_With) base54.consider("with"); else if (node instanceof AST_ObjectSetter) - base54.consider("set" + node.key); + base54.consider("set" + (typeof node.key === "string" ? node.key : "")); else if (node instanceof AST_ObjectGetter) - base54.consider("get" + node.key); - else if (node instanceof AST_ObjectKeyVal) + base54.consider("get" + (typeof node.key === "string" ? node.key : "")); + else if (node instanceof AST_ObjectKeyVal && typeof node.key === "string") + base54.consider(node.key); + else if (node instanceof AST_ConciseMethod && typeof node.key === "string") base54.consider(node.key); else if (node instanceof AST_New) base54.consider("new"); diff --git a/test/compress/arrays.js b/test/compress/arrays.js index bc573083..fa98309f 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -1,3 +1,5 @@ +// NOTE trailing comma doesn't contribute to length of an array +// That also means the array changes length if previous element is a hole too and got cut off holes_and_undefined: { input: { w = [1,,]; diff --git a/test/compress/arrow.js b/test/compress/arrow.js index 1a8b17ed..fb9063a2 100644 --- a/test/compress/arrow.js +++ b/test/compress/arrow.js @@ -90,3 +90,39 @@ arrow_function_with_single_parameter_with_default: { } expect_exact: "var foo=(a=0)=>doSomething(a);" } + +arrow_binding_pattern: { + input: { + var foo = ([]) => "foo"; + var bar = ({}) => "bar"; + var with_default = (foo = "default") => foo; + var object_with_default = ({foo = "default", bar: baz = "default"}) => foo; + var array_after_spread = (...[foo]) => foo; + var array_after_spread = (...{foo}) => foo; + var computed = ({ [compute()]: x }) => {}; + var array_hole = ([, , ...x] = [1, 2]) => {}; + var object_trailing_elision = ({foo,}) => {}; + var spread_empty_array = (...[]) => "foo"; + var spread_empty_object = (...{}) => "foo"; + } + expect: { + var foo = ([]) => "foo"; + var bar = ({}) => "bar"; + var with_default = (foo = "default") => foo; + var object_with_default = ({foo = "default", bar: baz = "default"}) => foo; + var array_after_spread = (...[foo]) => foo; + var array_after_spread = (...{foo}) => foo; + var computed = ({ [compute()]: x }) => {}; + var array_hole = ([, , ...x] = [1, 2]) => {}; + var object_trailing_elision = ({foo,}) => {}; + var spread_empty_array = (...[]) => "foo"; + var spread_empty_object = (...{}) => "foo"; + } +} + +arrow_binding_pattern_strict: { + input: { + var foo = ([,]) => "foo"; + } + expect_exact: 'var foo=([,])=>"foo";' +} diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 35be0a2c..5468bb81 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -6,7 +6,8 @@ destructuring_arrays: { {let [aa, [bb, cc]] = dd;} var [aa, bb] = cc; var [aa, [bb, cc]] = dd; - var [,[,,,,,],,,zz,] = xx; + var [,[,,,,,],,,zz,] = xx; // Trailing comma + var [,,zzz,,] = xxx; // Trailing comma after hole } expect: { {const [aa, bb] = cc;} @@ -15,10 +16,20 @@ destructuring_arrays: { {let [aa, [bb, cc]] = dd;} var [aa, bb] = cc; var [aa, [bb, cc]] = dd; - var [,[,,,,,],,,zz,] = xx; + var [,[,,,,,],,,zz] = xx; + var [,,zzz,,] = xxx; } } +destructuring_arrays_holes: { + input: { + var [,,,,] = a; + var [,,b,] = c; + var [d,,] = e; + } + expect_exact: "var[,,,,]=a;var[,,b]=c;var[d,,]=e;" +} + destructuring_objects: { input: { {const {aa, bb} = {aa:1, bb:2};} @@ -82,6 +93,9 @@ destructuring_vardef_in_loops: { } destructuring_expressions: { + beautify = { + ecma: 6 + } input: { ({a, b}); [{a}]; diff --git a/test/compress/object.js b/test/compress/object.js index b493204a..c3ad2989 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -89,6 +89,9 @@ getter_setter: { getter_setter_mangler: { mangle = {} + beautify = { + ecma: 6 + } input: { function f(get,set) { return { @@ -102,7 +105,18 @@ getter_setter_mangler: { }; } } - expect_exact: "function f(t,e){return{get:t,set:e,get g(){},set s(t){},c,a:1,m(){}}}" + expect_exact: "function f(n,t){return{get:n,set:t,get g(){},set s(n){},c,a:1,m(){}}}" +} + +use_shorthand_opportunity: { + beautify = { + ecma: 6 + } + input: { + var foo = 123; + var obj = {foo: foo}; + } + expect_exact: "var foo=123;var obj={foo};" } computed_property_names: { @@ -468,3 +482,12 @@ methods_and_getters_with_keep_quoted_props_enabled: { } expect_exact: 'var obj={a(){},"b"(){},get c(){return"c"},get"d"(){return"d"},set e(a){doSomething(a)},set f(a){doSomething(b)}};' } + +allow_assignments_to_property_values: { + input: { + var foo = {123: foo = 123} = {foo: "456"}; + } + expect: { + var foo = {123: foo = 123} = {foo: "456"}; + } +} diff --git a/test/mocha/arrow.js b/test/mocha/arrow.js index a02286c2..ffd037d0 100644 --- a/test/mocha/arrow.js +++ b/test/mocha/arrow.js @@ -18,6 +18,45 @@ describe("Arrow functions", function() { e.message === "SyntaxError: Unexpected token: expand (...)"; } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); + it("Should not accept holes in object binding patterns, while still allowing a trailing elision", function() { + var tests = [ + "f = ({, , ...x} = [1, 2]) => {};" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "SyntaxError: Unexpected token: punc (,)"; + } + + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); + it("Should not accept newlines before arrow token", function() { + var tests = [ + "f = foo\n=> 'foo';", + "f = (foo, bar)\n=> 'foo';", + "f = ()\n=> 'foo';", + "foo((bar)\n=>'baz';);" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "SyntaxError: Unexpected newline before arrow (=>)"; + } + for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } diff --git a/test/mocha/function.js b/test/mocha/function.js index 94c518ae..2ae8a202 100644 --- a/test/mocha/function.js +++ b/test/mocha/function.js @@ -13,30 +13,39 @@ describe("Function", function() { // Destructurings as arguments var destr_fun1 = uglify.parse('(function ({a, b}) {})').body[0].body; var destr_fun2 = uglify.parse('(function ([a, [b]]) {})').body[0].body; + var destr_fun3 = uglify.parse('({a, b}) => null').body[0].body; + var destr_fun4 = uglify.parse('([a, [b]]) => null').body[0].body; assert.equal(destr_fun1.argnames.length, 1); assert.equal(destr_fun2.argnames.length, 1); - - var destr_fun1 = uglify.parse('({a, b}) => null').body[0].body; - var destr_fun2 = uglify.parse('([a, [b]]) => null').body[0].body; - - assert.equal(destr_fun1.argnames.length, 1); - assert.equal(destr_fun2.argnames.length, 1); + assert.equal(destr_fun3.argnames.length, 1); + assert.equal(destr_fun4.argnames.length, 1); var destruct1 = destr_fun1.argnames[0]; var destruct2 = destr_fun2.argnames[0]; + var destruct3 = destr_fun3.argnames[0]; + var destruct4 = destr_fun4.argnames[0]; assert(destruct1 instanceof uglify.AST_Destructuring); assert(destruct2 instanceof uglify.AST_Destructuring); + assert(destruct3 instanceof uglify.AST_Destructuring); + assert(destruct4 instanceof uglify.AST_Destructuring); assert(destruct2.names[1] instanceof uglify.AST_Destructuring); + assert(destruct4.names[1] instanceof uglify.AST_Destructuring); assert.equal(destruct1.start.value, '{'); assert.equal(destruct1.end.value, '}'); assert.equal(destruct2.start.value, '['); assert.equal(destruct2.end.value, ']'); + assert.equal(destruct3.start.value, '{'); + assert.equal(destruct3.end.value, '}'); + assert.equal(destruct4.start.value, '['); + assert.equal(destruct4.end.value, ']'); assert.equal(destruct1.is_array, false); assert.equal(destruct2.is_array, true); + assert.equal(destruct3.is_array, false); + assert.equal(destruct4.is_array, true); var aAndB = [ ['SymbolFunarg', 'a'], @@ -46,15 +55,43 @@ describe("Function", function() { assert.deepEqual( [ destruct1.names[0].TYPE, - destruct1.names[0].name], + destruct1.names[0].name + ], + aAndB[0]); + assert.deepEqual( + [ + destruct1.names[1].TYPE, + destruct1.names[1].name + ], + aAndB[1]); + assert.deepEqual( + [ + destruct2.names[0].TYPE, + destruct2.names[0].name + ], aAndB[0]); - assert.deepEqual( [ destruct2.names[1].names[0].TYPE, destruct2.names[1].names[0].name ], aAndB[1]); + assert.strictEqual(typeof destruct3.names[0].key, "string"); + assert.strictEqual(destruct3.names[0].key, "a"); + assert.strictEqual(typeof destruct3.names[1].key, "string"); + assert.strictEqual(destruct3.names[1].key, "b"); + assert.deepEqual( + [ + destruct4.names[0].TYPE, + destruct4.names[0].name + ], + aAndB[0]); + assert.deepEqual( + [ + destruct4.names[1].names[0].TYPE, + destruct4.names[1].names[0].name + ], + aAndB[1]); assert.deepEqual( get_args(destr_fun1.args_as_names()), @@ -62,6 +99,12 @@ describe("Function", function() { assert.deepEqual( get_args(destr_fun2.args_as_names()), aAndB); + assert.deepEqual( + get_args(destr_fun3.args_as_names()), + aAndB); + assert.deepEqual( + get_args(destr_fun4.args_as_names()), + aAndB); // Making sure we don't accidentally accept things which // Aren't argument destructurings diff --git a/test/mocha/object.js b/test/mocha/object.js index f5cd1a62..cabe8a9a 100644 --- a/test/mocha/object.js +++ b/test/mocha/object.js @@ -124,4 +124,8 @@ describe("Object", function() { assert.throws(testCase(test), fail(test), errorMessage(test)); } }); + it("Should be able to use shorthand properties", function() { + var ast = Uglify.parse("var foo = 123\nvar obj = {foo: foo}"); + assert.strictEqual(ast.print_to_string({ecma: 6}), "var foo=123;var obj={foo};"); + }) }); From 7e80a979a7f6bad21f54c63612dd4217ef5a7ca8 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Tue, 18 Oct 2016 16:18:34 +0200 Subject: [PATCH 110/121] Remove AST_ObjectComputedKeyVal --- lib/ast.js | 8 +------- lib/compress.js | 2 +- lib/output.js | 21 +++++++-------------- lib/parse.js | 10 +--------- 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 68ef352a..2b835e88 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -401,9 +401,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.properties.map(to_fun_args) }); - } else if (ex instanceof AST_ObjectKeyVal || - ex instanceof AST_ObjectComputedKeyVal - ) { + } else if (ex instanceof AST_ObjectKeyVal) { if (ex.key instanceof AST_SymbolRef) { ex.key = to_fun_args(ex.key, 0, [ex.key], ex.default); } @@ -1014,10 +1012,6 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote default", { } }, AST_ObjectProperty); -var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { - $documentation: "An object property whose key is computed. Like `[Symbol.iterator]: function...` or `[routes.homepage]: renderHomepage`", -}, AST_ObjectProperty); - var AST_ObjectSetter = DEFNODE("ObjectSetter", "quote static", { $propdoc: { quote: "[string|undefined] the original quote character, if any", diff --git a/lib/compress.js b/lib/compress.js index b5b6c263..69cfbe0b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1280,7 +1280,7 @@ merge(Compressor.prototype, { return false; }); def(AST_ObjectProperty, function(compressor){ - if (this instanceof AST_ObjectComputedKeyVal && + if (this.key instanceof AST_ObjectKeyVal && this.key.has_side_effects(compressor)) return true; return this.value.has_side_effects(compressor); diff --git a/lib/output.js b/lib/output.js index 5fac50b8..eb42d0fa 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1508,7 +1508,13 @@ function OutputStream(options) { ) { self.print_property_name(self.key, self.quote, output); } else { - self.print_property_name(self.key, self.quote, output); + if (!(self.key instanceof AST_Node) || self.key instanceof AST_Symbol) { + self.print_property_name(self.key, self.quote, output); + } else { + output.with_square(function() { + self.key.print(output); + }); + } output.colon(); self.value.print(output); } @@ -1559,19 +1565,6 @@ function OutputStream(options) { } self.value._do_print(output, true); }); - DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { - output.print("["); - self.key.print(output); - output.print("]:"); - output.space(); - self.value.print(output); - if (self.default) { - output.space(); - output.print('='); - output.space(); - self.default.print(output); - } - }); AST_Symbol.DEFMETHOD("_do_print", function(output){ var def = this.definition(); output.print_name(def ? def.mangled_name || def.name : this.name); diff --git a/lib/parse.js b/lib/parse.js index aa1bda45..8c710d04 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1507,14 +1507,6 @@ function parse($TEXT, options) { name: property_token, end: prev() })); - } else if (property instanceof AST_Node) { - expect(":"); - elements.push(new AST_ObjectComputedKeyVal({ - start: property_token, - key: property, - value: binding_element(used_parameters, symbol_type), - end: prev() - })); } else { expect(":"); elements.push(new AST_ObjectKeyVal({ @@ -1989,7 +1981,7 @@ function parse($TEXT, options) { if (type == "punc" && start.value == "[") { expect(":"); - a.push(new AST_ObjectComputedKeyVal({ + a.push(new AST_ObjectKeyVal({ start: start, key: name, value: expression(false), From 5f6825f9ecc7a98fc04b33851275c5eea30f9360 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 19 Oct 2016 15:34:26 +0200 Subject: [PATCH 111/121] Introduce is_block_scope to AST_Node to determine block scope. Will return false if AST_Node is instance of AST_Scope for now. --- lib/ast.js | 5 ++--- lib/scope.js | 17 ++++++++------- test/compress/block-scope.js | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 2b835e88..e09150c5 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -282,10 +282,9 @@ var AST_With = DEFNODE("With", "expression", { /* -----[ scope and functions ]----- */ -var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions uses_with uses_eval parent_scope enclosed cname", { +var AST_Scope = DEFNODE("Scope", "directives variables functions uses_with uses_eval parent_scope enclosed cname", { $documentation: "Base class for all statements introducing a lexical scope", $propdoc: { - is_block_scope: "[boolean] identifies a block scope", directives: "[string*/S] an array of directives declared in this scope", variables: "[Object/S] a map of name -> SymbolDef for all variables/functions defined in this scope", functions: "[Object/S] like `variables`, but only lists function declarations", @@ -297,7 +296,7 @@ var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions }, get_defun_scope: function () { var self = this; - while (self.is_block_scope && self.parent_scope) { + while (self.is_block_scope() && self.parent_scope) { self = self.parent_scope; } return self; diff --git a/lib/scope.js b/lib/scope.js index 17cd7222..b17d04e3 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -110,15 +110,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var in_destructuring = null; var in_export; var tw = new TreeWalker(function(node, descend){ - var create_a_block_scope = - (options.screw_ie8 && node instanceof AST_Catch) || - ((node instanceof AST_Block) && node.creates_block_scope()); - if (create_a_block_scope) { + if (node.is_block_scope()) { var save_scope = scope; scope = new AST_Scope(node); scope.init_scope_vars(nesting); scope.parent_scope = save_scope; - scope.is_block_scope = true; if (!(node instanceof AST_Scope)) { scope.uses_with = save_scope.uses_with; scope.uses_eval = save_scope.uses_eval; @@ -188,7 +184,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // instanceof AST_Scope) but we get to the symbol a bit // later. var parent_lambda = defun.parent_scope; - while (parent_lambda.is_block_scope) { + while (parent_lambda.is_block_scope()) { parent_lambda = parent_lambda.parent_scope; } (node.scope = parent_lambda).def_function(node, in_export); @@ -305,11 +301,16 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ this.nesting = nesting; // the nesting level of this scope (0 means toplevel) }); -AST_Block.DEFMETHOD("creates_block_scope", function() { +AST_Node.DEFMETHOD("is_block_scope", function(){ + return false; // Behaviour will be overridden by AST_Block +}); + +AST_Block.DEFMETHOD("is_block_scope", function(){ return ( !(this instanceof AST_Lambda) && !(this instanceof AST_Toplevel) && - !(this instanceof AST_Class) + !(this instanceof AST_Class) && + !(this instanceof AST_SwitchBranch) ); }); diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index dd243009..cc2c316f 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -130,3 +130,45 @@ regression_block_scope_resolves: { }()); } } + +switch_block_scope_mangler: { + mangle = {} + input: { + var fn = function(code) { + switch (code) { + case 1: + let apple = code + 1; + let dog = code + 4; + console.log(apple, dog); + break; + case 2: + let banana = code + 2; + console.log(banana); + break; + default: + let cat = code + 3; + console.log(cat); + } + }; + } + expect: { + var fn = function(o) { + switch (o) { + case 1: + let e = o + 1 + let c = o + 4; + console.log(e, c); + break; + + case 2: + let l = o + 2; + console.log(l); + break; + + default: + let a = o + 3; + console.log(a); + } + }; + } +} From c2112d588695791d71d6d362232be6c2b5641f90 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Mon, 24 Oct 2016 23:35:14 +0200 Subject: [PATCH 112/121] Fix case where a lonely var is used as computed property --- lib/output.js | 2 +- test/compress/object.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/output.js b/lib/output.js index eb42d0fa..83b3253b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1508,7 +1508,7 @@ function OutputStream(options) { ) { self.print_property_name(self.key, self.quote, output); } else { - if (!(self.key instanceof AST_Node) || self.key instanceof AST_Symbol) { + if (!(self.key instanceof AST_Node)) { self.print_property_name(self.key, self.quote, output); } else { output.with_square(function() { diff --git a/test/compress/object.js b/test/compress/object.js index c3ad2989..724e0877 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -491,3 +491,14 @@ allow_assignments_to_property_values: { var foo = {123: foo = 123} = {foo: "456"}; } } + +variable_as_computed_property: { + input: { + function getLine(header) { + return { + [header]: {} + }; + } + } + expect_exact: "function getLine(header){return{[header]:{}}}" +} \ No newline at end of file From 0aa526e72cb498fbf2e9f91cfe34944f869efb40 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 28 Oct 2016 20:42:28 +0200 Subject: [PATCH 113/121] Do not allow arrow functions in the middle of an expression --- lib/parse.js | 18 ++++++++++-------- test/mocha/arrow.js | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 8c710d04..88f2ef5a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2477,6 +2477,16 @@ function parse($TEXT, options) { return arrow_function([]); } + if (is("name") && is_token(peek(), "arrow")) { + var param = new AST_SymbolFunarg({ + name: start.value, + start: start, + end: start, + }); + next(); + return arrow_function([param]) + } + var left = maybe_conditional(no_in); var val = S.token.value; @@ -2493,14 +2503,6 @@ function parse($TEXT, options) { } croak("SyntaxError: Invalid assignment"); } - if (is("arrow")) { - left = new AST_SymbolFunarg({ - name: left.name, - start: left.start, - end: left.end, - }); - return arrow_function([left]) - } return left; }; diff --git a/test/mocha/arrow.js b/test/mocha/arrow.js index ffd037d0..15a8033a 100644 --- a/test/mocha/arrow.js +++ b/test/mocha/arrow.js @@ -57,6 +57,25 @@ describe("Arrow functions", function() { e.message === "SyntaxError: Unexpected newline before arrow (=>)"; } + for (var i = 0; i < tests.length; i++) { + assert.throws(test(tests[i]), error); + } + }); + it("Should not accept arrow functions in the middle or end of an expression", function() { + var tests = [ + "typeof x => 0", + "0 + x => 0" + ]; + var test = function(code) { + return function() { + uglify.parse(code, {fromString: true}); + } + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "SyntaxError: Unexpected token: arrow (=>)"; + } + for (var i = 0; i < tests.length; i++) { assert.throws(test(tests[i]), error); } From 1b2c02c944b5f5345a83431be6c67e3a38ee59c0 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 28 Oct 2016 23:47:03 +0200 Subject: [PATCH 114/121] Fix nlb property for template strings tokens starting with nlb Also add .gitattributes to checkout lf eol style --- .gitattributes | 1 + lib/parse.js | 6 +- test/compress/dead-code.js | 414 +++++++++++++++---------------- test/compress/template-string.js | 36 +++ 4 files changed, 247 insertions(+), 210 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6edcf864 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.js text eol=lf diff --git a/lib/parse.js b/lib/parse.js index 88f2ef5a..7288caab 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -497,10 +497,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { S.template_braces.push(S.brace_counter); } var content = "", raw = "", ch, tok; - next(); - while ((ch = next(true)) !== "`") { + next(true, true); + while ((ch = next(true, true)) !== "`") { if (ch === "$" && peek() === "{") { - next(); + next(true, true); S.brace_counter++; tok = token(begin ? "template_head" : "template_substitution", content); tok.begin = begin; diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 652645d9..1fb06167 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -1,94 +1,94 @@ -dead_code_1: { - options = { - dead_code: true - }; - input: { - function f() { - a(); - b(); - x = 10; - return; - if (x) { - y(); - } - } - } - expect: { - function f() { - a(); - b(); - x = 10; - return; - } - } -} - -dead_code_2_should_warn: { - options = { - dead_code: true - }; - input: { - function f() { - g(); - x = 10; - throw "foo"; - // completely discarding the `if` would introduce some - // bugs. UglifyJS v1 doesn't deal with this issue; in v2 - // we copy any declarations to the upper scope. - if (x) { - y(); - var x; - function g(){}; - // but nested declarations should not be kept. - (function(){ - var q; - function y(){}; - })(); - } - } - } - expect: { - function f() { - g(); - x = 10; - throw "foo"; - var x; - function g(){}; - } - } -} - -dead_code_constant_boolean_should_warn_more: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - while (!((foo && bar) || (x + "0"))) { - console.log("unreachable"); - var foo; - function bar() {} - } - for (var x = 10, y; x && (y || x) && (!typeof x); ++x) { - asdf(); - foo(); - var moo; - } - } - expect: { - var foo; - function bar() {} - // nothing for the while - // as for the for, it should keep: - var x = 10, y; - var moo; - } -} - -dead_code_block_decls_die: { +dead_code_1: { + options = { + dead_code: true + }; + input: { + function f() { + a(); + b(); + x = 10; + return; + if (x) { + y(); + } + } + } + expect: { + function f() { + a(); + b(); + x = 10; + return; + } + } +} + +dead_code_2_should_warn: { + options = { + dead_code: true + }; + input: { + function f() { + g(); + x = 10; + throw "foo"; + // completely discarding the `if` would introduce some + // bugs. UglifyJS v1 doesn't deal with this issue; in v2 + // we copy any declarations to the upper scope. + if (x) { + y(); + var x; + function g(){}; + // but nested declarations should not be kept. + (function(){ + var q; + function y(){}; + })(); + } + } + } + expect: { + function f() { + g(); + x = 10; + throw "foo"; + var x; + function g(){}; + } + } +} + +dead_code_constant_boolean_should_warn_more: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + while (!((foo && bar) || (x + "0"))) { + console.log("unreachable"); + var foo; + function bar() {} + } + for (var x = 10, y; x && (y || x) && (!typeof x); ++x) { + asdf(); + foo(); + var moo; + } + } + expect: { + var foo; + function bar() {} + // nothing for the while + // as for the for, it should keep: + var x = 10, y; + var moo; + } +} + +dead_code_block_decls_die: { options = { dead_code : true, conditionals : true, @@ -110,119 +110,119 @@ dead_code_block_decls_die: { } } -dead_code_const_declaration: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - const CONST_FOO = false; - if (CONST_FOO) { - console.log("unreachable"); - var moo; - function bar() {} - } - } - expect: { - var unused; - const CONST_FOO = !1; - var moo; - function bar() {} - } -} - -dead_code_const_annotation: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - /** @const */ var CONST_FOO_ANN = false; - if (CONST_FOO_ANN) { - console.log("unreachable"); - var moo; - function bar() {} - } - } - expect: { - var unused; - var CONST_FOO_ANN = !1; - var moo; - function bar() {} - } -} - -dead_code_const_annotation_regex: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - // @constraint this shouldn't be a constant - var CONST_FOO_ANN = false; - if (CONST_FOO_ANN) { - console.log("reachable"); - } - } - expect: { - var unused; - var CONST_FOO_ANN = !1; - CONST_FOO_ANN && console.log('reachable'); - } -} - -dead_code_const_annotation_complex_scope: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused_var; - /** @const */ var test = 'test'; - // @const - var CONST_FOO_ANN = false; - var unused_var_2; - if (CONST_FOO_ANN) { - console.log("unreachable"); - var moo; - function bar() {} - } - if (test === 'test') { - var beef = 'good'; - /** @const */ var meat = 'beef'; - var pork = 'bad'; - if (meat === 'pork') { - console.log('also unreachable'); - } else if (pork === 'good') { - console.log('reached, not const'); - } - } - } - expect: { - var unused_var; - var test = 'test'; - var CONST_FOO_ANN = !1; - var unused_var_2; - var moo; - function bar() {} - var beef = 'good'; - var meat = 'beef'; - var pork = 'bad'; - 'good' === pork && console.log('reached, not const'); - } -} +dead_code_const_declaration: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + const CONST_FOO = false; + if (CONST_FOO) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + const CONST_FOO = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + /** @const */ var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation_regex: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + CONST_FOO_ANN && console.log('reachable'); + } +} + +dead_code_const_annotation_complex_scope: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused_var; + /** @const */ var test = 'test'; + // @const + var CONST_FOO_ANN = false; + var unused_var_2; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + if (test === 'test') { + var beef = 'good'; + /** @const */ var meat = 'beef'; + var pork = 'bad'; + if (meat === 'pork') { + console.log('also unreachable'); + } else if (pork === 'good') { + console.log('reached, not const'); + } + } + } + expect: { + var unused_var; + var test = 'test'; + var CONST_FOO_ANN = !1; + var unused_var_2; + var moo; + function bar() {} + var beef = 'good'; + var meat = 'beef'; + var pork = 'bad'; + 'good' === pork && console.log('reached, not const'); + } +} diff --git a/test/compress/template-string.js b/test/compress/template-string.js index 20f7f0c4..739c1547 100644 --- a/test/compress/template-string.js +++ b/test/compress/template-string.js @@ -341,3 +341,39 @@ escape_dollar_curly: { } expect_exact: "console.log(`\\${ beep }`);console.log(`1\\${2-0}\\${3-0}4`);console.log(`\\${not an expression}`);" } + +template_starting_with_newline: { + options = { + dead_code: true + } + input: { + function foo(e) { + return ` +this is a template string!`; + }; + } expect_exact: "function foo(e){return`\nthis is a template string!`}" +} + +template_with_newline: { + options = { + dead_code: true + } + input: { + function foo(e) { + return `yep, +this is a template string!`; + }; + } expect_exact: "function foo(e){return`yep,\nthis is a template string!`}" +} + +template_ending_with_newline: { + options = { + dead_code: true + } + input: { + function foo(e) { + return `this is a template string! +`; + }; + } expect_exact: "function foo(e){return`this is a template string!\n`}" +} \ No newline at end of file From 937f534392256b3ec5bb39d02fd3cd08dd977037 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Tue, 8 Nov 2016 13:08:55 +0100 Subject: [PATCH 115/121] Fix flag name in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df382511..a3a1e344 100644 --- a/README.md +++ b/README.md @@ -503,7 +503,7 @@ can pass additional arguments that control the code output: - `3` -- always use the original quotes - `keep_quoted_props` (default `false`) -- when turned on, prevents stripping quotes from property names in object literals. -- `es` (default `5`) -- set output printing mode. This will only change the +- `ecma` (default `5`) -- set output printing mode. This will only change the output in direct control of the beautifier. Non-compatible features in the abstract syntax tree will still be outputted as is. From b11c5151bc23f91d3ef63cc952f5b83073029e0c Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 30 Nov 2016 21:50:31 +0100 Subject: [PATCH 116/121] Fix regression with non-ascii function identifiers Regression since 110a1ac885ba224cbc677e42695e252068edd267 --- lib/output.js | 6 +----- test/compress/functions.js | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 test/compress/functions.js diff --git a/lib/output.js b/lib/output.js index 3a21a945..c0b5d7df 100644 --- a/lib/output.js +++ b/lib/output.js @@ -951,11 +951,7 @@ function OutputStream(options) { } } if (self.name instanceof AST_Symbol) { - if (typeof self.name.name === "string" && !is_identifier_string(self.name.name)) { - output.print_string(self.name.name); - } else { - self.name.print(output); - } + self.name.print(output); } else if (nokeyword && self.name instanceof AST_Node) { output.with_square(function() { self.name.print(output); // Computed method name diff --git a/test/compress/functions.js b/test/compress/functions.js new file mode 100644 index 00000000..3a8701b7 --- /dev/null +++ b/test/compress/functions.js @@ -0,0 +1,8 @@ +non_ascii_function_identifier_name: { + input: { + function fooλ(δλ) {} + function λ(δλ) {} + (function λ(δλ) {})() + } + expect_exact: "function fooλ(δλ){}function λ(δλ){}(function λ(δλ){})();" +} From abbeb266b521be5530eca26c466161f76678969d Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 10 Jan 2017 14:19:32 -0500 Subject: [PATCH 117/121] [ES6] output parens for yield when parented by AST_Dot or AST_Sub (#1419) --- lib/output.js | 6 ++++++ test/compress/yield.js | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/output.js b/lib/output.js index c0b5d7df..79165618 100644 --- a/lib/output.js +++ b/lib/output.js @@ -686,6 +686,12 @@ function OutputStream(options) { // -(yield 4) if (p instanceof AST_Unary) return true; + // (yield x).foo + if (p instanceof AST_Dot && p.expression === this) + return true; + // (yield x)['foo'] + if (p instanceof AST_Sub && p.expression === this) + return true; }); PARENS(AST_PropAccess, function(output){ diff --git a/test/compress/yield.js b/test/compress/yield.js index 4f28b647..d1e7ece3 100644 --- a/test/compress/yield.js +++ b/test/compress/yield.js @@ -163,4 +163,30 @@ empty_generator_as_parameter_without_side_effects: { evaluate(GeneratorPrototype); } expect_exact: "var GeneratorPrototype=Object.getPrototypeOf(Object.getPrototypeOf(function*(){}()));evaluate(GeneratorPrototype);" -} \ No newline at end of file +} + +yield_dot: { + options = { + } + input: { + function* foo(){ + yield x.foo; + (yield x).foo; + yield (yield obj.foo()).bar(); + } + } + expect_exact: "function*foo(){yield x.foo;(yield x).foo;yield(yield obj.foo()).bar()}" +} + +yield_sub: { + options = { + } + input: { + function* foo(){ + yield x['foo']; + (yield x)['foo']; + yield (yield obj.foo())['bar'](); + } + } + expect_exact: 'function*foo(){yield x["foo"];(yield x)["foo"];yield(yield obj.foo())["bar"]()}' +} From 52ce9a333c78ba372a995f56c3b0352a7b08f6e8 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Mon, 31 Oct 2016 00:10:52 +0100 Subject: [PATCH 118/121] Fix compression with unused containing destructuring --- lib/compress.js | 47 ++++++++++++++++- test/compress/destructuring.js | 96 ++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 387b87bb..933dbdbf 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1384,6 +1384,8 @@ merge(Compressor.prototype, { var in_use = []; var in_use_ids = {}; // avoid expensive linear scans of in_use var initializations = new Dictionary(); + var destructuring_value = null; + var in_definition = false; // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). var scope = this; @@ -1395,9 +1397,17 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && scope === self) { node.definitions.forEach(function(def){ - if (def.is_destructuring()) return; /* Destructurings are type assertions! */ if (def.value) { - initializations.add(def.name.name, def.value); + if (def.is_destructuring()) { + var destructuring_cache = destructuring_value; + destructuring_value = def.value; + in_definition = true; + def.walk(tw); + in_definition = false; + destructuring_value = destructuring_cache; + } else { + initializations.add(def.name.name, def.value); + } if (def.value.has_side_effects(compressor)) { def.value.walk(tw); } @@ -1420,6 +1430,39 @@ merge(Compressor.prototype, { scope = save_scope; return true; } + if (node instanceof AST_Destructuring) { + if (!in_definition) { + return true; + } + for (var i = 0; i < node.names.length; i++) { + if (node.names[i] instanceof AST_Destructuring) { + node.names[i].walk(tw); + } + else if (node.names[i] instanceof AST_Expansion) { + if (node.names[i].expression instanceof AST_Symbol) { + initializations.add(node.names[i].expression.name, destructuring_value); + } else { + throw new Error(string_template("Can't handle expansion of type: {type}", { + type: Object.getPrototypeOf(node.names[i].expression).TYPE + })); + } + } + else if (node.names[i] instanceof AST_Hole) { + continue; + } + else if (node.names[i] instanceof AST_ObjectKeyVal && typeof node.names[i].key === "string") { + initializations.add(node.names[i].key, destructuring_value); + } + else if (node.names[i] instanceof AST_Symbol) { + initializations.add(node.names[i].name, destructuring_value); + } else { + throw new Error(string_template("Unknown destructuring element of type: {type}", { + type: Object.getPrototypeOf(node.names[i]).TYPE + })); + } + } + return true; + } } }); self.walk(tw); diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 5468bb81..2f8401a8 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -104,3 +104,99 @@ destructuring_expressions: { expect_exact: "({a,b});[{a}];f({x});" } +destructuring_remove_unused_1: { + options = { + unused: true + } + input: { + function a() { + var unused = "foo"; + var a = [1]; + var [b] = a; + f(b); + } + function b() { + var unused = "foo"; + var a = {b: 1}; + var {b} = a; + f(b); + } + function c() { + var unused = "foo"; + var a = [[1]]; + var [[b]] = a; + f(b); + } + function d() { + var unused = "foo"; + var a = {b: {b:1}}; + var {b:{b}} = a; + f(b); + } + function e() { + var unused = "foo"; + var a = [1, 2, 3, 4, 5]; + var [b, ...c] = a; + f(b, c); + } + } + expect: { + function a() { + var a = [1]; + var [b] = a; + f(b); + } + function b() { + var a = {b: 1}; + var {b} = a; + f(b); + } + function c() { + var a = [[1]]; + var [[b]] = a; + f(b); + } + function d() { + var a = {b: {b:1}}; + var {b:{b}} = a; + f(b); + } + function e() { + var a = [1, 2, 3, 4, 5]; + var [b, ...c] = a; + f(b, c); + } + } +} + +destructuring_remove_unused_2: { + options = { + unused: true + } + input: { + function a() { + var unused = "foo"; + var a = [,,1]; + var [b] = a; + f(b); + } + function b() { + var unused = "foo"; + var a = [{a: [1]}]; + var [{b: a}] = a; + f(b); + } + } + expect: { + function a() { + var a = [,,1]; + var [b] = a; + f(b); + } + function b() { + var a = [{a: [1]}]; + var [{b: a}] = a; + f(b); + } + } +} From 4728bc73ad6c6031029766df72a7e544af0511fc Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Thu, 26 Jan 2017 12:06:46 +0100 Subject: [PATCH 119/121] Allow parsing regexp after arrow token (#1439) --- lib/parse.js | 3 ++- test/compress/arrow.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 7288caab..31eba54c 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -335,7 +335,8 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { function token(type, value, is_comment) { S.regex_allowed = ((type == "operator" && !UNARY_POSTFIX(value)) || (type == "keyword" && KEYWORDS_BEFORE_EXPRESSION(value)) || - (type == "punc" && PUNC_BEFORE_EXPRESSION(value))); + (type == "punc" && PUNC_BEFORE_EXPRESSION(value))) || + (type == "arrow"); prev_was_dot = (type == "punc" && value == "."); var ret = { type : type, diff --git a/test/compress/arrow.js b/test/compress/arrow.js index fb9063a2..e5baaaf5 100644 --- a/test/compress/arrow.js +++ b/test/compress/arrow.js @@ -126,3 +126,12 @@ arrow_binding_pattern_strict: { } expect_exact: 'var foo=([,])=>"foo";' } + +arrow_with_regexp: { + input: { + num => /\d{11,14}/.test( num ) + } + expect: { + num => /\d{11,14}/.test( num ) + } +} From 85c1cba7609cd0c18290dee39d51f0d07845c80b Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 24 Feb 2017 01:48:13 +0100 Subject: [PATCH 120/121] Remove duplicated code (#1456) [ES6] Remove duplicated code --- lib/output.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/output.js b/lib/output.js index 79165618..056bdb9c 100644 --- a/lib/output.js +++ b/lib/output.js @@ -687,10 +687,8 @@ function OutputStream(options) { if (p instanceof AST_Unary) return true; // (yield x).foo - if (p instanceof AST_Dot && p.expression === this) - return true; // (yield x)['foo'] - if (p instanceof AST_Sub && p.expression === this) + if (p instanceof AST_PropAccess && p.expression === this) return true; }); From 07734b000a0dbe2b193e97e1b782d08afa5479a8 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 24 Feb 2017 01:49:19 +0100 Subject: [PATCH 121/121] Destructuring consistency fixes (#1417) - Use AST_Destructuring for lhf assignment patterns - Use AST_DefaultAssign for default assignments - Add more checks for lhs expressions - Add lots of testing - Cleanup ast (e.g. remove default property) - Fix #1402 based on a patch from @kzc - Refine spread allowance in array destructring pattern - Add destructuring AST tree checker --- lib/ast.js | 75 ++++---- lib/compress.js | 19 +- lib/output.js | 48 +++-- lib/parse.js | 175 ++++++++++++------ test/compress/destructuring.js | 51 ++++++ test/compress/issue-203.js | 6 +- test/compress/new.js | 16 ++ test/compress/object.js | 6 + test/compress/parameters.js | 9 + test/compress/template-string.js | 2 +- test/compress/try-catch.js | 3 + test/mocha/arguments.js | 251 ++++++++++++++++++++++++- test/mocha/arrow.js | 302 +++++++++++++++++++++++++++++++ test/mocha/destructuring.js | 138 ++++++++++++++ test/mocha/function.js | 53 ++++-- test/mocha/lhs-expressions.js | 240 +++++++++++++++++++++++- 16 files changed, 1253 insertions(+), 141 deletions(-) create mode 100644 test/mocha/destructuring.js diff --git a/lib/ast.js b/lib/ast.js index e09150c5..c3059c51 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -392,46 +392,56 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { // We don't want anything which doesn't belong in a destructuring var root = this; return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { + var insert_default = function(ex, default_value) { + if (default_value) { + return new AST_DefaultAssign({ + start: ex.start, + left: ex, + operator: "=", + right: default_value, + end: default_value.end + }); + } + return ex; + } if (ex instanceof AST_Object) { - return new AST_Destructuring({ + return insert_default(new AST_Destructuring({ start: ex.start, end: ex.end, is_array: false, - default: default_seen_above, names: ex.properties.map(to_fun_args) - }); + }), default_seen_above); } else if (ex instanceof AST_ObjectKeyVal) { if (ex.key instanceof AST_SymbolRef) { - ex.key = to_fun_args(ex.key, 0, [ex.key], ex.default); + ex.key = to_fun_args(ex.key, 0, [ex.key]); } - ex.value = to_fun_args(ex.value, 0, [ex.key], ex.default); - return ex; + ex.value = to_fun_args(ex.value, 0, [ex.key]); + return insert_default(ex, default_seen_above); } else if (ex instanceof AST_Hole) { return ex; } else if (ex instanceof AST_Destructuring) { if (ex.names.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); ex.names = ex.names.map(to_fun_args); - return ex; + return insert_default(ex, default_seen_above); } else if (ex instanceof AST_SymbolRef) { - return new AST_SymbolFunarg({ + return insert_default(new AST_SymbolFunarg({ name: ex.name, - default: default_seen_above, start: ex.start, end: ex.end - }); + }), default_seen_above); } else if (ex instanceof AST_Expansion) { - return ex; + ex.expression = to_fun_args(ex.expression); + return insert_default(ex, default_seen_above); } else if (ex instanceof AST_Array) { - return new AST_Destructuring({ + return insert_default(new AST_Destructuring({ start: ex.start, end: ex.end, is_array: true, - default: default_seen_above, names: ex.elements.map(to_fun_args) - }); + }), default_seen_above); } else if (ex instanceof AST_Assign) { - return to_fun_args(ex.left, undefined, undefined, ex.right); + return insert_default(to_fun_args(ex.left, undefined, undefined, ex.right), default_seen_above); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -447,7 +457,7 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments is_generator", $propdoc: { is_generator: "[boolean] is generatorFn or not", name: "[AST_SymbolDeclaration?] the name of this function", - argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", + argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion|AST_DefaultAssign*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, args_as_names: function () { @@ -489,12 +499,11 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", $propdoc: { - "names": "[AST_Destructuring|AST_Expansion|AST_Hole|AST_ObjectKeyVal|AST_Symbol] Array of properties or elements", - "is_array": "[Boolean] Whether the destructuring represents an object or array", - "default": "[AST_Node?] Default assign value" + "names": "[AST_Node*] Array of properties or elements", + "is_array": "[Boolean] Whether the destructuring represents an object or array" }, _walk: function(visitor) { return visitor._visit(this, function(){ @@ -672,7 +681,7 @@ var AST_Try = DEFNODE("Try", "bcatch bfinally", { var AST_Catch = DEFNODE("Catch", "argname", { $documentation: "A `catch` node; only makes sense as part of a `try` statement", $propdoc: { - argname: "[AST_SymbolCatch] symbol for the exception" + argname: "[AST_SymbolCatch|AST_Destructuring|AST_Expansion|AST_DefaultAssign] symbol for the exception" }, _walk: function(visitor) { return visitor._visit(this, function(){ @@ -957,6 +966,10 @@ var AST_Assign = DEFNODE("Assign", null, { $documentation: "An assignment expression — `a = b + 5`", }, AST_Binary); +var AST_DefaultAssign = DEFNODE("DefaultAssign", null, { + $documentation: "A default assignment expression like in `(a = 3) => a`" +}, AST_Binary); + /* -----[ LITERALS ]----- */ var AST_Array = DEFNODE("Array", "elements", { @@ -991,8 +1004,7 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { key: "[string|AST_Node] the property name converted to a string for ObjectKeyVal. For setters, getters and computed property this is an arbitrary AST_Node", - value: "[AST_Node] property value. For setters and getters this is an AST_Function.", - default: "[AST_Expression] The default for this parameter, only used when nested inside a binding pattern" + value: "[AST_Node] property value. For setters and getters this is an AST_Function." }, _walk: function(visitor) { return visitor._visit(this, function(){ @@ -1003,11 +1015,10 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote default", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { $documentation: "A key: value object property", $propdoc: { - quote: "[string] the original quote character", - default: "[AST_Expression] The default parameter value, only used when nested inside a binding pattern" + quote: "[string] the original quote character" } }, AST_ObjectProperty); @@ -1066,19 +1077,13 @@ var AST_ClassExpression = DEFNODE("ClassExpression", null, { $documentation: "A class expression." }, AST_Class); -var AST_Symbol = DEFNODE("Symbol", "scope name thedef default", { +var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", scope: "[AST_Scope/S] the current scope (not necessarily the definition scope)", - thedef: "[SymbolDef/S] the definition of this symbol", - default: "[AST_Expression] The default parameter value, only used when nested inside a binding pattern" + thedef: "[SymbolDef/S] the definition of this symbol" }, - $documentation: "Base class for all symbols", - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } + $documentation: "Base class for all symbols" }); var AST_NewTarget = DEFNODE("NewTarget", null, { diff --git a/lib/compress.js b/lib/compress.js index 377ad83a..f935a561 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -74,6 +74,7 @@ function Compressor(options, false_by_default) { pure_funcs : null, negate_iife : !false_by_default, screw_ie8 : true, + ecma : 5, drop_console : false, angular : false, warnings : true, @@ -2317,7 +2318,7 @@ merge(Compressor.prototype, { if (!fun) return self; var args = fun.argnames.map(function(arg, i){ return make_node(AST_String, self.args[i], { - value: arg.print_to_string() + value: arg.print_to_string({ecma: compressor.option("ecma")}) }); }); var code = OutputStream(); @@ -2886,6 +2887,22 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_DefaultAssign, function(self, compressor){ + if (!compressor.option("evaluate")) { + return self; + } + var evaluateRight = self.right.evaluate(compressor); + + // `[x = undefined] = foo` ---> `[x] = foo` + if (evaluateRight.length > 1 && evaluateRight[1] === undefined) { + self = self.left; + } else { + self.right = evaluateRight[0]; + } + + return self; + }); + OPT(AST_Conditional, function(self, compressor){ if (!compressor.option("conditionals")) return self; if (self.condition instanceof AST_Seq) { diff --git a/lib/output.js b/lib/output.js index 056bdb9c..1a1a8216 100644 --- a/lib/output.js +++ b/lib/output.js @@ -641,13 +641,10 @@ function OutputStream(options) { || p instanceof AST_PropAccess // (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2 || p instanceof AST_Array // [ 1, (2, 3), 4 ] ==> [ 1, 3, 4 ] || p instanceof AST_ObjectProperty // { foo: (1, 2) }.foo ==> 2 - || (p instanceof AST_SymbolConst && p.default === this) // const { xCover = (0, function() {}) } - || (p instanceof AST_SymbolLet && p.default === this) // let { xCover = (0, function() {}) } - || (p instanceof AST_SymbolVar && p.default === this) // var { xCover = (0, function() {}) } - || (p instanceof AST_SymbolCatch && p.default === this) // } catch (xCover = (0, function() {}) ) { || p instanceof AST_Conditional /* (false, true) ? (a = 10, b = 20) : (c = 30) * ==> 20 (side effect, set a := 10 and b := 20) */ || p instanceof AST_Arrow // x => (x, x) + || p instanceof AST_DefaultAssign // x => (x = (0, function(){})) ; }); @@ -761,6 +758,9 @@ function OutputStream(options) { // (a = foo)["prop"] —or— (a = foo).prop if (p instanceof AST_PropAccess && p.expression === this) return true; + // ({a, b} = {a: 1, b: 2}), a destructuring assignment + if (this instanceof AST_Assign && this.left instanceof AST_Destructuring && this.left.is_array === false) + return true; }); /* -----[ PRINTERS ]----- */ @@ -789,12 +789,6 @@ function OutputStream(options) { output.comma(); }) output.print(self.is_array ? "]" : "}"); - if (self.default) { - output.space(); - output.print('='); - output.space(); - self.default.print(output) - } }); DEFPRINT(AST_Debugger, function(self, output){ @@ -1003,7 +997,7 @@ function OutputStream(options) { parent instanceof AST_Unary || (parent instanceof AST_Call && self === parent.expression); if (needs_parens) { output.print("(") } - if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol && !self.argnames[0].default) { + if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol) { self.argnames[0].print(output); } else { output.with_parens(function(){ @@ -1497,15 +1491,27 @@ function OutputStream(options) { }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ function get_name(self) { - var def = self.value.definition(); - return def ? def.mangled_name || def.name : self.value.name; + var def = self.definition(); + return def ? def.mangled_name || def.name : self.name; } - if (output.option("shorthand") && + + var allowShortHand = output.option("shorthand"); + if (allowShortHand && self.value instanceof AST_Symbol && is_identifier_string(self.key) && - get_name(self) === self.key + get_name(self.value) === self.key ) { self.print_property_name(self.key, self.quote, output); + + } else if (allowShortHand && + self.value instanceof AST_DefaultAssign && + self.value.left instanceof AST_Symbol && + is_identifier_string(self.key) && + get_name(self.value.left) === self.key + ) { + self.print_property_name(self.key, self.quote, output); + output.print("="); + self.value.right.print(output); } else { if (!(self.key instanceof AST_Node)) { self.print_property_name(self.key, self.quote, output); @@ -1517,12 +1523,6 @@ function OutputStream(options) { output.colon(); self.value.print(output); } - if (self.default) { - output.space(); - output.print('='); - output.space(); - self.default.print(output); - } }); AST_ObjectProperty.DEFMETHOD("_print_getter_setter", function(type, self, output) { if (self.static) { @@ -1573,12 +1573,6 @@ function OutputStream(options) { }); DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); - if (self.default) { - output.space(); - output.print('='); - output.space(); - self.default.print(output) - } }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); diff --git a/lib/parse.js b/lib/parse.js index 31eba54c..21fb834e 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1106,11 +1106,9 @@ function parse($TEXT, options) { case "export": return export_(); - - default: - unexpected(); } } + unexpected(); }); function labeled_statement() { @@ -1384,7 +1382,13 @@ function parse($TEXT, options) { if (is("operator", "=") && expand === false) { used_parameters.mark_default_assignment(S.token); next(); - param.default = expression(false); + param = new AST_DefaultAssign({ + start: param.start, + left: param, + operator: "=", + right: expression(false), + end: S.token + }); } if (expand !== false) { @@ -1458,7 +1462,13 @@ function parse($TEXT, options) { if (is("operator", "=") && is_expand === false) { used_parameters.mark_default_assignment(S.token); next(); - elements[elements.length - 1].default = expression(false); + elements[elements.length - 1] = new AST_DefaultAssign({ + start: elements[elements.length - 1].start, + left: elements[elements.length - 1], + operator: "=", + right: expression(false), + end: S.token + }); } if (is_expand) { if (!is("punc", "]")) { @@ -1489,10 +1499,15 @@ function parse($TEXT, options) { } if (is("name") && (is_token(peek(), "punc") || is_token(peek(), "operator")) && [",", "}", "="].indexOf(peek().value) !== -1) { used_parameters.add_parameter(S.token); - elements.push(new symbol_type({ - start: S.token, - name: S.token.value, - end: S.token + elements.push(new AST_ObjectKeyVal({ + start: prev(), + key: S.token.value, + value: new symbol_type({ + start: S.token, + name: S.token.value, + end: S.token + }), + end: prev() })); next(); } else if (is("punc", "}")) { @@ -1503,9 +1518,14 @@ function parse($TEXT, options) { if (property === null) { unexpected(prev()); } else if (prev().type === "name" && !is("punc", ":")) { - elements.push(new AST_SymbolFunarg({ + elements.push(new AST_ObjectKeyVal({ start: prev(), - name: property_token, + key: property, + value: new symbol_type({ + start: prev(), + name: property, + end: prev() + }), end: prev() })); } else { @@ -1513,16 +1533,22 @@ function parse($TEXT, options) { elements.push(new AST_ObjectKeyVal({ start: property_token, quote: property_token.quote, - end: prev(), key: property, - value: binding_element(used_parameters, symbol_type) + value: binding_element(used_parameters, symbol_type), + end: prev() })); } } if (is("operator", "=")) { used_parameters.mark_default_assignment(S.token); next(); - elements[elements.length - 1].default = expression(false); + elements[elements.length - 1].value = new AST_DefaultAssign({ + start: elements[elements.length - 1].value.start, + left: elements[elements.length - 1].value, + operator: "=", + right: expression(false), + end: S.token + }); } } expect("}"); @@ -1558,7 +1584,7 @@ function parse($TEXT, options) { next(); a.push(new AST_Expansion({ start: prev(), - expression: binding_element(undefined, AST_SymbolFunarg), + expression: expression(false), end: S.token, })); if (!is("punc", ")")) { @@ -1943,7 +1969,7 @@ function parse($TEXT, options) { a.push(new AST_Hole({ start: S.token, end: S.token })); } else if (is("expand", "...")) { next(); - a.push(new AST_Expansion({start: S.token, expression: expression(),end: S.token})); + a.push(new AST_Expansion({start: prev(), expression: expression(),end: S.token})); } else { a.push(expression(false)); } @@ -1970,63 +1996,51 @@ function parse($TEXT, options) { start = S.token; var type = start.type; var name = as_property_name(); + var value; + + // Check property and fetch value if (!is("punc", ":")) { var concise = concise_method_or_getset(name, start); if (concise) { a.push(concise); continue; } + if (!(start.type !== name)) { + unexpected(S.token); + } + + value = new AST_SymbolRef({ + start: prev(), + name: name, + end: prev() + }); } else if (name === null) { unexpected(prev()); + } else { + next(); // `:` - see first condition + value = expression(false); } - if (type == "punc" && start.value == "[") { - expect(":"); - a.push(new AST_ObjectKeyVal({ - start: start, - key: name, - value: expression(false), - end: prev() - })); - continue; - } - + // Check for default value and alter value accordingly if necessary if (is("operator", "=")) { next(); - a.push(new AST_Assign({ + value = new AST_Assign({ start: start, - // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. - left: new AST_SymbolRef({ - start: start, - end: start, - name: name - }), + left: value, operator: "=", right: expression(false), end: prev() - })); - } else if (!is("punc", ":")) { - // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectKeyVal({ - start: start, - key: name, - value: new AST_SymbolRef({ - start: start, - end: prev(), - name: name - }), - end: start, - })); - } else { - expect(":"); - a.push(new AST_ObjectKeyVal({ - start : start, - quote : start.quote, - key : name, - value : expression(false), - end : prev() - })); + }); } + + // Create property + a.push(new AST_ObjectKeyVal({ + start: start, + quote: start.quote, + key: name, + value: value, + end: prev() + })); } next(); return new AST_Object({ properties: a }) @@ -2493,6 +2507,55 @@ function parse($TEXT, options) { if (is("operator") && ASSIGNMENT(val)) { if (is_assignable(left)) { + + var walk = function(node) { + var newNode; + if (node instanceof AST_Object) { + newNode = new AST_Destructuring({ + start: node.start, + names: node.properties.map(walk), + is_array: false, + end: node.end + }); + node = newNode; + } else if (node instanceof AST_Array) { + var names = []; + + for (var i = 0; i < node.elements.length; i++) { + // Only allow expansion as last element + if (node.elements[i] instanceof AST_Expansion) { + if (i + 1 !== node.elements.length) { + token_error(node.elements[i].start, "SyntaxError: Spread must the be last element in destructuring array"); + } + node.elements[i].expression = walk(node.elements[i].expression); + } + + names.push(walk(node.elements[i])); + } + + newNode = new AST_Destructuring({ + start: node.start, + names: names, + is_array: true, + end: node.end + }); + node = newNode; + } else if (node instanceof AST_ObjectProperty) { + node.value = walk(node.value); + } else if (node instanceof AST_Assign) { + node = new AST_DefaultAssign({ + start: node.start, + left: node.left, + operator: "=", + right: node.right, + end: node.end + }); + } + + return node; + } + left = walk(left); + next(); return new AST_Assign({ start : start, diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 2f8401a8..81c02eeb 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -50,6 +50,9 @@ destructuring_objects: { } destructuring_objects_trailing_elision: { + beautify = { + ecma: 6 + } input: { var {cc,} = foo; } @@ -57,6 +60,9 @@ destructuring_objects_trailing_elision: { } nested_destructuring_objects: { + beautify = { + ecma: 6 + } input: { const [{a},b] = c; let [{a},b] = c; @@ -66,6 +72,9 @@ nested_destructuring_objects: { } destructuring_constdef_in_loops: { + beautify = { + ecma: 6 + } input: { for (const [x,y] in pairs); for (const [a] = 0;;); @@ -75,6 +84,9 @@ destructuring_constdef_in_loops: { } destructuring_letdef_in_loops: { + beautify = { + ecma: 6 + } input: { for (let [x,y] in pairs); for (let [a] = 0;;); @@ -84,6 +96,9 @@ destructuring_letdef_in_loops: { } destructuring_vardef_in_loops: { + beautify = { + ecma: 6 + } input: { for (var [x,y] in pairs); for (var [a] = 0;;); @@ -200,3 +215,39 @@ destructuring_remove_unused_2: { } } } + +object_destructuring_may_need_parentheses: { + beautify = { + ecma: 6 + } + input: { + ({a, b} = {a: 1, b: 2}); + } + expect_exact: "({a,b}={a:1,b:2});" +} + +destructuring_with_undefined_as_default_assignment: { + options = { + evaluate: true + } + input: { + [foo = undefined] = bar; + [foo = void 0] = bar; + } + expect: { + [foo] = bar; + [foo] = bar; + } +} + +destructuring_dont_evaluate_with_undefined_as_default_assignment: { + options = { + evaluate: false + } + input: { + [foo = undefined] = bar; + } + expect: { + [foo = void 0] = bar; + } +} diff --git a/test/compress/issue-203.js b/test/compress/issue-203.js index d894c586..0fa3c2eb 100644 --- a/test/compress/issue-203.js +++ b/test/compress/issue-203.js @@ -13,7 +13,11 @@ compress_new_function: { compress_new_function_with_destruct: { options = { - unsafe: true + unsafe: true, + ecma: 6 + } + beautify = { + ecma: 6 } input: { new Function("aa, [bb]", 'return aa;'); diff --git a/test/compress/new.js b/test/compress/new.js index 83da88e6..2364a16e 100644 --- a/test/compress/new.js +++ b/test/compress/new.js @@ -82,3 +82,19 @@ new_with_unary_prefix: { } expect_exact: 'var bar=(+new Date).toString(32);'; } + +new_with_assignement_expression: { + options = { + evaluate: true + } + input: { + var a; + new x(a = 5 * 2, b = [1, 2, 3], c = {a: "a", b: "b", cd: "c" + "d"}); + new y([a, b] = [3, 4]); + } + expect: { + var a; + new x(a = 10, b = [1, 2, 3], c = {a: "a", b: "b", cd: "cd"}); + new y([a, b] = [3, 4]); + } +} diff --git a/test/compress/object.js b/test/compress/object.js index 724e0877..a9f6b3cb 100644 --- a/test/compress/object.js +++ b/test/compress/object.js @@ -170,6 +170,9 @@ shorthand_properties: { } concise_methods: { + beautify = { + ecma: 6 + } input: { x = { foo(a, b) { @@ -299,6 +302,9 @@ concise_methods_and_mangle_props: { } concise_generators: { + beautify = { + ecma: 6 + } input: { x = { *foo(a, b) { diff --git a/test/compress/parameters.js b/test/compress/parameters.js index fd0149b0..b7844edf 100644 --- a/test/compress/parameters.js +++ b/test/compress/parameters.js @@ -119,6 +119,9 @@ destructuring_arguments_2: { } destructuring_arguments_3: { + beautify = { + ecma: 6 + } input: { function fn3({x: {y: {z: {} = 42}}}) {} const { cover = (function () {}), xCover = (0, function() {}) } = {}; @@ -129,6 +132,9 @@ destructuring_arguments_3: { } default_arguments: { + beautify = { + ecma: 6 + } input: { function x(a = 6) { } function x(a = (6 + 5)) { } @@ -138,6 +144,9 @@ default_arguments: { } default_values_in_destructurings: { + beautify = { + ecma: 6 + } input: { function x({a=(4), b}) {} function x([b, c=(12)]) {} diff --git a/test/compress/template-string.js b/test/compress/template-string.js index 739c1547..4b3acd49 100644 --- a/test/compress/template-string.js +++ b/test/compress/template-string.js @@ -376,4 +376,4 @@ template_ending_with_newline: { `; }; } expect_exact: "function foo(e){return`this is a template string!\n`}" -} \ No newline at end of file +} diff --git a/test/compress/try-catch.js b/test/compress/try-catch.js index 52926614..9c21aaf1 100644 --- a/test/compress/try-catch.js +++ b/test/compress/try-catch.js @@ -1,4 +1,7 @@ catch_destructuring_with_sequence: { + beautify = { + ecma: 6 + } input: { try { throw {}; diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js index 73993a73..e43be0e4 100644 --- a/test/mocha/arguments.js +++ b/test/mocha/arguments.js @@ -27,4 +27,253 @@ describe("arguments", function() { assert.strictEqual(ast.body[0].body[0].uses_arguments, true); assert.strictEqual(ast.body[0].body[0].body[0].uses_arguments, false); }); -}); \ No newline at end of file + + it("Should parse a function containing default assignment correctly", function() { + var ast = UglifyJS.parse("function foo(a = 123) {}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // First argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].operator, "="); + assert(ast.body[0].argnames[0].right instanceof UglifyJS.AST_Number); + + ast = UglifyJS.parse("function foo(a = a) {}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // First argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].operator, "="); + assert(ast.body[0].argnames[0].right instanceof UglifyJS.AST_SymbolRef); + }); + + it("Should parse a function containing default assignments in destructuring correctly", function() { + var ast = UglifyJS.parse("function foo([a = 123]) {}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // First argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, true); + assert.strictEqual(ast.body[0].argnames[0].names.length, 1); + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].names[0].left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].names[0].operator, "="); + assert(ast.body[0].argnames[0].names[0].right instanceof UglifyJS.AST_Number); + + + ast = UglifyJS.parse("function foo({a = 123}) {}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // First argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, false); + assert.strictEqual(ast.body[0].argnames[0].names.length, 1); + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[0].key, "a"); + + // Property a of first argument + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].names[0].value.left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].names[0].value.operator, "="); + assert(ast.body[0].argnames[0].names[0].value.right instanceof UglifyJS.AST_Number); + + + ast = UglifyJS.parse("function foo({a: a = 123}) {}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // First argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, false); + assert.strictEqual(ast.body[0].argnames[0].names.length, 1); + + // Content destructuring of first argument + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[0].key, "a"); + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_DefaultAssign); + + // Property a of first argument + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].names[0].value.left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].names[0].value.operator, "="); + assert(ast.body[0].argnames[0].names[0].value.right instanceof UglifyJS.AST_Number); + }); + + it("Should parse a function containing default assignments in complex destructuring correctly", function() { + var ast = UglifyJS.parse("function foo([a, [b = 123]]){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, true); + assert.strictEqual(ast.body[0].argnames[0].names.length, 2); + + // Check whole destructuring structure of first argument + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[1] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].names[1].is_array, true); + + // Check content of second destructuring element (which is the nested destructuring pattern) + assert(ast.body[0].argnames[0].names[1].names[0] instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].names[1].names[0].left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].names[1].names[0].operator, "="); + assert(ast.body[0].argnames[0].names[1].names[0].right instanceof UglifyJS.AST_Number); + + + ast = UglifyJS.parse("function foo([a, {b: c = 123}]){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, true); + assert.strictEqual(ast.body[0].argnames[0].names.length, 2); + + // Check whole destructuring structure of first argument + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[1] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].names[1].is_array, false); + + // Check content of second destructuring element (which is the nested destructuring pattern) + assert(ast.body[0].argnames[0].names[1].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[1].names[0].key, "b"); + assert(ast.body[0].argnames[0].names[1].names[0].value instanceof UglifyJS.AST_DefaultAssign); + + // Property b of second argument + assert(ast.body[0].argnames[0].names[1].names[0].value instanceof UglifyJS.AST_DefaultAssign); + assert(ast.body[0].argnames[0].names[1].names[0].value.left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].argnames[0].names[1].names[0].value.operator, "="); + assert(ast.body[0].argnames[0].names[1].names[0].value.right instanceof UglifyJS.AST_Number); + + + ast = UglifyJS.parse("function foo({a, b: {b = 123}}){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, false); + assert.strictEqual(ast.body[0].argnames[0].names.length, 2); + + // Check whole destructuring structure of first argument + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[0].key, "a"); + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[1] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[1].key, "b"); + assert(ast.body[0].argnames[0].names[1].value instanceof UglifyJS.AST_Destructuring); + + // Check content of nested destructuring in first parameter + var content = ast.body[0].argnames[0].names[1].value + assert.strictEqual(content.is_array, false); + assert.strictEqual(content.names.length, 1); + assert(content.names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(content.names[0].key, "b"); + assert(content.names[0].value instanceof UglifyJS.AST_DefaultAssign); + assert(content.names[0].value.left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(content.names[0].value.operator, "="); + assert(content.names[0].value.right instanceof UglifyJS.AST_Number); + + + ast = UglifyJS.parse("function foo({a: {b = 123}}){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first argument + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, false); + assert.strictEqual(ast.body[0].argnames[0].names.length, 1); + + // Check whole destructuring structure of first argument + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[0].key, "a"); + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_Destructuring); + + // Check content of nested destructuring + content = ast.body[0].argnames[0].names[0].value + assert.strictEqual(content.is_array, false); + assert.strictEqual(content.names.length, 1); + assert(content.names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(content.names[0].key, "b"); + assert(content.names[0].value instanceof UglifyJS.AST_DefaultAssign); + assert(content.names[0].value.left instanceof UglifyJS.AST_SymbolFunarg); + assert.strictEqual(content.names[0].value.operator, "="); + assert(content.names[0].value.right instanceof UglifyJS.AST_Number); + }); + + it("Should parse spread correctly", function() { + var ast = UglifyJS.parse("function foo(a, b, ...c){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 3); + + // Check parameters + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[1] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[2] instanceof UglifyJS.AST_Expansion); + assert(ast.body[0].argnames[2].expression instanceof UglifyJS.AST_SymbolFunarg); + + + ast = UglifyJS.parse("function foo([a, b, ...c]){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first parameter + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, true); + + // Check content first parameter + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[1] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[2] instanceof UglifyJS.AST_Expansion); + assert(ast.body[0].argnames[0].names[2].expression instanceof UglifyJS.AST_SymbolFunarg); + + + ast = UglifyJS.parse("function foo([a, b, [c, ...d]]){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first parameter + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, true); + + // Check content outer destructuring array + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[1] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[2] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].names[2].is_array, true); + + // Check content nested destructuring array + assert.strictEqual(ast.body[0].argnames[0].names[2].names.length, 2); + assert(ast.body[0].argnames[0].names[2].names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[2].names[1] instanceof UglifyJS.AST_Expansion); + assert(ast.body[0].argnames[0].names[2].names[1].expression instanceof UglifyJS.AST_SymbolFunarg); + + + ast = UglifyJS.parse("function foo({a: [b, ...c]}){}"); + assert(ast.body[0] instanceof UglifyJS.AST_Defun); + assert.strictEqual(ast.body[0].argnames.length, 1); + + // Check first parameter + assert(ast.body[0].argnames[0] instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].is_array, false); + + // Check outer destructuring object + assert.strictEqual(ast.body[0].argnames[0].names.length, 1); + assert(ast.body[0].argnames[0].names[0] instanceof UglifyJS.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].argnames[0].names[0].key, "a"); + assert(ast.body[0].argnames[0].names[0].value instanceof UglifyJS.AST_Destructuring); + assert.strictEqual(ast.body[0].argnames[0].names[0].value.is_array, true); + + // Check content nested destructuring array + assert.strictEqual(ast.body[0].argnames[0].names[0].value.names.length, 2); + assert(ast.body[0].argnames[0].names[0].value.names[0] instanceof UglifyJS.AST_SymbolFunarg); + assert(ast.body[0].argnames[0].names[0].value.names[1] instanceof UglifyJS.AST_Expansion); + assert(ast.body[0].argnames[0].names[0].value.names[1].expression instanceof UglifyJS.AST_SymbolFunarg); + }); +}); diff --git a/test/mocha/arrow.js b/test/mocha/arrow.js index 15a8033a..1ee6d403 100644 --- a/test/mocha/arrow.js +++ b/test/mocha/arrow.js @@ -80,4 +80,306 @@ describe("Arrow functions", function() { assert.throws(test(tests[i]), error); } }); + + it("Should parse a function containing default assignment correctly", function() { + var ast = uglify.parse("var a = (a = 123) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // First argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].right instanceof uglify.AST_Number); + + ast = uglify.parse("var a = (a = a) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // First argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].right instanceof uglify.AST_SymbolRef); + }); + + it("Should parse a function containing default assignments in destructuring correctly", function() { + var ast = uglify.parse("var a = ([a = 123]) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // First argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, true); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 1); + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].right instanceof uglify.AST_Number); + + + ast = uglify.parse("var a = ({a = 123}) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // First argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, false); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 1); + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_ObjectKeyVal); + + // First object element in first argument + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].key, "a"); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].value.operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.right instanceof uglify.AST_Number); + + + ast = uglify.parse("var a = ({a: a = 123}) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // First argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, false); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 1); + + // Content destructuring of first argument + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_ObjectProperty); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].key, "a"); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].value.operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.right instanceof uglify.AST_Number); + }); + + it("Should parse a function containing default assignments in complex destructuring correctly", function() { + var ast = uglify.parse("var a = ([a, [b = 123]]) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, true); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 2); + + // Check whole destructuring structure of first argument + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[1] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].is_array, true); + + // Check content of second destructuring element (which is the nested destructuring pattern) + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].right instanceof uglify.AST_Number); + + + ast = uglify.parse("var a = ([a, {b: c = 123}]) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, true); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 2); + + // Check whole destructuring structure of first argument + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[1] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].is_array, false); + + // Check content of second destructuring element (which is the nested destructuring pattern) + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0] instanceof uglify.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].key, "b"); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].value instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].value.left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].value.operator, "="); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].names[0].value.right instanceof uglify.AST_Number); + + + ast = uglify.parse("var a = ({a, b: {b = 123}}) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, false); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 2); + + // First argument, property 1 + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].key, "a"); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value instanceof uglify.AST_SymbolFunarg); + + // First argument, property 2 + assert(ast.body[0].definitions[0].value.argnames[0].names[1] instanceof uglify.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[1].key, "b"); + assert(ast.body[0].definitions[0].value.argnames[0].names[1].value instanceof uglify.AST_Destructuring); + + // Check content of nested destructuring + var content = ast.body[0].definitions[0].value.argnames[0].names[1].value + assert.strictEqual(content.is_array, false); + assert.strictEqual(content.names.length, 1); + assert(content.names[0] instanceof uglify.AST_ObjectKeyVal); + + // Content of first property in nested destructuring + assert.strictEqual(content.names[0].key, "b"); + assert(content.names[0].value instanceof uglify.AST_DefaultAssign); + assert(content.names[0].value.left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(content.names[0].value.operator, "="); + assert(content.names[0].value.right instanceof uglify.AST_Number); + + + ast = uglify.parse("var a = ({a: {b = 123}}) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first argument + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, false); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 1); + + // Check whole destructuring structure of first argument + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].key, "a"); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value instanceof uglify.AST_Destructuring); + + // Check content of nested destructuring + content = ast.body[0].definitions[0].value.argnames[0].names[0].value + assert.strictEqual(content.is_array, false); + assert.strictEqual(content.names.length, 1); + assert(content.names[0] instanceof uglify.AST_ObjectKeyVal); + + // Check first property of nested destructuring + assert.strictEqual(content.names[0].key, "b"); + assert(content.names[0].value instanceof uglify.AST_DefaultAssign); + assert(content.names[0].value.left instanceof uglify.AST_SymbolFunarg); + assert.strictEqual(content.names[0].value.operator, "="); + assert(content.names[0].value.right instanceof uglify.AST_Number); + }); + + it("Should parse spread correctly", function() { + var ast = uglify.parse("var a = (a, b, ...c) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 3); + + // Check parameters + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[1] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[2] instanceof uglify.AST_Expansion); + assert(ast.body[0].definitions[0].value.argnames[2].expression instanceof uglify.AST_SymbolFunarg); + + + ast = uglify.parse("var a = ([a, b, ...c]) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first parameter + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, true); + + // Check content first parameter + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[1] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[2] instanceof uglify.AST_Expansion); + assert(ast.body[0].definitions[0].value.argnames[0].names[2].expression instanceof uglify.AST_SymbolFunarg); + + + ast = uglify.parse("var a = ([a, b, [c, ...d]]) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first parameter + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, true); + + // Check content outer destructuring array + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[1] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[2] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[2].is_array, true); + + // Check content nested destructuring array + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[2].names.length, 2); + assert(ast.body[0].definitions[0].value.argnames[0].names[2].names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[2].names[1] instanceof uglify.AST_Expansion); + assert(ast.body[0].definitions[0].value.argnames[0].names[2].names[1].expression instanceof uglify.AST_SymbolFunarg); + + + ast = uglify.parse("var a = ({a: [b, ...c]}) => {}"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert.strictEqual(ast.body[0].definitions.length, 1); + assert(ast.body[0].definitions[0] instanceof uglify.AST_VarDef); + assert(ast.body[0].definitions[0].name instanceof uglify.AST_SymbolVar); + assert(ast.body[0].definitions[0].value instanceof uglify.AST_Arrow); + assert.strictEqual(ast.body[0].definitions[0].value.argnames.length, 1); + + // Check first parameter + assert(ast.body[0].definitions[0].value.argnames[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].is_array, false); + + // Check outer destructuring object + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names.length, 1); + assert(ast.body[0].definitions[0].value.argnames[0].names[0] instanceof uglify.AST_ObjectKeyVal); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].key, "a"); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].value.is_array, true); + + // Check content nested destructuring array + assert.strictEqual(ast.body[0].definitions[0].value.argnames[0].names[0].value.names.length, 2); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.names[0] instanceof uglify.AST_SymbolFunarg); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.names[1] instanceof uglify.AST_Expansion); + assert(ast.body[0].definitions[0].value.argnames[0].names[0].value.names[1].expression instanceof uglify.AST_SymbolFunarg); + }); + }); diff --git a/test/mocha/destructuring.js b/test/mocha/destructuring.js new file mode 100644 index 00000000..73548694 --- /dev/null +++ b/test/mocha/destructuring.js @@ -0,0 +1,138 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Destructuring", function() { + it("Should generate similar trees for destructuring in left hand side expressions, definitions, functions and arrow functions", function() { + var patterns = [ + "[]", + "{}", + + "[a, b, c]", + "{a: b, c: d}", + "{a}", + "{a, b}", + + "{a: {}}", + "{a: []}", + "[{}]", + "[[]]", + "{a: {b}}", + + // Can't do `a = 123` with lhs expression, so only test in destructuring + "[foo = bar]", + "{a = 123}", + "[{foo: abc = 123}]", + "{foo: [abc = 123]}", + + "[...foo]", + "[...{}]", + "[...[]]" + + // Can't do `...` because that is an invalid lhs expression, spread in array destructuring should be fine though + ]; + + var types = [ + { + name: "lhs", + symbolType: uglify.AST_SymbolRef, + tree: function (ast) { + return ast.body[0].body.left; + }, + generate: function (code) { + return "(" + code + " = a)"; + } + }, + { + name: "var", + symbolType: uglify.AST_SymbolVar, + tree: function (ast) { + return ast.body[0].definitions[0].name; + }, + generate: function (code) { + return "var " + code + " = a"; + } + }, + { + name: "function", + symbolType: uglify.AST_SymbolFunarg, + tree: function (ast) { + return ast.body[0].argnames[0]; + }, + generate: function (code) { + return "function a(" + code + ") {}"; + } + }, + { + name: "arrow", + symbolType: uglify.AST_SymbolFunarg, + tree: function (ast) { + return ast.body[0].definitions[0].value.argnames[0]; + }, + generate: function (code) { + return "var a = (" + code + ") => {}"; + } + } + ]; + + var walker = function(type, ref, code, result) { + var w = new uglify.TreeWalker(function(node) { + if (w.parent() instanceof uglify.AST_DefaultAssign && + w.parent().right === node + ) { + return true; // Don't check the content of the default assignment + + } else if (node instanceof uglify.AST_Symbol) { + assert(node instanceof type.symbolType, + node.TYPE + " while " + type.symbolType.TYPE + " expected at pos " + + node.start.pos + " in `" + code + "` (" + ref + ")" + ); + + result.push([ + new uglify.AST_Symbol({ + start: node.start, + name: node.name, + end: node.end + }), + w.parent() + ]); + + return; + } + + result.push([node, w.parent()]); + }); + + return w; + }; + + var getNodeType = function(node) { + return node[0].TYPE + (node[1] ? " " + node[1].TYPE : ""); + } + + for (var i = 0; i < patterns.length; i++) { + var results = []; + + for (var j = 0; j < types.length; j++) { + var code = types[j].generate(patterns[i]) + var ast = types[j].tree( + uglify.parse(code) + ); + results.push([]); + ast.walk(walker( + types[j], + "`" + patterns[i] + "` on " + types[j].name, + code, + results[j] + )); + + if (j > 0) { + assert.deepEqual( + results[0].map(getNodeType), + results[j].map(getNodeType), + "AST disagree on " + patterns[i] + " with " + types[j].name + ); + } + } + } + }); +}); diff --git a/test/mocha/function.js b/test/mocha/function.js index 2ae8a202..01206f49 100644 --- a/test/mocha/function.js +++ b/test/mocha/function.js @@ -47,64 +47,83 @@ describe("Function", function() { assert.equal(destruct3.is_array, false); assert.equal(destruct4.is_array, true); - var aAndB = [ - ['SymbolFunarg', 'a'], - ['SymbolFunarg', 'b'] - ]; - + // destruct 1 assert.deepEqual( [ destruct1.names[0].TYPE, - destruct1.names[0].name + destruct1.names[0].key, + destruct1.names[0].value.name ], - aAndB[0]); + ['ObjectKeyVal', 'a', 'a'] + ); assert.deepEqual( [ destruct1.names[1].TYPE, - destruct1.names[1].name + destruct1.names[1].key, + destruct1.names[1].value.name ], - aAndB[1]); + ['ObjectKeyVal', 'b', 'b'] + ); + + // destruct 2 assert.deepEqual( [ destruct2.names[0].TYPE, destruct2.names[0].name ], - aAndB[0]); + ['SymbolFunarg', 'a'] + ); assert.deepEqual( [ destruct2.names[1].names[0].TYPE, destruct2.names[1].names[0].name ], - aAndB[1]); + ['SymbolFunarg', 'b'] + ); + + // destruct 3 assert.strictEqual(typeof destruct3.names[0].key, "string"); assert.strictEqual(destruct3.names[0].key, "a"); + assert.strictEqual(destruct3.names[0].value.TYPE, "SymbolFunarg"); + assert.strictEqual(destruct3.names[0].value.name, "a"); assert.strictEqual(typeof destruct3.names[1].key, "string"); assert.strictEqual(destruct3.names[1].key, "b"); + assert.strictEqual(destruct3.names[1].value.TYPE, "SymbolFunarg"); + assert.strictEqual(destruct3.names[1].value.name, "b"); + + // destruct 4 assert.deepEqual( [ destruct4.names[0].TYPE, destruct4.names[0].name ], - aAndB[0]); + ['SymbolFunarg', 'a'] + ); + assert.strictEqual(destruct4.names[1].TYPE, "Destructuring"); assert.deepEqual( [ destruct4.names[1].names[0].TYPE, destruct4.names[1].names[0].name ], - aAndB[1]); + ['SymbolFunarg', 'b'] + ); assert.deepEqual( get_args(destr_fun1.args_as_names()), - aAndB); + [['SymbolFunarg', 'a'], ['SymbolFunarg', 'b']] + ); assert.deepEqual( get_args(destr_fun2.args_as_names()), - aAndB); + [['SymbolFunarg', 'a'], ['SymbolFunarg', 'b']] + ); assert.deepEqual( get_args(destr_fun3.args_as_names()), - aAndB); + [['SymbolFunarg', 'a'], ['SymbolFunarg', 'b']] + ); assert.deepEqual( get_args(destr_fun4.args_as_names()), - aAndB); + [['SymbolFunarg', 'a'], ['SymbolFunarg', 'b']] + ); // Making sure we don't accidentally accept things which // Aren't argument destructurings diff --git a/test/mocha/lhs-expressions.js b/test/mocha/lhs-expressions.js index 1edd44a2..32b88883 100644 --- a/test/mocha/lhs-expressions.js +++ b/test/mocha/lhs-expressions.js @@ -7,17 +7,26 @@ describe("Left-hand side expressions", function () { assert.equal(decls.body[0].TYPE, 'Var'); assert.equal(decls.body[0].definitions.length, 2); + + // Item 1 assert.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); assert.equal(decls.body[0].definitions[0].value.TYPE, 'SymbolRef'); + // Item 2 + assert.equal(decls.body[0].definitions[1].name.TYPE, 'Destructuring'); + assert.equal(decls.body[0].definitions[1].value.TYPE, 'SymbolRef'); + var nested_def = uglify.parse('var [{x}] = foo').body[0].definitions[0]; - assert.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar'); - assert.equal(nested_def.name.names[0].names[0].name, 'x'); + assert.equal(nested_def.name.names[0].names[0].TYPE, 'ObjectKeyVal'); + assert.equal(nested_def.name.names[0].names[0].value.TYPE, 'SymbolVar'); + assert.equal(nested_def.name.names[0].names[0].key, 'x'); + assert.equal(nested_def.name.names[0].names[0].value.name, 'x'); var holey_def = uglify.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; assert.equal(holey_def.name.names[0].TYPE, 'Hole'); + assert.equal(holey_def.name.names[1].TYPE, 'Hole'); assert.equal(holey_def.name.names[2].TYPE, 'SymbolConst'); var expanding_def = uglify.parse('var [first, ...rest] = [1,2,3]').body[0].definitions[0]; @@ -26,4 +35,231 @@ describe("Left-hand side expressions", function () { assert.equal(expanding_def.name.names[1].TYPE, 'Expansion'); assert.equal(expanding_def.name.names[1].expression.TYPE, 'SymbolVar'); }); + + it("Parser should use AST_Array for array literals", function() { + var ast = uglify.parse('["foo", "bar"]'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + assert(ast.body[0].body instanceof uglify.AST_Array); + + ast = uglify.parse('a = ["foo"]'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_SymbolRef); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Array); + }); + + it("Parser should use AST_Object for object literals", function() { + var ast = uglify.parse('({foo: "bar"})'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + assert(ast.body[0].body instanceof uglify.AST_Object); + + // This example should be fine though + ast = uglify.parse('a = {foo: "bar"}'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_SymbolRef); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Object); + }); + + it("Parser should use AST_Destructuring for array assignment patterns", function() { + var ast = uglify.parse('[foo, bar] = [1, 2]'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, true); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Array); + }); + + it("Parser should use AST_Destructuring for object assignment patterns", function() { + var ast = uglify.parse('({a: b, b: c} = {b: "c", c: "d"})'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, false); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Object); + }); + + it("Parser should be able to handle nested destructuring", function() { + var ast = uglify.parse('[{a,b},[d, e, f, {g, h}]] = [{a: 1, b: 2}, [3, 4, 5, {g: 6, h: 7}]]'); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, true); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Array); + + assert(ast.body[0].body.left.names[0] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.names[0].is_array, false); + + assert(ast.body[0].body.left.names[1] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.names[1].is_array, true); + + assert(ast.body[0].body.left.names[1].names[3] instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.names[1].names[3].is_array, false); + }); + + it("Should handle spread operator in destructuring", function() { + var ast = uglify.parse("[a, b, ...c] = [1, 2, 3, 4, 5]"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, true); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_Array); + + assert(ast.body[0].body.left.names[0] instanceof uglify.AST_SymbolRef); + assert(ast.body[0].body.left.names[1] instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[2] instanceof uglify.AST_Expansion); + }); + + it("Should handle default assignments in destructuring", function() { + var ast = uglify.parse("({x: v, z = z + 5} = obj);"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, false); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[0].value instanceof uglify.AST_SymbolRef); + assert.strictEqual(ast.body[0].body.left.names[0].start.value, "x"); + + assert(ast.body[0].body.left.names[1].value instanceof uglify.AST_DefaultAssign); + assert.strictEqual(ast.body[0].body.left.names[1].start.value, "z"); + assert(ast.body[0].body.left.names[1].value.left instanceof uglify.AST_SymbolRef); + assert.strictEqual(ast.body[0].body.left.names[1].value.operator, "="); + assert(ast.body[0].body.left.names[1].value.right instanceof uglify.AST_Binary); + + + ast = uglify.parse("({x = 123} = obj);"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, false); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[0].value instanceof uglify.AST_DefaultAssign); + assert.strictEqual(ast.body[0].body.left.names[0].value.operator, "="); + assert(ast.body[0].body.left.names[0].value.left instanceof uglify.AST_SymbolRef); + + + ast = uglify.parse("[x, y = 5] = foo"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, true); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[0] instanceof uglify.AST_SymbolRef); + assert.strictEqual(ast.body[0].body.left.names[0].start.value, "x"); + + assert(ast.body[0].body.left.names[1] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].body.left.names[1].left instanceof uglify.AST_SymbolRef); + assert.strictEqual(ast.body[0].body.left.names[1].start.value, "y"); + }); + + it("Should handle default assignments containing assignments in a destructuring", function() { + var ast = uglify.parse("[x, y = z = 2] = a;"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, true); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[0] instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[1] instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].body.left.names[1].left instanceof uglify.AST_SymbolRef); + assert.equal(ast.body[0].body.left.names[1].operator, "="); + assert(ast.body[0].body.left.names[1].right instanceof uglify.AST_Assign); + + assert(ast.body[0].body.left.names[1].right.left instanceof uglify.AST_SymbolRef); + assert.equal(ast.body[0].body.left.names[1].right.operator, "="); + assert(ast.body[0].body.left.names[1].right.right instanceof uglify.AST_Number); + + ast = uglify.parse("({a: a = 123} = obj)"); + assert(ast.body[0] instanceof uglify.AST_SimpleStatement); + + assert(ast.body[0].body instanceof uglify.AST_Assign); + assert(ast.body[0].body.left instanceof uglify.AST_Destructuring); + assert.strictEqual(ast.body[0].body.left.is_array, false); + assert.equal(ast.body[0].body.operator, "="); + assert(ast.body[0].body.right instanceof uglify.AST_SymbolRef); + + assert(ast.body[0].body.left.names[0] instanceof uglify.AST_ObjectProperty); + assert.strictEqual(ast.body[0].body.left.names[0].key, "a"); + assert(ast.body[0].body.left.names[0].value instanceof uglify.AST_DefaultAssign); + assert(ast.body[0].body.left.names[0].value.left instanceof uglify.AST_SymbolRef); + assert.strictEqual(ast.body[0].body.left.names[0].value.operator, "="); + assert(ast.body[0].body.left.names[0].value.right instanceof uglify.AST_Number); + }); + + it("Should allow multiple spread in array literals", function() { + var ast = uglify.parse("var a = [1, 2, 3], b = [4, 5, 6], joined; joined = [...a, ...b]"); + assert(ast.body[0] instanceof uglify.AST_Var); + assert(ast.body[1] instanceof uglify.AST_SimpleStatement); + + // Check statement containing array with spreads + assert(ast.body[1].body instanceof uglify.AST_Assign); + assert(ast.body[1].body.left instanceof uglify.AST_SymbolRef); + assert.equal(ast.body[1].body.operator, "="); + assert(ast.body[1].body.right instanceof uglify.AST_Array); + + // Check array content + assert.strictEqual(ast.body[1].body.right.elements.length, 2); + assert(ast.body[1].body.right.elements[0] instanceof uglify.AST_Expansion); + assert(ast.body[1].body.right.elements[0].expression instanceof uglify.AST_SymbolRef); + assert(ast.body[1].body.right.elements[0].expression.name, "a"); + assert(ast.body[1].body.right.elements[1] instanceof uglify.AST_Expansion); + assert(ast.body[1].body.right.elements[1].expression instanceof uglify.AST_SymbolRef); + assert(ast.body[1].body.right.elements[1].expression.name, "b"); + }); + + it("Should not allow spread on invalid locations", function() { + var expect = function(input, expected) { + var execute = function(input) { + return function() { + uglify.parse(input); + } + } + var check = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === expected; + } + + assert.throws(execute(input), check); + } + + // Spreads are not allowed in destructuring array if it's not the last element + expect("[...a, ...b] = [1, 2, 3, 4]", "SyntaxError: Spread must the be last element in destructuring array"); + + // Multiple spreads are not allowed in destructuring array + expect("[...a, ...b] = [1, 2, 3, 4]", "SyntaxError: Spread must the be last element in destructuring array"); + + // Spread in obvious object pattern + expect("({...a} = foo)", "SyntaxError: Unexpected token: expand (...)"); + + // Spread in block should not be allowed + expect("{...a} = foo", "SyntaxError: Unexpected token: expand (...)"); + + // Not in standard yet + expect("let foo = {bar: 42}, bar; bar = {...foo}", "SyntaxError: Unexpected token: expand (...)"); + }); });