[CS2] Destructuring object spreads (#4493)

* Don’t confuse the syntax highlighter

* Comment Assign::compilePatternMatch a bit

* Assignment expressions in conditionals are a bad practice

* Rename `wrapInBraces` to `wrapInParentheses`, to set the stage for future `wrapInBraces` that uses `{` and `wrapInBrackets` that uses `[`

* Correct comment

* object destructuring

* Allow custom position of the rest element.

* Output simple array destructuring assignments to ES2015

* Output simple object destructured assignments to ES2015

* Compile shorthand object properties to ES2015 shorthand properties

This dramatically improves the appearance of destructured imports.

* Don’t confuse the syntax highlighter

* Comment Assign::compilePatternMatch a bit

* Assignment expressions in conditionals are a bad practice

* Rename `wrapInBraces` to `wrapInParentheses`, to set the stage for future `wrapInBraces` that uses `{` and `wrapInBrackets` that uses `[`

* object destructuring

* Allow custom position of the rest element.

* rest element in object destructuring

* rest element in object destructuring

* fix string interpolation

* merging

* fixing splats in object literal

* Rest element in parameter destructuring

* merging with CS2

* merged with CS2

* Add support for the object spread initializer. https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md

* Fix misspellings, trailing whitespace, other minor details

* merging with beta2

* refactor object spread properties

* small fix

* - Fixed object spread function parameters.
- Clean up "Assign" and moved all logic for object rest properties in single method (compileObjectDestruct).
- Add helper function "objectWithoutKeys" to the "UTILITIES" for use with object rest properties,
  e.g. {a, b, r...} = obj => {a, b} = obj, r = objectWithoutKeys(...)
- Clean up "Obj" and moved all logic for object spread properties in single method (compileSpread).
- Clean up "Code".
- Add method "hasSplat" to "Obj" and "Value" for checking if Obj contains the splat.
- Enable placing spread syntax triple dots on either right or left, per #85 (https://github.com/coffeescript6/discuss/issues/85)

* Fixed typos

* Remove unused code

* Removed dots (e.g. splat) on the left side from the grammar

* Initial release for deep spread properties, e.g. obj2 = {obj.b..., a: 1} or {obj[b][c]..., d: 7}
Tests need to be prepared!

* 1. Object literal spread properties

Object literals:
- obj = { {b:{c:{d:1}}}..., a:1 }

Parenthetical:
- obj = { ( body ), a:1 }
- obj = { ( body )..., a:1 }

Invocation:
- obj = { ( (args) -> ... )(params), a:1 }
- obj = { ( (args) -> ... )(params)..., a:1 }
- obj = { foo(), a:1 }
- obj = { foo()..., a:1 }

2. Refactor, cleanup & other optimizations.

* Merged with 2.0

* Cleanup

* Some more cleanup.

* Fixed error with freeVariable and object destructuring.

* Fixed errors with object spread properties.

* Improvements, fixed errors.

* Minor improvement.

* Minor improvements.

* Typo.

* Remove unnecessary whitespace.

* Remove unnecessary whitespace.

* Changed few "assertErrorFormat" tests since parentheses are now allowed in the Obj.

* Whitespace cleanup

* Comments cleanup

* fix destructured obj param declarations

* refine fix; add test

* Refactor function args ({a, b...})

* Additional tests for object destructuring in function argument.

* Minor improvement for object destructuring variable declaration.

* refactor function args ({a, b...}) and ({a, b...} = {}); Obj And Param cleanup

* fix comment

* Fix object destructuring variable declaration.

* more tests with default values

* fix typo

* Fixed default values in object destructuring.

* small fix

* Babel’s tests for object rest spread

* Style: spaces after colons in object declarations

* Cleanup comments

* Simplify Babel tests

* Fix comments

* Fix destructuring with splats in multiple objects

* Add test for default values in detsructuring assignment with splats

* Handle default values when assigning to object splats

* Rewrite traverseRest to fix handling of dynamic keys

* Fix double parens around destructuring with splats

* Update compileObjectDestruct comments

* Improve formatting of top-level destructures with splats and tidy parens

* Added a bigger destructuring-with-defaults test and fixed a bug

* Refactor destructuring grammar to allow additional forms

* Add a missing case to ObjSpreadExpr

* These tests shouldn’t run in the browser

* Fix test.html
This commit is contained in:
Geoffrey Booth 2017-06-29 22:57:42 -07:00 committed by GitHub
parent 58c608620e
commit a7a6006533
11 changed files with 822 additions and 240 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ test/*.js
parser.output parser.output
/node_modules /node_modules
npm-debug.log* npm-debug.log*
yarn.lock

View File

@ -39,6 +39,7 @@
<script type="text/coffeescript"> <script type="text/coffeescript">
@testingBrowser = yes @testingBrowser = yes
@global = window @global = window
bold = red = green = reset = ''
stdout = document.getElementById 'stdout' stdout = document.getElementById 'stdout'
start = new Date start = new Date
@currentFile = '' @currentFile = ''

View File

@ -123,7 +123,7 @@
AssignObj: [ AssignObj: [
o('ObjAssignable', function() { o('ObjAssignable', function() {
return new Value($1); return new Value($1);
}), o('ObjAssignable : Expression', function() { }), o('ObjRestValue'), o('ObjAssignable : Expression', function() {
return new Assign(LOC(1)(new Value($1)), $3, 'object', { return new Assign(LOC(1)(new Value($1)), $3, 'object', {
operatorToken: LOC(2)(new Literal($2)) operatorToken: LOC(2)(new Literal($2))
}); });
@ -143,6 +143,29 @@
], ],
SimpleObjAssignable: [o('Identifier'), o('Property'), o('ThisProperty')], SimpleObjAssignable: [o('Identifier'), o('Property'), o('ThisProperty')],
ObjAssignable: [o('SimpleObjAssignable'), o('AlphaNumeric')], ObjAssignable: [o('SimpleObjAssignable'), o('AlphaNumeric')],
ObjRestValue: [
o('SimpleObjAssignable ...', function() {
return new Splat(new Value($1));
}), o('ObjSpreadExpr ...', function() {
return new Splat($1);
})
],
ObjSpreadExpr: [
o('ObjSpreadIdentifier'), o('Object'), o('Parenthetical'), o('Super'), o('This'), o('SUPER Arguments', function() {
return new SuperCall(LOC(1)(new Super), $2);
}), o('SimpleObjAssignable Arguments', function() {
return new Call(new Value($1), $2);
}), o('ObjSpreadExpr Arguments', function() {
return new Call($1, $2);
})
],
ObjSpreadIdentifier: [
o('SimpleObjAssignable . Property', function() {
return (new Value($1)).add(new Access($3));
}), o('SimpleObjAssignable INDEX_START IndexValue INDEX_END', function() {
return (new Value($1)).add($3);
})
],
Return: [ Return: [
o('RETURN Expression', function() { o('RETURN Expression', function() {
return new Return($2); return new Return($2);

View File

@ -1543,8 +1543,20 @@
return !this.isAssignable(); return !this.isAssignable();
} }
hasSplat() {
var j, len1, prop, ref1, splat;
ref1 = this.properties;
for (j = 0, len1 = ref1.length; j < len1; j++) {
prop = ref1[j];
if (prop instanceof Splat) {
splat = true;
}
}
return splat != null ? splat : false;
}
compileNode(o) { compileNode(o) {
var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, p, prop, props, ref1, unwrappedVal, value; var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, props, q, ref1, unwrappedVal, value;
props = this.properties; props = this.properties;
if (this.generated) { if (this.generated) {
for (j = 0, len1 = props.length; j < len1; j++) { for (j = 0, len1 = props.length; j < len1; j++) {
@ -1554,6 +1566,9 @@
} }
} }
} }
if (this.hasSplat()) {
return this.compileSpread(o);
}
idt = o.indent += TAB; idt = o.indent += TAB;
lastNoncom = this.lastNonComment(this.properties); lastNoncom = this.lastNonComment(this.properties);
if (this.lhs) { if (this.lhs) {
@ -1581,7 +1596,7 @@
} }
answer = []; answer = [];
answer.push(this.makeCode(isCompact ? '' : '\n')); answer.push(this.makeCode(isCompact ? '' : '\n'));
for (i = p = 0, len4 = props.length; p < len4; i = ++p) { for (i = q = 0, len4 = props.length; q < len4; i = ++q) {
prop = props[i]; prop = props[i];
join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n'; join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n';
indent = isCompact || prop instanceof Comment ? '' : idt; indent = isCompact || prop instanceof Comment ? '' : idt;
@ -1660,6 +1675,38 @@
return results; return results;
} }
compileSpread(o) {
var addSlice, j, len1, prop, propSlices, props, slices, splatSlice;
props = this.properties;
splatSlice = [];
propSlices = [];
slices = [];
addSlice = function() {
if (propSlices.length) {
slices.push(new Obj(propSlices));
}
if (splatSlice.length) {
slices.push(...splatSlice);
}
splatSlice = [];
return propSlices = [];
};
for (j = 0, len1 = props.length; j < len1; j++) {
prop = props[j];
if (prop instanceof Splat) {
splatSlice.push(new Value(prop.name));
addSlice();
} else {
propSlices.push(prop);
}
}
addSlice();
if (!(slices[0] instanceof Obj)) {
slices.unshift(new Obj);
}
return (new Call(new Literal('Object.assign'), slices)).compileToFragments(o);
}
}; };
Obj.prototype.children = ['properties']; Obj.prototype.children = ['properties'];
@ -2445,6 +2492,11 @@
if (!this.variable.isAssignable()) { if (!this.variable.isAssignable()) {
return this.compileDestructuring(o); return this.compileDestructuring(o);
} }
if (this.variable.isObject() && this.variable.contains(function(node) {
return node instanceof Obj && node.hasSplat();
})) {
return this.compileObjectDestruct(o);
}
} }
if (this.variable.isSplice()) { if (this.variable.isSplice()) {
return this.compileSplice(o); return this.compileSplice(o);
@ -2501,13 +2553,117 @@
return compiledName.concat(this.makeCode(this.csx ? '=' : ': '), val); return compiledName.concat(this.makeCode(this.csx ? '=' : ': '), val);
} }
answer = compiledName.concat(this.makeCode(` ${this.context || '='} `), val); answer = compiledName.concat(this.makeCode(` ${this.context || '='} `), val);
if (o.level > LEVEL_LIST || (isValue && this.variable.base instanceof Obj && !this.nestedLhs && !this.param)) { if (o.level > LEVEL_LIST || (o.level === LEVEL_TOP && isValue && this.variable.base instanceof Obj && !this.nestedLhs && !this.param)) {
return this.wrapInParentheses(answer); return this.wrapInParentheses(answer);
} else { } else {
return answer; return answer;
} }
} }
compileObjectDestruct(o) {
var fragments, getPropKey, getPropName, j, len1, restElement, restElements, result, setScopeVar, traverseRest, value, valueRef;
setScopeVar = function(prop) {
var newVar;
newVar = false;
if (prop instanceof Assign && prop.value.base instanceof Obj) {
return;
}
if (prop instanceof Assign) {
if (prop.value.base instanceof IdentifierLiteral) {
newVar = prop.value.base.compile(o);
} else {
newVar = prop.variable.base.compile(o);
}
} else {
newVar = prop.compile(o);
}
if (newVar) {
return o.scope.add(newVar, 'var', true);
}
};
getPropKey = function(prop) {
var key;
if (prop instanceof Assign) {
[prop.variable, key] = prop.variable.cache(o);
return key;
} else {
return prop;
}
};
getPropName = function(prop) {
var cached, key;
key = getPropKey(prop);
cached = prop instanceof Assign && prop.variable !== key;
if (cached || !key.isAssignable()) {
return key;
} else {
return new Literal(`'${key.compile(o)}'`);
}
};
traverseRest = (properties, source) => {
var base1, index, j, len1, nestedProperties, nestedSource, nestedSourceDefault, p, prop, restElements, restIndex;
restElements = [];
restIndex = void 0;
for (index = j = 0, len1 = properties.length; j < len1; index = ++j) {
prop = properties[index];
setScopeVar(prop.unwrap());
if (prop instanceof Assign) {
if (typeof (base1 = prop.value).isObject === "function" ? base1.isObject() : void 0) {
nestedProperties = prop.value.base.properties;
} else if (prop.value instanceof Assign && prop.value.variable.isObject()) {
nestedProperties = prop.value.variable.base.properties;
[prop.value.value, nestedSourceDefault] = prop.value.value.cache(o);
}
if (nestedProperties) {
nestedSource = new Value(source.base, source.properties.concat([new Access(getPropKey(prop))]));
if (nestedSourceDefault) {
nestedSource = new Value(new Op('?', nestedSource, nestedSourceDefault));
}
restElements = restElements.concat(traverseRest(nestedProperties, nestedSource));
}
} else if (prop instanceof Splat) {
if (restIndex != null) {
prop.error("multiple rest elements are disallowed in object destructuring");
}
restIndex = index;
restElements.push({
name: prop.name.unwrapAll(),
source,
excludeProps: new Arr((function() {
var k, len2, results;
results = [];
for (k = 0, len2 = properties.length; k < len2; k++) {
p = properties[k];
if (p !== prop) {
results.push(getPropName(p));
}
}
return results;
})())
});
}
}
if (restIndex != null) {
properties.splice(restIndex, 1);
}
return restElements;
};
[this.value, valueRef] = this.value.cache(o);
restElements = traverseRest(this.variable.base.properties, valueRef);
result = new Block([this]);
for (j = 0, len1 = restElements.length; j < len1; j++) {
restElement = restElements[j];
value = new Call(new Value(new Literal(utility('objectWithoutKeys', o))), [restElement.source, restElement.excludeProps]);
result.push(new Assign(restElement.name, value));
}
fragments = result.compileToFragments(o);
if (o.level === LEVEL_TOP) {
fragments.shift();
fragments.pop();
}
return fragments;
}
compileDestructuring(o) { compileDestructuring(o) {
var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, message, name, obj, objects, olen, ref, rest, top, val, value, vvar, vvarText; var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, message, name, obj, objects, olen, ref, rest, top, val, value, vvar, vvarText;
top = o.level === LEVEL_TOP; top = o.level === LEVEL_TOP;
@ -2825,17 +2981,13 @@
if (param.name instanceof Arr) { if (param.name instanceof Arr) {
splatParamName = o.scope.freeVariable('arg'); splatParamName = o.scope.freeVariable('arg');
params.push(ref = new Value(new IdentifierLiteral(splatParamName))); params.push(ref = new Value(new IdentifierLiteral(splatParamName)));
exprs.push(new Assign(new Value(param.name), ref, null, { exprs.push(new Assign(new Value(param.name), ref));
param: true
}));
} else { } else {
params.push(ref = param.asReference(o)); params.push(ref = param.asReference(o));
splatParamName = fragmentsToText(ref.compileNode(o)); splatParamName = fragmentsToText(ref.compileNode(o));
} }
if (param.shouldCache()) { if (param.shouldCache()) {
exprs.push(new Assign(new Value(param.name), ref, null, { exprs.push(new Assign(new Value(param.name), ref));
param: true
}));
} }
} else { } else {
splatParamName = o.scope.freeVariable('args'); splatParamName = o.scope.freeVariable('args');
@ -2848,14 +3000,10 @@
haveBodyParam = true; haveBodyParam = true;
if (param.value != null) { if (param.value != null) {
condition = new Op('===', param, new UndefinedLiteral); condition = new Op('===', param, new UndefinedLiteral);
ifTrue = new Assign(new Value(param.name), param.value, null, { ifTrue = new Assign(new Value(param.name), param.value);
param: true
});
exprs.push(new If(condition, ifTrue)); exprs.push(new If(condition, ifTrue));
} else { } else {
exprs.push(new Assign(new Value(param.name), param.asReference(o), null, { exprs.push(new Assign(new Value(param.name), param.asReference(o)));
param: true
}));
} }
} }
if (!haveSplatParam) { if (!haveSplatParam) {
@ -2875,6 +3023,17 @@
param.name.eachName(function(prop) { param.name.eachName(function(prop) {
return o.scope.parameter(prop.value); return o.scope.parameter(prop.value);
}); });
if (param.name instanceof Obj && param.name.hasSplat()) {
splatParamName = o.scope.freeVariable('arg');
o.scope.parameter(splatParamName);
ref = new Value(new IdentifierLiteral(splatParamName));
exprs.push(new Assign(new Value(param.name), ref));
if ((param.value != null) && !param.assignedInBody) {
ref = new Assign(ref, param.value, null, {
param: true
});
}
}
} else { } else {
o.scope.parameter(fragmentsToText((param.value != null ? param : ref).compileToFragments(o))); o.scope.parameter(fragmentsToText((param.value != null ? param : ref).compileToFragments(o)));
} }
@ -3060,12 +3219,12 @@
exports.Param = Param = (function() { exports.Param = Param = (function() {
class Param extends Base { class Param extends Base {
constructor(name1, value1, splat) { constructor(name1, value1, splat1) {
var message, token; var message, token;
super(); super();
this.name = name1; this.name = name1;
this.value = value1; this.value = value1;
this.splat = splat; this.splat = splat1;
message = isUnassignable(this.name.unwrapAll().value); message = isUnassignable(this.name.unwrapAll().value);
if (message) { if (message) {
this.name.error(message); this.name.error(message);
@ -4296,6 +4455,9 @@
modulo: function() { modulo: function() {
return 'function(a, b) { return (+a % (b = +b) + b) % b; }'; return 'function(a, b) { return (+a % (b = +b) + b) % b; }';
}, },
objectWithoutKeys: function() {
return "function(o, ks) { var res = {}; for (var k in o) ([].indexOf.call(ks, k) < 0 && {}.hasOwnProperty.call(o, k)) && (res[k] = o[k]); return res; }";
},
boundMethodCheck: function() { boundMethodCheck: function() {
return "function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } }"; return "function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } }";
}, },

File diff suppressed because one or more lines are too long

View File

@ -190,6 +190,7 @@ grammar =
# the ordinary **Assign** is that these allow numbers and strings as keys. # the ordinary **Assign** is that these allow numbers and strings as keys.
AssignObj: [ AssignObj: [
o 'ObjAssignable', -> new Value $1 o 'ObjAssignable', -> new Value $1
o 'ObjRestValue'
o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value $1), $3, 'object', o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value $1), $3, 'object',
operatorToken: LOC(2)(new Literal $2) operatorToken: LOC(2)(new Literal $2)
o 'ObjAssignable : o 'ObjAssignable :
@ -214,6 +215,28 @@ grammar =
o 'AlphaNumeric' o 'AlphaNumeric'
] ]
# Object literal spread properties.
ObjRestValue: [
o 'SimpleObjAssignable ...', -> new Splat new Value $1
o 'ObjSpreadExpr ...', -> new Splat $1
]
ObjSpreadExpr: [
o 'ObjSpreadIdentifier'
o 'Object'
o 'Parenthetical'
o 'Super'
o 'This'
o 'SUPER Arguments', -> new SuperCall LOC(1)(new Super), $2
o 'SimpleObjAssignable Arguments', -> new Call (new Value $1), $2
o 'ObjSpreadExpr Arguments', -> new Call $1, $2
]
ObjSpreadIdentifier: [
o 'SimpleObjAssignable . Property', -> (new Value $1).add(new Access $3)
o 'SimpleObjAssignable INDEX_START IndexValue INDEX_END', -> (new Value $1).add($3)
]
# A return statement from a function body. # A return statement from a function body.
Return: [ Return: [
o 'RETURN Expression', -> new Return $2 o 'RETURN Expression', -> new Return $2
@ -701,7 +724,6 @@ grammar =
Expression', -> new Assign $1, $4, $2 Expression', -> new Assign $1, $4, $2
] ]
# Precedence # Precedence
# ---------- # ----------

View File

@ -1157,11 +1157,20 @@ exports.Obj = class Obj extends Base
shouldCache: -> shouldCache: ->
not @isAssignable() not @isAssignable()
# Check if object contains splat.
hasSplat: ->
splat = yes for prop in @properties when prop instanceof Splat
splat ? no
compileNode: (o) -> compileNode: (o) ->
props = @properties props = @properties
if @generated if @generated
for node in props when node instanceof Value for node in props when node instanceof Value
node.error 'cannot have an implicit value in an implicit object' node.error 'cannot have an implicit value in an implicit object'
# Object spread properties. https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md
return @compileSpread o if @hasSplat()
idt = o.indent += TAB idt = o.indent += TAB
lastNoncom = @lastNonComment @properties lastNoncom = @lastNonComment @properties
@ -1203,12 +1212,10 @@ exports.Obj = class Obj extends Base
prop.variable prop.variable
else if prop not instanceof Comment else if prop not instanceof Comment
prop prop
if key instanceof Value and key.hasProperties() if key instanceof Value and key.hasProperties()
key.error 'invalid object key' if prop.context is 'object' or not key.this key.error 'invalid object key' if prop.context is 'object' or not key.this
key = key.properties[0].name key = key.properties[0].name
prop = new Assign key, prop, 'object' prop = new Assign key, prop, 'object'
if key is prop if key is prop
if prop.shouldCache() if prop.shouldCache()
[key, value] = prop.base.cache o [key, value] = prop.base.cache o
@ -1216,7 +1223,6 @@ exports.Obj = class Obj extends Base
prop = new Assign key, value, 'object' prop = new Assign key, value, 'object'
else if not prop.bareLiteral?(IdentifierLiteral) else if not prop.bareLiteral?(IdentifierLiteral)
prop = new Assign prop, prop, 'object' prop = new Assign prop, prop, 'object'
if indent then answer.push @makeCode indent if indent then answer.push @makeCode indent
prop.csx = yes if @csx prop.csx = yes if @csx
answer.push @makeCode ' ' if @csx and i is 0 answer.push @makeCode ' ' if @csx and i is 0
@ -1236,6 +1242,29 @@ exports.Obj = class Obj extends Base
prop = prop.unwrapAll() prop = prop.unwrapAll()
prop.eachName iterator if prop.eachName? prop.eachName iterator if prop.eachName?
# Object spread properties. https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md
# `obj2 = {a: 1, obj..., c: 3, d: 4}` `obj2 = Object.assign({}, {a: 1}, obj, {c: 3, d: 4})`
compileSpread: (o) ->
props = @properties
# Store object spreads.
splatSlice = []
propSlices = []
slices = []
addSlice = ->
slices.push new Obj propSlices if propSlices.length
slices.push splatSlice... if splatSlice.length
splatSlice = []
propSlices = []
for prop in props
if prop instanceof Splat
splatSlice.push new Value prop.name
addSlice()
else
propSlices.push prop
addSlice()
slices.unshift new Obj unless slices[0] instanceof Obj
(new Call new Literal('Object.assign'), slices).compileToFragments o
#### Arr #### Arr
# An array literal. # An array literal.
@ -1796,6 +1825,9 @@ exports.Assign = class Assign extends Base
# destructured variables. # destructured variables.
@variable.base.lhs = yes @variable.base.lhs = yes
return @compileDestructuring o unless @variable.isAssignable() return @compileDestructuring o unless @variable.isAssignable()
# Object destructuring. Can be removed once ES proposal hits Stage 4.
return @compileObjectDestruct(o) if @variable.isObject() and @variable.contains (node) ->
node instanceof Obj and node.hasSplat()
return @compileSplice o if @variable.isSplice() return @compileSplice o if @variable.isSplice()
return @compileConditional o if @context in ['||=', '&&=', '?='] return @compileConditional o if @context in ['||=', '&&=', '?=']
@ -1839,11 +1871,105 @@ exports.Assign = class Assign extends Base
answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val
# Per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration, # Per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration,
# if were destructuring without declaring, the destructuring assignment must be wrapped in parentheses. # if were destructuring without declaring, the destructuring assignment must be wrapped in parentheses.
if o.level > LEVEL_LIST or (isValue and @variable.base instanceof Obj and not @nestedLhs and not @param) if o.level > LEVEL_LIST or (o.level is LEVEL_TOP and isValue and @variable.base instanceof Obj and not @nestedLhs and not @param)
@wrapInParentheses answer @wrapInParentheses answer
else else
answer answer
# Check object destructuring variable for rest elements;
# can be removed once ES proposal hits Stage 4.
compileObjectDestruct: (o) ->
# Per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration,
# if were destructuring without declaring, the destructuring assignment
# must be wrapped in parentheses: `({a, b} = obj)`. Helper function
# `setScopeVar()` declares variables `a` and `b` at the top of the
# current scope.
setScopeVar = (prop) ->
newVar = false
return if prop instanceof Assign and prop.value.base instanceof Obj
if prop instanceof Assign
if prop.value.base instanceof IdentifierLiteral
newVar = prop.value.base.compile o
else
newVar = prop.variable.base.compile o
else
newVar = prop.compile o
o.scope.add(newVar, 'var', true) if newVar
# Returns a safe (cached) reference to the key for a given property
getPropKey = (prop) ->
if prop instanceof Assign
[prop.variable, key] = prop.variable.cache o
key
else
prop
# Returns the name of a given property for use with excludeProps
# Property names are quoted (e.g. `a: b` -> 'a'), and everything else uses the key reference
# (e.g. `'a': b -> 'a'`, `"#{a}": b` -> <cached>`)
getPropName = (prop) ->
key = getPropKey prop
cached = prop instanceof Assign and prop.variable != key
if cached or not key.isAssignable()
key
else
new Literal "'#{key.compile o}'"
# Recursive function for searching and storing rest elements in objects.
# e.g. `{[properties...]} = source`.
traverseRest = (properties, source) =>
restElements = []
restIndex = undefined
for prop, index in properties
setScopeVar prop.unwrap()
if prop instanceof Assign
# prop is `k: expr`, we need to check `expr` for nested splats
if prop.value.isObject?()
# prop is `k: {...}`
nestedProperties = prop.value.base.properties
else if prop.value instanceof Assign and prop.value.variable.isObject()
# prop is `k: {...} = default`
nestedProperties = prop.value.variable.base.properties
[prop.value.value, nestedSourceDefault] = prop.value.value.cache o
if nestedProperties
nestedSource = new Value source.base, source.properties.concat [new Access getPropKey prop]
nestedSource = new Value new Op '?', nestedSource, nestedSourceDefault if nestedSourceDefault
restElements = restElements.concat traverseRest nestedProperties, nestedSource
else if prop instanceof Splat
prop.error "multiple rest elements are disallowed in object destructuring" if restIndex?
restIndex = index
restElements.push {
name: prop.name.unwrapAll()
source
excludeProps: new Arr (getPropName p for p in properties when p isnt prop)
}
if restIndex?
# Remove rest element from the properties after iteration
properties.splice restIndex, 1
restElements
# Cache the value for reuse with rest elements
[@value, valueRef] = @value.cache o
# Find all rest elements.
restElements = traverseRest @variable.base.properties, valueRef
result = new Block [@]
for restElement in restElements
value = new Call new Value(new Literal utility 'objectWithoutKeys', o), [restElement.source, restElement.excludeProps]
result.push new Assign restElement.name, value
fragments = result.compileToFragments o
if o.level is LEVEL_TOP
# Remove leading tab and trailing semicolon
fragments.shift()
fragments.pop()
fragments
# Brief implementation of recursive pattern matching, when assigning array or # Brief implementation of recursive pattern matching, when assigning array or
# object literals to a value. Peeks at their properties to assign inner names. # object literals to a value. Peeks at their properties to assign inner names.
compileDestructuring: (o) -> compileDestructuring: (o) ->
@ -1907,7 +2033,7 @@ exports.Assign = class Assign extends Base
# At this point, there are several things to destructure. So the `fn()` in # At this point, there are several things to destructure. So the `fn()` in
# `{a, b} = fn()` must be cached, for example. Make vvar into a simple # `{a, b} = fn()` must be cached, for example. Make vvar into a simple
# variable if it isn't already. # variable if it isnt already.
if value.unwrap() not instanceof IdentifierLiteral or @variable.assigns(vvarText) if value.unwrap() not instanceof IdentifierLiteral or @variable.assigns(vvarText)
ref = o.scope.freeVariable 'ref' ref = o.scope.freeVariable 'ref'
assigns.push [@makeCode(ref + ' = '), vvar...] assigns.push [@makeCode(ref + ' = '), vvar...]
@ -2098,7 +2224,6 @@ exports.Code = class Code extends Base
@eachParamName (name, node, param) -> @eachParamName (name, node, param) ->
node.error "multiple parameters named '#{name}'" if name in paramNames node.error "multiple parameters named '#{name}'" if name in paramNames
paramNames.push name paramNames.push name
if node.this if node.this
name = node.properties[0].name.value name = node.properties[0].name.value
name = "_#{name}" if name in JS_FORBIDDEN name = "_#{name}" if name in JS_FORBIDDEN
@ -2123,7 +2248,6 @@ exports.Code = class Code extends Base
param.error 'only one splat or expansion parameter is allowed per function definition' param.error 'only one splat or expansion parameter is allowed per function definition'
else if param instanceof Expansion and @params.length is 1 else if param instanceof Expansion and @params.length is 1
param.error 'an expansion parameter cannot be the only parameter in a function definition' param.error 'an expansion parameter cannot be the only parameter in a function definition'
haveSplatParam = yes haveSplatParam = yes
if param.splat if param.splat
if param.name instanceof Arr if param.name instanceof Arr
@ -2132,12 +2256,12 @@ exports.Code = class Code extends Base
# function parameter list, and if so, how? # function parameter list, and if so, how?
splatParamName = o.scope.freeVariable 'arg' splatParamName = o.scope.freeVariable 'arg'
params.push ref = new Value new IdentifierLiteral splatParamName params.push ref = new Value new IdentifierLiteral splatParamName
exprs.push new Assign new Value(param.name), ref, null, param: yes exprs.push new Assign new Value(param.name), ref
else else
params.push ref = param.asReference o params.push ref = param.asReference o
splatParamName = fragmentsToText ref.compileNode o splatParamName = fragmentsToText ref.compileNode o
if param.shouldCache() if param.shouldCache()
exprs.push new Assign new Value(param.name), ref, null, param: yes exprs.push new Assign new Value(param.name), ref
else # `param` is an Expansion else # `param` is an Expansion
splatParamName = o.scope.freeVariable 'args' splatParamName = o.scope.freeVariable 'args'
params.push new Value new IdentifierLiteral splatParamName params.push new Value new IdentifierLiteral splatParamName
@ -2157,10 +2281,10 @@ exports.Code = class Code extends Base
# `(arg) => { var a = arg.a; }`, with a default value if it has one. # `(arg) => { var a = arg.a; }`, with a default value if it has one.
if param.value? if param.value?
condition = new Op '===', param, new UndefinedLiteral condition = new Op '===', param, new UndefinedLiteral
ifTrue = new Assign new Value(param.name), param.value, null, param: yes ifTrue = new Assign new Value(param.name), param.value
exprs.push new If condition, ifTrue exprs.push new If condition, ifTrue
else else
exprs.push new Assign new Value(param.name), param.asReference(o), null, param: yes exprs.push new Assign new Value(param.name), param.asReference(o)
# If this parameter comes before the splat or expansion, it will go # If this parameter comes before the splat or expansion, it will go
# in the function definition parameter list. # in the function definition parameter list.
@ -2182,6 +2306,16 @@ exports.Code = class Code extends Base
param.name.lhs = yes param.name.lhs = yes
param.name.eachName (prop) -> param.name.eachName (prop) ->
o.scope.parameter prop.value o.scope.parameter prop.value
# Compile `foo({a, b...}) ->` to `foo(arg) -> {a, b...} = arg`.
# Can be removed once ES proposal hits Stage 4.
if param.name instanceof Obj and param.name.hasSplat()
splatParamName = o.scope.freeVariable 'arg'
o.scope.parameter splatParamName
ref = new Value new IdentifierLiteral splatParamName
exprs.push new Assign new Value(param.name), ref
# Compile `foo({a, b...} = {}) ->` to `foo(arg = {}) -> {a, b...} = arg`.
if param.value? and not param.assignedInBody
ref = new Assign ref, param.value, null, param: yes
else else
o.scope.parameter fragmentsToText (if param.value? then param else ref).compileToFragments o o.scope.parameter fragmentsToText (if param.value? then param else ref).compileToFragments o
params.push ref params.push ref
@ -2194,7 +2328,8 @@ exports.Code = class Code extends Base
condition = new Op '===', param, new UndefinedLiteral condition = new Op '===', param, new UndefinedLiteral
ifTrue = new Assign new Value(param.name), param.value ifTrue = new Assign new Value(param.name), param.value
exprs.push new If condition, ifTrue exprs.push new If condition, ifTrue
# Add this parameter to the scope, since it wouldnt have been added yet since it was skipped earlier. # Add this parameter to the scope, since it wouldnt have been added
# yet since it was skipped earlier.
o.scope.add param.name.value, 'var', yes if param.name?.value? o.scope.add param.name.value, 'var', yes if param.name?.value?
# If there were parameters after the splat or expansion parameter, those # If there were parameters after the splat or expansion parameter, those
@ -3181,6 +3316,13 @@ exports.If = class If extends Base
UTILITIES = UTILITIES =
modulo: -> 'function(a, b) { return (+a % (b = +b) + b) % b; }' modulo: -> 'function(a, b) { return (+a % (b = +b) + b) % b; }'
objectWithoutKeys: -> "
function(o, ks) {
var res = {};
for (var k in o) ([].indexOf.call(ks, k) < 0 && {}.hasOwnProperty.call(o, k)) && (res[k] = o[k]);
return res;
}
"
boundMethodCheck: -> " boundMethodCheck: -> "
function(instance, Constructor) { function(instance, Constructor) {
if (!(instance instanceof Constructor)) { if (!(instance instanceof Constructor)) {

View File

@ -1,4 +1,5 @@
return unless require? return unless require?
{buildCSOptionParser} = require '../lib/coffeescript/command' {buildCSOptionParser} = require '../lib/coffeescript/command'
optionParser = buildCSOptionParser() optionParser = buildCSOptionParser()

View File

@ -235,6 +235,154 @@ test "destructuring assignment against an expression", ->
eq a, y eq a, y
eq b, z eq b, z
test "destructuring assignment with objects and splats: ES2015", ->
obj = {a: 1, b: 2, c: 3, d: 4, e: 5}
throws (-> CoffeeScript.compile "{a, r..., s...} = x"), null, "multiple rest elements are disallowed"
throws (-> CoffeeScript.compile "{a, r..., s..., b} = x"), null, "multiple rest elements are disallowed"
prop = "b"
{a, b, r...} = obj
eq a, 1
eq b, 2
eq r.e, obj.e
eq r.a, undefined
{d, c: x, r...} = obj
eq x, 3
eq d, 4
eq r.c, undefined
eq r.b, 2
{a, 'b': z, g = 9, r...} = obj
eq g, 9
eq z, 2
eq r.b, undefined
test "destructuring assignment with splats and default values", ->
obj = {}
c = {b: 1}
{ a: {b} = c, d...} = obj
eq b, 1
deepEqual d, {}
test "destructuring assignment with splat with default value", ->
obj = {}
c = {val: 1}
{ a: {b...} = c } = obj
deepEqual b, val: 1
test "destructuring assignment with multiple splats in different objects", ->
obj = { a: {val: 1}, b: {val: 2} }
{ a: {a...}, b: {b...} } = obj
deepEqual a, val: 1
deepEqual b, val: 2
test "destructuring assignment with dynamic keys and splats", ->
i = 0
foo = -> ++i
obj = {1: 'a', 2: 'b'}
{ "#{foo()}": a, b... } = obj
eq a, 'a'
eq i, 1
deepEqual b, 2: 'b'
# Tests from https://babeljs.io/docs/plugins/transform-object-rest-spread/.
test "destructuring assignment with objects and splats: Babel tests", ->
# What Babel calls rest properties:
{ x, y, z... } = { x: 1, y: 2, a: 3, b: 4 }
eq x, 1
eq y, 2
deepEqual z, { a: 3, b: 4 }
# What Babel calls spread properties:
n = { x, y, z... }
deepEqual n, { x: 1, y: 2, a: 3, b: 4 }
test "deep destructuring assignment with objects: ES2015", ->
a1={}; b1={}; c1={}; d1={}
obj = {
a: a1
b: {
'c': {
d: {
b1
e: c1
f: d1
}
}
}
b2: {b1, c1}
}
{a: w, b: {c: {d: {b1: bb, r1...}}}, r2...} = obj
eq r1.e, c1
eq r2.b, undefined
eq bb, b1
eq r2.b2, obj.b2
test "deep destructuring assignment with defaults: ES2015", ->
obj =
b: { c: 1, baz: 'qux' }
foo: 'bar'
j =
f: 'world'
i =
some: 'prop'
{
a...
b: { c, d... }
e: {
f: hello
g: { h... } = i
} = j
} = obj
deepEqual a, foo: 'bar'
eq c, 1
deepEqual d, baz: 'qux'
eq hello, 'world'
deepEqual h, some: 'prop'
test "object spread properties: ES2015", ->
obj = {a: 1, b: 2, c: 3, d: 4, e: 5}
obj2 = {obj..., c:9}
eq obj2.c, 9
eq obj.a, obj2.a
obj2 = {obj..., a: 8, c: 9, obj...}
eq obj2.c, 3
eq obj.a, obj2.a
obj3 = {obj..., b: 7, g: {obj2..., c: 1}}
eq obj3.g.c, 1
eq obj3.b, 7
deepEqual obj3.g, {obj..., c: 1}
(({a, b, r...}) ->
eq 1, a
deepEqual r, {c: 3, d: 44, e: 55}
) {obj2..., d: 44, e: 55}
obj = {a: 1, b: 2, c: {d: 3, e: 4, f: {g: 5}}}
obj4 = {a: 10, obj.c...}
eq obj4.a, 10
eq obj4.d, 3
eq obj4.f.g, 5
deepEqual obj4.f, obj.c.f
obj5 = {obj..., ((k) -> {b: k})(99)...}
eq obj5.b, 99
deepEqual obj5.c, obj.c
fn = -> {c: {d: 33, e: 44, f: {g: 55}}}
obj6 = {obj..., fn()...}
eq obj6.c.d, 33
deepEqual obj6.c, {d: 33, e: 44, f: {g: 55}}
obj7 = {obj..., fn()..., {c: {d: 55, e: 66, f: {77}}}...}
eq obj7.c.d, 55
deepEqual obj6.c, {d: 33, e: 44, f: {g: 55}}
test "bracket insertion when necessary", -> test "bracket insertion when necessary", ->
[a] = [0] ? [1] [a] = [0] ? [1]
eq a, 0 eq a, 0

View File

@ -804,26 +804,27 @@ test "unexpected object keys", ->
[[]]: 1 [[]]: 1
^ ^
''' '''
assertErrorFormat ''' assertErrorFormat '''
{(a + "b")} {(a + "b")}
''', ''' ''', '''
[stdin]:1:2: error: unexpected ( [stdin]:1:11: error: unexpected }
{(a + "b")} {(a + "b")}
^ ^
''' '''
assertErrorFormat ''' assertErrorFormat '''
{(a + "b"): 1} {(a + "b"): 1}
''', ''' ''', '''
[stdin]:1:2: error: unexpected ( [stdin]:1:11: error: unexpected :
{(a + "b"): 1} {(a + "b"): 1}
^ ^
''' '''
assertErrorFormat ''' assertErrorFormat '''
(a + "b"): 1 (a + "b"): 1
''', ''' ''', '''
[stdin]:1:1: error: unexpected ( [stdin]:1:10: error: unexpected :
(a + "b"): 1 (a + "b"): 1
^ ^
''' '''
assertErrorFormat ''' assertErrorFormat '''
a: 1, [[]]: 2 a: 1, [[]]: 2

View File

@ -183,6 +183,69 @@ test "destructuring in function definition", ->
url: '/home', async: true, beforeSend: fn, cache: true, method: 'post', data: {} url: '/home', async: true, beforeSend: fn, cache: true, method: 'post', data: {}
} }
test "rest element destructuring in function definition", ->
obj = {a: 1, b: 2, c: 3, d: 4, e: 5}
(({a, b, r...}) ->
eq 1, a
eq 2, b,
deepEqual r, {c: 3, d: 4, e: 5}
) obj
(({a: p, b, r...}, q) ->
eq p, 1
eq q, 9
deepEqual r, {c: 3, d: 4, e: 5}
) {a:1, b:2, c:3, d:4, e:5}, 9
a1={}; b1={}; c1={}; d1={}
obj1 = {
a: a1
b: {
'c': {
d: {
b1
e: c1
f: d1
}
}
}
b2: {b1, c1}
}
(({a: w, b: {c: {d: {b1: bb, r1...}}}, r2...}) ->
eq a1, w
eq bb, b1
eq r2.b, undefined
deepEqual r1, {e: c1, f: d1}
deepEqual r2.b2, {b1, c1}
) obj1
b = 3
f = ({a, b...}) ->
f {}
eq 3, b
(({a, r...} = {}) ->
eq a, undefined
deepEqual r, {}
)()
(({a, r...} = {}) ->
eq a, 1
deepEqual r, {b: 2, c: 3}
) {a: 1, b: 2, c: 3}
f = ({a, r...} = {}) -> [a, r]
deepEqual [undefined, {}], f()
deepEqual [1, {b: 2}], f {a: 1, b: 2}
deepEqual [1, {}], f {a: 1}
f = ({a, r...} = {a: 1, b: 2}) -> [a, r]
deepEqual [1, {b:2}], f()
deepEqual [2, {}], f {a:2}
deepEqual [3, {c:5}], f {a:3, c:5}
test "#4005: `([a = {}]..., b) ->` weirdness", -> test "#4005: `([a = {}]..., b) ->` weirdness", ->
fn = ([a = {}]..., b) -> [a, b] fn = ([a = {}]..., b) -> [a, b]
deepEqual fn(5), [{}, 5] deepEqual fn(5), [{}, 5]