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] 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)); + } + }); +});