From 98c5255b91389f8d9c262ef1977d1a0c6f8d51b8 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sat, 11 Apr 2015 16:30:55 -0700 Subject: [PATCH] add support for object literal decorators - fixes #1154 --- src/acorn/src/expression.js | 9 +++ src/acorn/src/statement.js | 2 +- src/babel/transformation/file/index.js | 1 + .../transformation/helpers/define-map.js | 36 +++++++++-- .../helper-create-decorated-object.js | 33 ++++++++++ .../transformers/es5/properties.mutators.js | 4 +- .../transformers/es6/classes.js | 33 +--------- .../transformers/es7/decorators.js | 29 +++++++++ test/acorn/tests-babel.js | 60 +++++++++++++++++++ .../class-init-instance-props/expected.js | 6 +- .../class-init-static-props/expected.js | 6 +- .../class-no-init-instance-props/expected.js | 6 +- .../class-no-init-static-props/expected.js | 6 +- .../exec-object-method-autobind.js | 20 +++++++ .../object-getter-and-setter/actual.js | 11 ++++ .../object-getter-and-setter/expected.js | 8 +++ .../es7.decorators/object-getter/actual.js | 6 ++ .../es7.decorators/object-getter/expected.js | 7 +++ .../es7.decorators/object-setter/actual.js | 6 ++ .../es7.decorators/object-setter/expected.js | 7 +++ .../es7.decorators/object/actual.js | 11 ++++ .../es7.decorators/object/expected.js | 14 +++++ 22 files changed, 270 insertions(+), 51 deletions(-) create mode 100644 src/babel/transformation/templates/helper-create-decorated-object.js create mode 100644 test/core/fixtures/transformation/es7.decorators/exec-class-method-autobind/exec-object-method-autobind.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/actual.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/expected.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-getter/actual.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-getter/expected.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-setter/actual.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object-setter/expected.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object/actual.js create mode 100644 test/core/fixtures/transformation/es7.decorators/object/expected.js diff --git a/src/acorn/src/expression.js b/src/acorn/src/expression.js index 9309f2e3e7..7d62e0cc90 100755 --- a/src/acorn/src/expression.js +++ b/src/acorn/src/expression.js @@ -516,10 +516,15 @@ pp.parseObj = function(isPattern, refShorthandDefaultPos) { if (this.afterTrailingComma(tt.braceR)) break } else first = false + while (this.type === tt.at) { + this.decorators.push(this.parseDecorator()) + } + let prop = this.startNode(), isGenerator = false, isAsync = false, start if (this.options.features["es7.objectRestSpread"] && this.type === tt.ellipsis) { prop = this.parseSpread() prop.type = "SpreadProperty" + this.takeDecorators(prop) node.properties.push(prop) continue } @@ -545,8 +550,12 @@ pp.parseObj = function(isPattern, refShorthandDefaultPos) { } this.parseObjPropValue(prop, start, isGenerator, isAsync, isPattern, refShorthandDefaultPos); this.checkPropClash(prop, propHash) + this.takeDecorators(prop) node.properties.push(this.finishNode(prop, "Property")) } + if (this.decorators.length) { + this.raise(this.start, "You have trailing decorators with no property"); + } return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression") } diff --git a/src/acorn/src/statement.js b/src/acorn/src/statement.js index 08ae517d3d..35104336d9 100755 --- a/src/acorn/src/statement.js +++ b/src/acorn/src/statement.js @@ -475,7 +475,7 @@ pp.parseClass = function(node, isStatement) { if (this.eat(tt.semi)) continue if (this.type === tt.at) { this.decorators.push(this.parseDecorator()) - continue; + continue } var method = this.startNode() this.takeDecorators(method) diff --git a/src/babel/transformation/file/index.js b/src/babel/transformation/file/index.js index 8f9fc5d2c0..8fbf30093b 100644 --- a/src/babel/transformation/file/index.js +++ b/src/babel/transformation/file/index.js @@ -59,6 +59,7 @@ export default class File { "defaults", "create-class", "create-decorated-class", + "create-decorated-object", "tagged-template-literal", "tagged-template-literal-loose", "interop-require", diff --git a/src/babel/transformation/helpers/define-map.js b/src/babel/transformation/helpers/define-map.js index 8f09a27e18..de02f1275a 100644 --- a/src/babel/transformation/helpers/define-map.js +++ b/src/babel/transformation/helpers/define-map.js @@ -4,17 +4,43 @@ import each from "lodash/collection/each"; import has from "lodash/object/has"; import * as t from "../../types"; -export function push(mutatorMap, key, kind, computed, value) { - var alias = t.toKeyAlias({ computed }, key); +export function push(mutatorMap, node, kind, file) { + var alias = t.toKeyAlias(node); + + // var map = {}; if (has(mutatorMap, alias)) map = mutatorMap[alias]; mutatorMap[alias] = map; - map._key = key; - if (computed) map._computed = true; + // - map[kind] = value; + map._inherits ||= []; + map._inherits.push(node); + + map._key = node.key; + + if (node.computed) { + map._computed = true; + } + + if (node.decorators) { + var decorators = map.decorators ||= t.arrayExpression([]); + decorators.elements = decorators.elements.concat(node.decorators.map(dec => dec.expression)); + } + + if (map.value || map.initializer) { + throw file.errorWithNode(node, "Key conflict with sibling node"); + } + + if (node.kind === "init") kind = "value"; + if (node.kind === "get") kind = "get"; + if (node.kind === "set") kind = "set"; + + t.inheritsComments(node.value, node); + map[kind] = node.value; + + return map; } export function hasComputed(mutatorMap) { diff --git a/src/babel/transformation/templates/helper-create-decorated-object.js b/src/babel/transformation/templates/helper-create-decorated-object.js new file mode 100644 index 0000000000..03e2e81b52 --- /dev/null +++ b/src/babel/transformation/templates/helper-create-decorated-object.js @@ -0,0 +1,33 @@ +(function (descriptors) { + var target = {}; + + for (var i = 0; i < descriptors.length; i ++) { + var descriptor = descriptors[i]; + var decorators = descriptor.decorators; + var key = descriptor.key; + + // don't want to expose these to userland since i know people will rely on them + // and think it's spec behaviour + delete descriptor.key; + delete descriptor.decorators; + + descriptor.enumerable = true; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + + if (decorators) { + for (var f = 0; f < decorators.length; f++) { + var decorator = decorators[f]; + if (typeof decorator === "function") { + descriptor = decorator(target, key, descriptor) || descriptor; + } else { + throw new TypeError("The decorator for method " + descriptor.key + " is of the invalid type " + typeof decorator); + } + } + } + + Object.defineProperty(target, key, descriptor); + } + + return target; +}) diff --git a/src/babel/transformation/transformers/es5/properties.mutators.js b/src/babel/transformation/transformers/es5/properties.mutators.js index e2cd098722..b199bbccef 100644 --- a/src/babel/transformation/transformers/es5/properties.mutators.js +++ b/src/babel/transformation/transformers/es5/properties.mutators.js @@ -5,14 +5,14 @@ export function check(node) { return t.isProperty(node) && (node.kind === "get" || node.kind === "set"); } -export function ObjectExpression(node) { +export function ObjectExpression(node, parent, scope, file) { var mutatorMap = {}; var hasAny = false; node.properties = node.properties.filter(function (prop) { if (prop.kind === "get" || prop.kind === "set") { hasAny = true; - defineMap.push(mutatorMap, prop.key, prop.kind, prop.computed, prop.value); + defineMap.push(mutatorMap, prop, prop.kind, file); return false; } else { return true; diff --git a/src/babel/transformation/transformers/es6/classes.js b/src/babel/transformation/transformers/es6/classes.js index 2a17c4db96..ceed3114a3 100644 --- a/src/babel/transformation/transformers/es6/classes.js +++ b/src/babel/transformation/transformers/es6/classes.js @@ -243,44 +243,15 @@ class ClassTransformer { mutatorMap = this.instanceMutatorMap; } - var alias = t.toKeyAlias(node); - - // - - var map = {}; - if (has(mutatorMap, alias)) map = mutatorMap[alias]; - mutatorMap[alias] = map; - - // - - map._inherits ||= []; - map._inherits.push(node); - - map._key = node.key; + var map = defineMap.push(mutatorMap, node, kind, this.file); if (enumerable) { map.enumerable = t.literal(true) } - if (node.computed) { - map._computed = true; - } - - if (node.decorators) { + if (map.decorators) { this.hasDecorators = true; - var decorators = map.decorators ||= t.arrayExpression([]); - decorators.elements = decorators.elements.concat(node.decorators.map(dec => dec.expression)); } - - if (map.value || map.initializer) { - throw this.file.errorWithNode(node, "Key conflict with sibling node"); - } - - if (node.kind === "get") kind = "get"; - if (node.kind === "set") kind = "set"; - - t.inheritsComments(node.value, node); - map[kind] = node.value; } /** diff --git a/src/babel/transformation/transformers/es7/decorators.js b/src/babel/transformation/transformers/es7/decorators.js index 576c7fb7c0..a7980b6920 100644 --- a/src/babel/transformation/transformers/es7/decorators.js +++ b/src/babel/transformation/transformers/es7/decorators.js @@ -1,4 +1,33 @@ +import * as defineMap from "../../helpers/define-map"; +import * as t from "../../../types"; + export var metadata = { optional: true, stage: 1 }; + +export function check(node) { + return !!node.decorators; +} + +export function ObjectExpression(node, parent, scope, file) { + var hasDecorators = false; + for (var i = 0; i < node.properties.length; i++) { + var prop = node.properties[i]; + if (prop.decorators) { + hasDecorators = true; + break; + } + } + if (!hasDecorators) return; + + var mutatorMap = {}; + + for (var i = 0; i < node.properties.length; i++) { + defineMap.push(mutatorMap, node.properties[i], null, file); + } + + var obj = defineMap.toClassObject(mutatorMap); + obj = defineMap.toComputedObjectFromClass(obj); + return t.callExpression(file.addHelper("create-decorated-object"), [obj]); +} diff --git a/test/acorn/tests-babel.js b/test/acorn/tests-babel.js index b46ddf9628..381aa4ea4a 100644 --- a/test/acorn/tests-babel.js +++ b/test/acorn/tests-babel.js @@ -2787,6 +2787,66 @@ test('class Foo { @bar static foo = "bar"; }', { features: { "es7.classProperties": true, "es7.decorators": true } }); +test("var obj = { @foo bar: 'wow' };", { + "start": 0, + "body": [{ + "start": 0, + "declarations": [{ + "start": 4, + "id": { + "start": 4, + "name": "obj", + "type": "Identifier", + "end": 7 + }, + "init": { + "start": 10, + "properties": [{ + "start": 17, + "key": { + "start": 17, + "name": "bar", + "type": "Identifier", + "end": 20 + }, + "value": { + "start": 22, + "value": "wow", + "raw": "'wow'", + "type": "Literal", + "end": 27 + }, + "kind": "init", + "decorators": [{ + "start": 12, + "expression": { + "start": 13, + "name": "foo", + "type": "Identifier", + "end": 16 + }, + "type": "Decorator", + "end": 16 + }], + "type": "Property", + "end": 27 + }], + "type": "ObjectExpression", + "end": 29 + }, + "type": "VariableDeclarator", + "end": 29 + }], + "kind": "var", + "type": "VariableDeclaration", + "end": 30 + }], + "type": "Program", + "end": 30 +}, { + features: { "es7.decorators": true } +}); + // ES7 export extensions - https://github.com/leebyron/ecmascript-more-export-from test('export foo, { bar } from "bar";', { diff --git a/test/core/fixtures/transformation/es7.decorators/class-init-instance-props/expected.js b/test/core/fixtures/transformation/es7.decorators/class-init-instance-props/expected.js index a9f20009d2..a87c3fc846 100644 --- a/test/core/fixtures/transformation/es7.decorators/class-init-instance-props/expected.js +++ b/test/core/fixtures/transformation/es7.decorators/class-init-instance-props/expected.js @@ -10,11 +10,11 @@ var Foo = (function () { babelHelpers.createDecoratedClass(Foo, [{ key: "foo", - enumerable: true, decorators: [bar], initializer: function () { return "Bar"; - } + }, + enumerable: true }], null, _instanceInitializers); return Foo; -})(); \ No newline at end of file +})(); diff --git a/test/core/fixtures/transformation/es7.decorators/class-init-static-props/expected.js b/test/core/fixtures/transformation/es7.decorators/class-init-static-props/expected.js index fef3cd04e4..f7978b1856 100644 --- a/test/core/fixtures/transformation/es7.decorators/class-init-static-props/expected.js +++ b/test/core/fixtures/transformation/es7.decorators/class-init-static-props/expected.js @@ -9,12 +9,12 @@ var Foo = (function () { babelHelpers.createDecoratedClass(Foo, null, [{ key: "foo", - enumerable: true, decorators: [bar], initializer: function () { return "Bar"; - } + }, + enumerable: true }], null, _staticInitializers); Foo.foo = _staticInitializers.foo.call(Foo); return Foo; -})(); \ No newline at end of file +})(); diff --git a/test/core/fixtures/transformation/es7.decorators/class-no-init-instance-props/expected.js b/test/core/fixtures/transformation/es7.decorators/class-no-init-instance-props/expected.js index 94d9c195e3..02b348ffff 100644 --- a/test/core/fixtures/transformation/es7.decorators/class-no-init-instance-props/expected.js +++ b/test/core/fixtures/transformation/es7.decorators/class-no-init-instance-props/expected.js @@ -10,9 +10,9 @@ var Foo = (function () { babelHelpers.createDecoratedClass(Foo, [{ key: "foo", - enumerable: true, decorators: [bar], - initializer: function () {} + initializer: function () {}, + enumerable: true }], null, _instanceInitializers); return Foo; -})(); \ No newline at end of file +})(); diff --git a/test/core/fixtures/transformation/es7.decorators/class-no-init-static-props/expected.js b/test/core/fixtures/transformation/es7.decorators/class-no-init-static-props/expected.js index 5ac983268b..aca6f49378 100644 --- a/test/core/fixtures/transformation/es7.decorators/class-no-init-static-props/expected.js +++ b/test/core/fixtures/transformation/es7.decorators/class-no-init-static-props/expected.js @@ -9,10 +9,10 @@ var Foo = (function () { babelHelpers.createDecoratedClass(Foo, null, [{ key: "foo", - enumerable: true, decorators: [bar], - initializer: function () {} + initializer: function () {}, + enumerable: true }], null, _staticInitializers); Foo.foo = _staticInitializers.foo.call(Foo); return Foo; -})(); \ No newline at end of file +})(); diff --git a/test/core/fixtures/transformation/es7.decorators/exec-class-method-autobind/exec-object-method-autobind.js b/test/core/fixtures/transformation/es7.decorators/exec-class-method-autobind/exec-object-method-autobind.js new file mode 100644 index 0000000000..01cdde6e01 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/exec-class-method-autobind/exec-object-method-autobind.js @@ -0,0 +1,20 @@ +var autobind = function (target, name, descriptor) { + var fn = descriptor.value; + delete descriptor.value; + delete descriptor.writable; + descriptor.get = function () { + return fn.bind(this); + }; +}; + +var person = { + first: "Sebastian", + last: "McKenzie", + + @autobind + getName() { + return `${this.first} ${this.last}`; + } +} + +assert.equal(person.getName.call(null), "Sebastian McKenzie") diff --git a/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/actual.js b/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/actual.js new file mode 100644 index 0000000000..772c63c875 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/actual.js @@ -0,0 +1,11 @@ +var obj = { + @foo + get foo() { + + }, + + @foo + set foo() { + + } +}; diff --git a/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/expected.js b/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/expected.js new file mode 100644 index 0000000000..117515628e --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-getter-and-setter/expected.js @@ -0,0 +1,8 @@ +"use strict"; + +var obj = babelHelpers.createDecoratedObject([{ + key: "foo", + decorators: [foo, foo], + get: function get() {}, + set: function set() {} +}]); diff --git a/test/core/fixtures/transformation/es7.decorators/object-getter/actual.js b/test/core/fixtures/transformation/es7.decorators/object-getter/actual.js new file mode 100644 index 0000000000..632f156498 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-getter/actual.js @@ -0,0 +1,6 @@ +var obj = { + @foo + get foo() { + + } +}; diff --git a/test/core/fixtures/transformation/es7.decorators/object-getter/expected.js b/test/core/fixtures/transformation/es7.decorators/object-getter/expected.js new file mode 100644 index 0000000000..bb651cf283 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-getter/expected.js @@ -0,0 +1,7 @@ +"use strict"; + +var obj = babelHelpers.createDecoratedObject([{ + key: "foo", + decorators: [foo], + get: function get() {} +}]); diff --git a/test/core/fixtures/transformation/es7.decorators/object-setter/actual.js b/test/core/fixtures/transformation/es7.decorators/object-setter/actual.js new file mode 100644 index 0000000000..ec760bb339 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-setter/actual.js @@ -0,0 +1,6 @@ +var obj = { + @foo + set foo() { + + } +}; diff --git a/test/core/fixtures/transformation/es7.decorators/object-setter/expected.js b/test/core/fixtures/transformation/es7.decorators/object-setter/expected.js new file mode 100644 index 0000000000..6b16d5c65c --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object-setter/expected.js @@ -0,0 +1,7 @@ +"use strict"; + +var obj = babelHelpers.createDecoratedObject([{ + key: "foo", + decorators: [foo], + set: function set() {} +}]); diff --git a/test/core/fixtures/transformation/es7.decorators/object/actual.js b/test/core/fixtures/transformation/es7.decorators/object/actual.js new file mode 100644 index 0000000000..03c6a75573 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object/actual.js @@ -0,0 +1,11 @@ +var obj = { + @foo + bar() { + + }, + + @bar + foo: "lol", + + yes: "wow" +}; diff --git a/test/core/fixtures/transformation/es7.decorators/object/expected.js b/test/core/fixtures/transformation/es7.decorators/object/expected.js new file mode 100644 index 0000000000..9cb660df56 --- /dev/null +++ b/test/core/fixtures/transformation/es7.decorators/object/expected.js @@ -0,0 +1,14 @@ +"use strict"; + +var obj = babelHelpers.createDecoratedObject([{ + key: "bar", + decorators: [foo], + value: function value() {} +}, { + key: "foo", + decorators: [bar], + value: "lol" +}, { + key: "yes", + value: "wow" +}]);