From 91fc1c82b56986df51ad1450c18718bc585deca9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 8 Nov 2020 15:38:32 +0000 Subject: [PATCH] support computed property name in object literal (#4268) --- lib/ast.js | 26 +++++----- lib/compress.js | 106 +++++++++++++++++++++------------------ lib/mozilla-ast.js | 5 +- lib/output.js | 38 +++++++------- lib/parse.js | 36 ++++++------- lib/propmangle.js | 14 ++---- lib/transform.js | 1 + test/compress/objects.js | 35 +++++++++++++ test/mocha/tokens.js | 8 +-- test/ufuzz/index.js | 30 ++++++----- 10 files changed, 163 insertions(+), 136 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index ad58ec21..02a38b9e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1015,24 +1015,28 @@ var AST_Object = DEFNODE("Object", "properties", { var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { $documentation: "Base class for literal object properties", $propdoc: { - key: "[string|AST_SymbolAccessor] property name. For ObjectKeyVal this is a string. For getters and setters this is an AST_SymbolAccessor.", - value: "[AST_Node] property value. For getters and setters this is an AST_Accessor." + key: "[string|AST_Node] property name. For computed property this is an AST_Node.", + value: "[AST_Node] property value. For getters and setters this is an AST_Accessor.", }, walk: function(visitor) { var node = this; visitor.visit(node, function() { + if (node.key instanceof AST_Node) node.key.walk(visitor); node.value.walk(visitor); }); - } + }, + _validate: function() { + if (typeof this.key != "string") { + if (!(this.key instanceof AST_Node)) throw new Error("key must be string or AST_Node"); + must_be_expression(this, "key"); + } + if (!(this.value instanceof AST_Node)) throw new Error("value must be AST_Node"); + }, }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", null, { $documentation: "A key: value object property", - $propdoc: { - quote: "[string] the original quote character" - }, _validate: function() { - if (typeof this.key != "string") throw new Error("key must be string"); must_be_expression(this, "value"); }, }, AST_ObjectProperty); @@ -1040,7 +1044,6 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { $documentation: "An object setter property", _validate: function() { - if (!(this.key instanceof AST_SymbolAccessor)) throw new Error("key must be AST_SymbolAccessor"); if (!(this.value instanceof AST_Accessor)) throw new Error("value must be AST_Accessor"); }, }, AST_ObjectProperty); @@ -1048,7 +1051,6 @@ var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { $documentation: "An object getter property", _validate: function() { - if (!(this.key instanceof AST_SymbolAccessor)) throw new Error("key must be AST_SymbolAccessor"); if (!(this.value instanceof AST_Accessor)) throw new Error("value must be AST_Accessor"); }, }, AST_ObjectProperty); @@ -1065,10 +1067,6 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { }, }); -var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { - $documentation: "The name of a property accessor (setter/getter function)" -}, AST_Symbol); - var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { $documentation: "A declaration symbol (symbol in var, function name or argument, symbol in catch)", }, AST_Symbol); diff --git a/lib/compress.js b/lib/compress.js index 47eba90f..87e26f88 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1749,11 +1749,10 @@ merge(Compressor.prototype, { } } else if (expr instanceof AST_Object) { expr.properties.forEach(function(prop) { - if (prop instanceof AST_ObjectKeyVal) { - hit_stack.push(prop); - extract_candidates(prop.value); - hit_stack.pop(); - } + hit_stack.push(prop); + if (prop.key instanceof AST_Node) extract_candidates(prop.key); + if (prop instanceof AST_ObjectKeyVal) extract_candidates(prop.value); + hit_stack.pop(); }); } else if (expr instanceof AST_Sequence) { expr.expressions.forEach(extract_candidates); @@ -1799,7 +1798,7 @@ merge(Compressor.prototype, { if (parent instanceof AST_Exit) return node; if (parent instanceof AST_If) return node; if (parent instanceof AST_IterationStatement) return node; - if (parent instanceof AST_ObjectKeyVal) return node; + if (parent instanceof AST_ObjectProperty) return node; if (parent instanceof AST_PropAccess) return node; if (parent instanceof AST_Sequence) { return (parent.tail_node() === node ? find_stop : find_stop_unused)(parent, level + 1); @@ -1857,7 +1856,7 @@ merge(Compressor.prototype, { if (parent.condition !== node) return node; return find_stop_value(parent, level + 1); } - if (parent instanceof AST_ObjectKeyVal) { + if (parent instanceof AST_ObjectProperty) { var obj = scanner.parent(level + 1); return all(obj.properties, function(prop) { return prop instanceof AST_ObjectKeyVal; @@ -1905,7 +1904,7 @@ merge(Compressor.prototype, { if (parent instanceof AST_Exit) return find_stop_unused(parent, level + 1); if (parent instanceof AST_If) return find_stop_unused(parent, level + 1); if (parent instanceof AST_IterationStatement) return node; - if (parent instanceof AST_ObjectKeyVal) { + if (parent instanceof AST_ObjectProperty) { var obj = scanner.parent(level + 1); return all(obj.properties, function(prop) { return prop instanceof AST_ObjectKeyVal; @@ -2699,9 +2698,12 @@ merge(Compressor.prototype, { if (prop instanceof AST_Node) break; prop = "" + prop; var diff = prop == "__proto__" || compressor.has_directive("use strict") ? function(node) { - return node.key != prop && node.key.name != prop; + return typeof node.key == "string" && node.key != prop; } : function(node) { - return node.key.name != prop; + if (node instanceof AST_ObjectGetter || node instanceof AST_ObjectSetter) { + return typeof node.key == "string" && node.key != prop; + } + return true; }; if (!all(value.properties, diff)) break; value.properties.push(make_node(AST_ObjectKeyVal, node, { @@ -2986,10 +2988,9 @@ merge(Compressor.prototype, { def(AST_Lambda, return_false); def(AST_Null, return_true); def(AST_Object, function(compressor) { - if (!is_strict(compressor)) return false; - for (var i = this.properties.length; --i >=0;) - if (this.properties[i].value instanceof AST_Accessor) return true; - return false; + return is_strict(compressor) && !all(this.properties, function(prop) { + return prop instanceof AST_ObjectKeyVal; + }); }); def(AST_Sequence, function(compressor) { return this.tail_node()._dot_throw(compressor); @@ -3585,8 +3586,12 @@ merge(Compressor.prototype, { var val = {}; for (var i = 0; i < this.properties.length; i++) { var prop = this.properties[i]; + if (!(prop instanceof AST_ObjectKeyVal)) return this; var key = prop.key; - if (key instanceof AST_Symbol) key = key.name; + if (key instanceof AST_Node) { + key = key._eval(compressor, ignore_side_effects, cached, depth); + if (key === prop.key) return this; + } if (prop.value instanceof AST_Function) { if (typeof Object.prototype[key] == "function") return this; continue; @@ -4108,7 +4113,8 @@ merge(Compressor.prototype, { return any(this.properties, compressor); }); def(AST_ObjectProperty, function(compressor) { - return this.value.has_side_effects(compressor); + return this.key instanceof AST_Node && this.key.has_side_effects(compressor) + || this.value.has_side_effects(compressor); }); def(AST_Sub, function(compressor) { return this.expression.may_throw_on_access(compressor) @@ -4220,7 +4226,8 @@ merge(Compressor.prototype, { return any(this.properties, compressor); }); def(AST_ObjectProperty, function(compressor) { - return this.value.may_throw(compressor); + return this.key instanceof AST_Node && this.key.may_throw(compressor) + || this.value.may_throw(compressor); }); def(AST_Return, function(compressor) { return this.value && this.value.may_throw(compressor); @@ -4316,7 +4323,7 @@ merge(Compressor.prototype, { return all(this.properties); }); def(AST_ObjectProperty, function() { - return this.value.is_constant_expression(); + return typeof this.key == "string" && this.value.is_constant_expression(); }); def(AST_Unary, function() { return this.expression.is_constant_expression(); @@ -5742,7 +5749,7 @@ merge(Compressor.prototype, { return right instanceof AST_Object && right.properties.length > 0 && all(right.properties, function(prop) { - return prop instanceof AST_ObjectKeyVal; + return prop instanceof AST_ObjectKeyVal && typeof prop.key == "string"; }) && all(def.references, function(ref) { return ref.fixed_value() === right; @@ -5932,12 +5939,14 @@ merge(Compressor.prototype, { return safe_to_drop(this, compressor) ? null : this; }); def(AST_Object, function(compressor, first_in_statement) { - var values = trim(this.properties, compressor, first_in_statement); + var exprs = []; + this.properties.forEach(function(prop) { + if (prop.key instanceof AST_Node) exprs.push(prop.key); + exprs.push(prop.value); + }); + var values = trim(exprs, compressor, first_in_statement); return values && make_sequence(this, values); }); - def(AST_ObjectProperty, function(compressor, first_in_statement) { - return this.value.drop_side_effect_free(compressor, first_in_statement); - }); def(AST_Sequence, function(compressor, first_in_statement) { var expressions = trim(this.expressions, compressor, first_in_statement); if (!expressions) return null; @@ -9317,22 +9326,21 @@ merge(Compressor.prototype, { var props = expr.properties; for (var i = props.length; --i >= 0;) { var prop = props[i]; - if ("" + prop.key == key) { - if (!all(props, function(prop) { - return prop instanceof AST_ObjectKeyVal; - })) break; - if (!safe_to_flatten(prop.value, compressor)) break; - return make_node(AST_Sub, this, { - expression: make_node(AST_Array, expr, { - elements: props.map(function(prop) { - return prop.value; - }) - }), - property: make_node(AST_Number, this, { - value: i + if (prop.key != key) continue; + if (!all(props, function(prop) { + return prop instanceof AST_ObjectKeyVal && typeof prop.key == "string"; + })) break; + if (!safe_to_flatten(prop.value, compressor)) break; + return make_node(AST_Sub, this, { + expression: make_node(AST_Array, expr, { + elements: props.map(function(prop) { + return prop.value; }) - }); - } + }), + property: make_node(AST_Number, this, { + value: i + }) + }); } } }); @@ -9402,22 +9410,22 @@ merge(Compressor.prototype, { var keys = new Dictionary(); var values = []; self.properties.forEach(function(prop) { - if (typeof prop.key != "string") { - flush(); - values.push(prop); - return; + if (prop.key instanceof AST_Node) { + var key = prop.key.evaluate(compressor); + if (key !== prop.key) prop.key = "" + key; } - if (prop.value.has_side_effects(compressor)) { + if (prop instanceof AST_ObjectKeyVal && typeof prop.key == "string") { + if (prop.value.has_side_effects(compressor)) flush(); + keys.add(prop.key, prop.value); + } else { flush(); + values.push(prop); } - keys.add(prop.key, prop.value); }); flush(); - if (self.properties.length != values.length) { - return make_node(AST_Object, self, { - properties: values - }); - } + if (self.properties.length != values.length) return make_node(AST_Object, self, { + properties: values + }); return self; function flush() { diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js index 1ce16921..3ceaf584 100644 --- a/lib/mozilla-ast.js +++ b/lib/mozilla-ast.js @@ -115,9 +115,6 @@ value : from_moz(M.value) }; if (M.kind == "init") return new AST_ObjectKeyVal(args); - args.key = new AST_SymbolAccessor({ - name: args.key - }); args.value = new AST_Accessor(args.value); if (M.kind == "get") return new AST_ObjectGetter(args); if (M.kind == "set") return new AST_ObjectSetter(args); @@ -385,7 +382,7 @@ def_to_moz(AST_ObjectProperty, function To_Moz_Property(M) { var key = { type: "Literal", - value: M.key instanceof AST_SymbolAccessor ? M.key.name : M.key + value: M.key }; var kind; if (M instanceof AST_ObjectKeyVal) { diff --git a/lib/output.js b/lib/output.js index ef1d41f6..884cafc3 100644 --- a/lib/output.js +++ b/lib/output.js @@ -699,6 +699,7 @@ function OutputStream(options) { // (false, true) ? (a = 10, b = 20) : (c = 30) // ==> 20 (side effect, set a := 10 and b := 20) || p instanceof AST_Conditional + // { [(1, 2)]: 3 }[2] ==> 3 // { foo: (1, 2) }.foo ==> 2 || p instanceof AST_ObjectProperty // (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2 @@ -1289,25 +1290,33 @@ function OutputStream(options) { else print_braced_empty(this, output); }); - function print_property_name(key, quote, output) { - if (output.option("quote_keys")) { + function print_property_key(self, output) { + var key = self.key; + if (key instanceof AST_Node) { + output.with_square(function() { + key.print(output); + }); + } else if (output.option("quote_keys")) { output.print_string(key); } else if ("" + +key == key && key >= 0) { output.print(make_num(key)); - } else if (RESERVED_WORDS[key] ? !output.option("ie8") : is_identifier_string(key)) { - if (quote && output.option("keep_quoted_props")) { - output.print_string(key, quote); + } else { + var quote = self.start && self.start.quote; + if (RESERVED_WORDS[key] ? !output.option("ie8") : is_identifier_string(key)) { + if (quote && output.option("keep_quoted_props")) { + output.print_string(key, quote); + } else { + output.print_name(key); + } } else { - output.print_name(key); + output.print_string(key, quote); } - } else { - output.print_string(key, quote); } } DEFPRINT(AST_ObjectKeyVal, function(output) { var self = this; - print_property_name(self.key, self.quote, output); + print_property_key(self, output); output.colon(); self.value.print(output); }); @@ -1316,7 +1325,7 @@ function OutputStream(options) { var self = this; output.print(type); output.space(); - print_property_name(self.key.name, self.quote, output); + print_property_key(self, output); self.value._codegen(output, true); }; } @@ -1488,14 +1497,7 @@ function OutputStream(options) { output.add_mapping(this.start); }); - DEFMAP([ - AST_ObjectGetter, - AST_ObjectSetter, - ], function(output) { - output.add_mapping(this.start, this.key.name); - }); - DEFMAP([ AST_ObjectProperty ], function(output) { - output.add_mapping(this.start, this.key); + if (typeof this.key == "string") output.add_mapping(this.start, this.key); }); })(); diff --git a/lib/parse.js b/lib/parse.js index 84b14439..c91fb73f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1340,8 +1340,7 @@ function parse($TEXT, options) { // allow trailing comma if (!options.strict && is("punc", "}")) break; var start = S.token; - var type = start.type; - var name = as_property_name(); + var key = as_property_key(); if (is("punc", "(")) { var func_start = S.token; var func = function_(AST_Function); @@ -1349,22 +1348,17 @@ function parse($TEXT, options) { func.end = prev(); a.push(new AST_ObjectKeyVal({ start: start, - quote: start.quote, - key: "" + name, + key: key, value: func, end: prev(), })); continue; } - if (!is("punc", ":") && type == "name") switch (name) { + if (!is("punc", ":") && start.type == "name") switch (key) { case "get": a.push(new AST_ObjectGetter({ start: start, - key: new AST_SymbolAccessor({ - start: S.token, - name: "" + as_property_name(), - end: prev(), - }), + key: as_property_key(), value: create_accessor(), end: prev(), })); @@ -1372,11 +1366,7 @@ function parse($TEXT, options) { case "set": a.push(new AST_ObjectSetter({ start: start, - key: new AST_SymbolAccessor({ - start: S.token, - name: "" + as_property_name(), - end: prev(), - }), + key: as_property_key(), value: create_accessor(), end: prev(), })); @@ -1384,8 +1374,7 @@ function parse($TEXT, options) { default: a.push(new AST_ObjectKeyVal({ start: start, - quote: start.quote, - key: "" + name, + key: key, value: _make_symbol(AST_SymbolRef, start), end: prev(), })); @@ -1394,8 +1383,7 @@ function parse($TEXT, options) { expect(":"); a.push(new AST_ObjectKeyVal({ start: start, - quote: start.quote, - key: "" + name, + key: key, value: expression(false), end: prev(), })); @@ -1404,7 +1392,7 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }); }); - function as_property_name() { + function as_property_key() { var tmp = S.token; switch (tmp.type) { case "operator": @@ -1415,7 +1403,13 @@ function parse($TEXT, options) { case "keyword": case "atom": next(); - return tmp.value; + return "" + tmp.value; + case "punc": + if (tmp.value != "[") unexpected(); + next(); + var key = expression(false); + expect("]"); + return key; default: unexpected(); } diff --git a/lib/propmangle.js b/lib/propmangle.js index e47497de..80797599 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -81,8 +81,8 @@ var builtins = function() { function reserve_quoted_keys(ast, reserved) { ast.walk(new TreeWalker(function(node) { - if (node instanceof AST_ObjectKeyVal) { - if (node.quote) add(node.key); + if (node instanceof AST_ObjectProperty) { + if (node.start && node.start.quote) add(node.key); } else if (node instanceof AST_Sub) { addStrings(node.property, add); } @@ -165,11 +165,8 @@ function mangle_properties(ast, options) { } } else if (node instanceof AST_Dot) { add(node.property); - } else if (node instanceof AST_ObjectKeyVal) { - add(node.key); } else if (node instanceof AST_ObjectProperty) { - // setter or getter, since KeyVal is handled above - add(node.key.name); + if (typeof node.key == "string") add(node.key); } else if (node instanceof AST_Sub) { addStrings(node.property, add); } @@ -198,11 +195,8 @@ function mangle_properties(ast, options) { } } else if (node instanceof AST_Dot) { node.property = mangle(node.property); - } else if (node instanceof AST_ObjectKeyVal) { - node.key = mangle(node.key); } else if (node instanceof AST_ObjectProperty) { - // setter or getter - node.key.name = mangle(node.key.name); + if (typeof node.key == "string") node.key = mangle(node.key); } else if (node instanceof AST_Sub) { if (!options.keep_quoted) mangleStrings(node.property); } diff --git a/lib/transform.js b/lib/transform.js index 13598ea1..4aeb0f02 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -164,6 +164,7 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); DEF(AST_ObjectProperty, function(self, tw) { + if (self.key instanceof AST_Node) self.key = self.key.transform(tw); self.value = self.value.transform(tw); }); })(function(node, descend) { diff --git a/test/compress/objects.js b/test/compress/objects.js index fb1e5865..3fb6ec04 100644 --- a/test/compress/objects.js +++ b/test/compress/objects.js @@ -221,3 +221,38 @@ numeric_literal: { "8 7 8", ] } + +evaluate_computed_key: { + options = { + evaluate: true, + objects: true, + } + input: { + console.log({ + ["foo" + "bar"]: "PASS", + }.foobar); + } + expect: { + console.log({ + foobar: "PASS", + }.foobar); + } + expect_stdout: "PASS" + node_version: ">=4" +} + +keep_computed_key: { + options = { + side_effects: true, + } + input: { + ({ + [console.log("PASS")]: 42, + }); + } + expect: { + console.log("PASS"); + } + expect_stdout: "PASS" + node_version: ">=4" +} diff --git a/test/mocha/tokens.js b/test/mocha/tokens.js index 6241f25d..4a495b5b 100644 --- a/test/mocha/tokens.js +++ b/test/mocha/tokens.js @@ -5,7 +5,7 @@ describe("tokens", function() { it("Should give correct positions for accessors", function() { // location 0 1 2 3 4 // 01234567890123456789012345678901234567890123456789 - var ast = UglifyJS.parse("var obj = { get latest() { return undefined; } }"); + var ast = UglifyJS.parse("var obj = { get [prop]() { return undefined; } }"); // test all AST_ObjectProperty tokens are set as expected var found = false; ast.walk(new UglifyJS.TreeWalker(function(node) { @@ -13,9 +13,9 @@ describe("tokens", function() { found = true; assert.equal(node.start.pos, 12); assert.equal(node.end.endpos, 46); - assert(node.key instanceof UglifyJS.AST_SymbolAccessor); - assert.equal(node.key.start.pos, 16); - assert.equal(node.key.end.endpos, 22); + assert(node.key instanceof UglifyJS.AST_SymbolRef); + assert.equal(node.key.start.pos, 17); + assert.equal(node.key.end.endpos, 21); assert(node.value instanceof UglifyJS.AST_Accessor); assert.equal(node.value.start.pos, 22); assert.equal(node.value.end.endpos, 46); diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index f158854d..82fdc37a 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -913,14 +913,18 @@ function getDotKey(assign) { return key; } -function createObjectFunction(type, recurmax, stmtDepth, canThrow) { +function createObjectKey(recurmax, stmtDepth, canThrow) { + return rng(10) ? KEYS[rng(KEYS.length)] : "[" + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + "]"; +} + +function createObjectFunction(recurmax, stmtDepth, canThrow) { var namesLenBefore = VAR_NAMES.length; var s; createBlockVariables(recurmax, stmtDepth, canThrow, function(defns) { - switch (type) { - case "get": + switch (rng(3)) { + case 0: s = [ - "get " + getDotKey() + "(){", + "get " + createObjectKey(recurmax, stmtDepth, canThrow) + "(){", strictMode(), defns(), _createStatements(2, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), @@ -928,8 +932,8 @@ function createObjectFunction(type, recurmax, stmtDepth, canThrow) { "},", ]; break; - case "set": - var prop1 = getDotKey(); + case 1: + var prop1 = createObjectKey(recurmax, stmtDepth, canThrow); var prop2; do { prop2 = getDotKey(); @@ -945,7 +949,7 @@ function createObjectFunction(type, recurmax, stmtDepth, canThrow) { break; default: s = [ - type + "(" + createParams(NO_DUPLICATE) + "){", + createObjectKey(recurmax, stmtDepth, canThrow) + "(" + createParams(NO_DUPLICATE) + "){", strictMode(), defns(), _createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), @@ -961,21 +965,15 @@ function createObjectFunction(type, recurmax, stmtDepth, canThrow) { function createObjectLiteral(recurmax, stmtDepth, canThrow) { recurmax--; var obj = ["({"]; - for (var i = rng(6); --i >= 0;) switch (rng(50)) { + for (var i = rng(6); --i >= 0;) switch (rng(30)) { case 0: - obj.push(createObjectFunction("get", recurmax, stmtDepth, canThrow)); + obj.push(createObjectFunction(recurmax, stmtDepth, canThrow)); break; case 1: - obj.push(createObjectFunction("set", recurmax, stmtDepth, canThrow)); - break; - case 2: - obj.push(createObjectFunction(KEYS[rng(KEYS.length)], recurmax, stmtDepth, canThrow)); - break; - case 3: obj.push(getVarName() + ","); break; default: - obj.push(KEYS[rng(KEYS.length)] + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "),"); + obj.push(createObjectKey(recurmax, stmtDepth, canThrow) + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "),"); break; } obj.push("})"); -- 2.34.1