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() {