Skip to content

Commit

Permalink
Implement Spread Properties
Browse files Browse the repository at this point in the history
https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Spec.md#spread-properties

This add support for ... in object literals which is currently at stage 2.

```js
var x = {a: 1};
var y = {...a, b: 2};  // {a: 1, b: 2}
```
  • Loading branch information
arv committed Mar 22, 2016
1 parent 7ca682a commit 09a6914
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 45 deletions.
2 changes: 2 additions & 0 deletions src/Options.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const optionsV01 = enumerableOnlyObject({
sourceMaps: false,
sourceRoot: false,
spread: true,
spreadProperties: false,
symbols: true,
templateLiterals: true,
types: false,
Expand Down Expand Up @@ -153,6 +154,7 @@ addFeatureOption('generatorComprehension', EXPERIMENTAL);
addFeatureOption('jsx', EXPERIMENTAL);
addFeatureOption('memberVariables', EXPERIMENTAL);
addFeatureOption('require', EXPERIMENTAL);
addFeatureOption('spreadProperties', EXPERIMENTAL);
addFeatureOption('types', EXPERIMENTAL);

let transformOptionsPrototype = {};
Expand Down
5 changes: 5 additions & 0 deletions src/codegeneration/FromOptionsTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {ProperTailCallTransformer} from './ProperTailCallTransformer.js';
import {PropertyNameShorthandTransformer} from './PropertyNameShorthandTransformer.js';
import {RegularExpressionTransformer} from './RegularExpressionTransformer.js';
import {RestParameterTransformer} from './RestParameterTransformer.js';
import {SpreadPropertiesTransformer} from './SpreadPropertiesTransformer.js';
import {SpreadTransformer} from './SpreadTransformer.js';
import {SuperTransformer} from './SuperTransformer.js';
import {SymbolTransformer} from './SymbolTransformer.js';
Expand Down Expand Up @@ -168,6 +169,10 @@ export class FromOptionsTransformer extends MultiTransformer {
append(ClassTransformer);
}

if (transformOptions.spreadProperties) {
append(SpreadPropertiesTransformer);
}

if (transformOptions.propertyMethods ||
transformOptions.computedPropertyNames ||
transformOptions.properTailCalls) {
Expand Down
33 changes: 6 additions & 27 deletions src/codegeneration/JsxTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import {
JSX_PLACEHOLDER,
JSX_SPREAD_ATTRIBUTE,
JSX_TEXT,
PROPERTY_NAME_ASSIGNMENT,
} from '../syntax/trees/ParseTreeType.js';
import {
JsxText,
LiteralExpression,
LiteralPropertyName,
SpreadExpression,
} from '../syntax/trees/ParseTrees.js';
import {ParseTreeTransformer} from './ParseTreeTransformer.js';
import {STRING} from '../syntax/TokenType.js';
Expand All @@ -39,6 +39,7 @@ import {
createTrueLiteral,
} from './ParseTreeFactory.js';
import {parseExpression} from './PlaceholderParser.js';
import {spreadProperties} from './SpreadPropertiesTransformer.js';

/**
* Desugars JSX expressions.
Expand Down Expand Up @@ -105,31 +106,8 @@ export class JsxTransformer extends ParseTreeTransformer {
// <a b='b' c='c' {...d} {...g} />
// =>
// React.createElement('a',
// $traceurRuntime.objectAssign({b: 'b', c: 'c'}, d, g))

// Accummulate consecutive jsx attributes into a single js property.
let args = [];
let accummulatedProps = null;
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.type === PROPERTY_NAME_ASSIGNMENT) {
if (!accummulatedProps) {
accummulatedProps = [];
}
accummulatedProps.push(attr);
} else { // JSX spread attribute transformed into an expression.
if (accummulatedProps) {
args.push(createObjectLiteral(accummulatedProps));
accummulatedProps = null;
}
args.push(attr);
}
}
if (accummulatedProps) {
args.push(createObjectLiteral(accummulatedProps));
}
return parseExpression `$traceurRuntime.objectAssign(${
createArgumentList(args)})`;
// $traceurRuntime.spreadProperties({b: 'b', c: 'c'}, d, g))
return spreadProperties(attrs);
}

transformJsxElementName(tree) {
Expand Down Expand Up @@ -168,7 +146,8 @@ export class JsxTransformer extends ParseTreeTransformer {
}

transformJsxSpreadAttribute(tree) {
return this.transformAny(tree.expression);
return new SpreadExpression(tree.location,
this.transformAny(tree.expression));
}


Expand Down
5 changes: 5 additions & 0 deletions src/codegeneration/PureES6Transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {InlineES6ModuleTransformer} from './InlineES6ModuleTransformer.js';
import {JsxTransformer} from './JsxTransformer.js';
import {MemberVariableTransformer} from './MemberVariableTransformer.js';
import {MultiTransformer} from './MultiTransformer.js';
import {SpreadPropertiesTransformer} from './SpreadPropertiesTransformer.js';
import {TypeTransformer} from './TypeTransformer.js';
import {UniqueIdentifierGenerator} from './UniqueIdentifierGenerator.js';
import {validate as validateFreeVariables} from
Expand Down Expand Up @@ -58,6 +59,10 @@ export class PureES6Transformer extends MultiTransformer {
append(JsxTransformer);
}

if (options.spreadProperties) {
append(SpreadPropertiesTransformer);
}

if (options.memberVariables) {
append(MemberVariableTransformer);
}
Expand Down
68 changes: 68 additions & 0 deletions src/codegeneration/SpreadPropertiesTransformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2016 Traceur Authors.
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {SPREAD_EXPRESSION} from '../syntax/trees/ParseTreeType.js';
import {createObjectLiteral, createArgumentList} from './ParseTreeFactory.js';
import {parseExpression} from './PlaceholderParser.js';
import {ParseTreeTransformer} from './ParseTreeTransformer.js';

function hasSpread(trees) {
return trees.some((tree) => tree && tree.type === SPREAD_EXPRESSION);
}

/**
* Adds support for spread properties on object literals
*
* https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Spec.md
*
* {a, ...b, c, ...d}
* =>
* $spreadProperties({a}, b, {c}, d)
*/
export class SpreadPropertiesTransformer extends ParseTreeTransformer {
transformObjectLiteral(tree) {
if (!hasSpread(tree.propertyNameAndValues)) {
return super.transformObjectLiteral(tree);
}

const properties = this.transformList(tree.propertyNameAndValues);
return spreadProperties(properties);
}
}

export function spreadProperties(properties) {
// Accummulate consecutive properties into a single js property.
let args = [];
let accummulatedProps = null;
for (let i = 0; i < properties.length; i++) {
let property = properties[i];
if (property.type === SPREAD_EXPRESSION) {
if (accummulatedProps) {
args.push(createObjectLiteral(accummulatedProps));
accummulatedProps = null;
}
args.push(property.expression);
} else {
if (!accummulatedProps) {
accummulatedProps = [];
}
accummulatedProps.push(property);
}
}
if (accummulatedProps) {
args.push(createObjectLiteral(accummulatedProps));
}
return parseExpression `$traceurRuntime.spreadProperties(${
createArgumentList(args)})`;
}
4 changes: 2 additions & 2 deletions src/runtime/jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import objectAssign from './modules/objectAssign.js';
$traceurRuntime.objectAssign = objectAssign;
import spreadProperties from './modules/spreadProperties.js';
$traceurRuntime.spreadProperties = spreadProperties;
15 changes: 0 additions & 15 deletions src/runtime/modules/objectAssign.js

This file was deleted.

59 changes: 59 additions & 0 deletions src/runtime/modules/spreadProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2016 Traceur Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

const {
defineProperty,
getOwnPropertyNames,
getOwnPropertySymbols,
propertyIsEnumerable,
} = Object;

function createDataProperty(o, p, v) {
defineProperty(o, p, {
configurable: true,
enumerable: true,
value: v,
writable: true,
});
}

function copyDataProperties(target, source) {
if (source == null) { // needs to be ==
return;
}

// The spec has a ToObject here but getOwnProperty* does the ToObjext so we
// do not need to call it here.

const copy = keys => {
for (let i = 0; i < keys.length; i++) {
const nextKey = keys[i];
if (propertyIsEnumerable.call(source, nextKey)) {
const propValue = source[nextKey];
createDataProperty(target, nextKey, propValue);
}
}
};

copy(getOwnPropertyNames(source));
copy(getOwnPropertySymbols(source));
}

export default function() {
const target = arguments[0];
for (let i = 1; i < arguments.length; i++) {
copyDataProperties(target, arguments[i]);
}
return target;
}
2 changes: 2 additions & 0 deletions src/syntax/ParseTreeValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import {
PROPERTY_VARIABLE_DECLARATION,
REST_PARAMETER,
SET_ACCESSOR,
SPREAD_EXPRESSION,
TEMPLATE_LITERAL_PORTION,
TEMPLATE_SUBSTITUTION,
TYPE_ALIAS_DECLARATION,
Expand Down Expand Up @@ -827,6 +828,7 @@ export class ParseTreeValidator extends ParseTreeVisitor {
break;
case PROPERTY_NAME_ASSIGNMENT:
case PROPERTY_NAME_SHORTHAND:
case SPREAD_EXPRESSION:
break;
default:
this.fail_(propertyNameAndValue, 'accessor, property name ' +
Expand Down
8 changes: 7 additions & 1 deletion src/syntax/Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2409,6 +2409,7 @@ export class Parser {
this.popFunctionState_(fs);
return result;
}

parsePropertyDefinition_() {
let start = this.getTreeStartLocation_();

Expand All @@ -2424,6 +2425,10 @@ export class Parser {
return m;
}

if (this.options_.spreadProperties && peek(DOT_DOT_DOT)) {
return this.parseSpreadExpression_();
}

let token = peekToken();
let name = this.parsePropertyName_();

Expand Down Expand Up @@ -2667,7 +2672,8 @@ export class Parser {
*/
peekPropertyDefinition_(type) {
return this.peekPropertyName_(type) ||
type === STAR && this.options_.propertyMethods && this.options_.generators;
type === STAR && this.options_.propertyMethods && this.options_.generators ||
type === DOT_DOT_DOT && this.options_.spreadProperties;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions test/feature/SpreadProperties/Getters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Options: --spread-properties

var n = 0;
var o;
assert.deepEqual(o = {a: 'a', ...{get a() { n++; return 'b'; }}}, {a: 'b'});
assert.equal(1, n);
assert.equal(o.a, 'b');
assert.equal(1, n);
18 changes: 18 additions & 0 deletions test/feature/SpreadProperties/Setters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Options: --spread-properties

function fail() {
assert.isTrue(false, 'unreachable');
}

var o;
assert.deepEqual({set a(x) {fail()}, ...{a: 'a'}}, {a: 'a'});
assert.deepEqual(o = {a: 'a', ...{set a(x) {fail()}}}, {a: undefined});
o.a = 'b';
assert.equal(o.a, 'b');

var n = 0;
var o;
assert.deepEqual(o = {a: 'a', ...{get a() { n++; return 'b'; }}}, {a: 'b'});
assert.equal(1, n);
assert.equal(o.a, 'b');
assert.equal(1, n);
13 changes: 13 additions & 0 deletions test/feature/SpreadProperties/Simple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Options: --spread-properties

assert.deepEqual({...{a: 'a'}}, {a: 'a'});
assert.deepEqual({a: 'a', ...{b: 'b'}}, {a: 'a', b: 'b'});
assert.deepEqual({a: 'a', ...{b: 'b'}, c: 'c'}, {a: 'a', b: 'b', c: 'c'});
assert.deepEqual({a: 'a', b: 'b', ...{c: 'c'}}, {a: 'a', b: 'b', c: 'c'});
assert.deepEqual({a: 'a', b: 'b', c: 'c', ...{}}, {a: 'a', b: 'b', c: 'c'});
assert.deepEqual({...{a: 'a'}, b: 'b', c: 'c'}, {a: 'a', b: 'b', c: 'c'});
assert.deepEqual({...{a: 'a'}, ...{b: 'b'}, ...{c: 'c'}},
{a: 'a', b: 'b', c: 'c'});

assert.deepEqual({a: 1, ...{a: 2}}, {a: 2});
assert.deepEqual({...{a: 1}, a: 2}, {a: 2});
30 changes: 30 additions & 0 deletions test/feature/SpreadProperties/Super.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Options: --spread-properties

var p = {
m() {
return 'm';
},
n() {
return 'n';
},
};

var a = {a: 'a'};
var b = {b: 'b'};

var o = {
__proto__: p,
...a,
m() {
return super.m();
},
...b,
n() {
return super.n();
},
};

assert.equal(o.a, 'a');
assert.equal(o.b, 'b');
assert.equal(o.m(), 'm');
assert.equal(o.n(), 'n');
8 changes: 8 additions & 0 deletions test/feature/SpreadProperties/Symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Options: --spread-properties

var s1 = Symbol();
var s2 = Symbol();

var o = {[s1]: 1, ...{[s2]: 2}};
assert.equal(o[s1], 1);
assert.equal(o[s2], 2);

0 comments on commit 09a6914

Please sign in to comment.