Implement ES2015-like destructuring defaults

This let's you do things like:

    fullName = ({first = 'John', last = 'Doe'}) -> "#{first} #{last}"

Note: CoffeeScrits treats `undefined` and `null` the same, and that's true in
the case of destructuring defaults as well, as opposed to ES2015 which only uses
the default value if the target is `undefined`. A similar ES2015 difference
already exists for function parameter defaults. It is important for CoffeeScript
to be consistent with itself.

    fullName2 = (first = 'John', last = 'Doe') -> "#{first} #{last}"
    assert fullName('Bob', null) is fullName2(first: 'Bob', last: null)

Fixes #1558, #3288 and #4005.
This commit is contained in:
Simon Lydell 2015-08-22 21:39:26 +02:00
parent 66716cd730
commit 6d9553a016
9 changed files with 396 additions and 183 deletions

View File

@ -108,9 +108,18 @@
return new Assign(LOC(1)(new Value($1)), $3, 'object');
}), o('ObjAssignable : INDENT Expression OUTDENT', function() {
return new Assign(LOC(1)(new Value($1)), $4, 'object');
}), o('SimpleObjAssignable = Expression', function() {
return new Assign(LOC(1)(new Value($1)), $3, null, {
operatorToken: LOC(2)(new Literal($2))
});
}), o('SimpleObjAssignable = INDENT Expression OUTDENT', function() {
return new Assign(LOC(1)(new Value($1)), $4, null, {
operatorToken: LOC(2)(new Literal($2))
});
}), o('Comment')
],
ObjAssignable: [o('Identifier'), o('AlphaNumeric'), o('ThisProperty')],
SimpleObjAssignable: [o('Identifier'), o('ThisProperty')],
ObjAssignable: [o('SimpleObjAssignable'), o('AlphaNumeric')],
Return: [
o('RETURN Expression', function() {
return new Return($2);

View File

@ -1310,8 +1310,13 @@
if (hasDynamic && i < dynamicIndex) {
indent += TAB;
}
if (prop instanceof Assign && prop.variable instanceof Value && prop.variable.hasProperties()) {
prop.variable.error('invalid object key');
if (prop instanceof Assign) {
if (prop.context !== 'object') {
prop.operatorToken.error("unexpected " + prop.operatorToken.value);
}
if (prop.variable instanceof Value && prop.variable.hasProperties()) {
prop.variable.error('invalid object key');
}
}
if (prop instanceof Value && prop["this"]) {
prop = new Assign(prop.properties[0].name, prop, 'object');
@ -1631,8 +1636,10 @@
this.variable = variable1;
this.value = value1;
this.context = context;
this.param = options && options.param;
this.subpattern = options && options.subpattern;
if (options == null) {
options = {};
}
this.param = options.param, this.subpattern = options.subpattern, this.operatorToken = options.operatorToken;
forbidden = (ref3 = (name = this.variable.unwrapAll().value), indexOf.call(STRICT_PROSCRIBED, ref3) >= 0);
if (forbidden && this.context !== 'object') {
this.variable.error("variable name may not be \"" + name + "\"");
@ -1713,7 +1720,7 @@
};
Assign.prototype.compilePatternMatch = function(o) {
var acc, assigns, code, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, ref7, ref8, rest, top, val, value, vvar, vvarText;
var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, ref7, rest, top, val, value, vvar, vvarText;
top = o.level === LEVEL_TOP;
value = this.value;
objects = this.variable.base.objects;
@ -1731,17 +1738,29 @@
}
isObject = this.variable.isObject();
if (top && olen === 1 && !(obj instanceof Splat)) {
if (obj instanceof Assign) {
defaultValue = null;
if (obj instanceof Assign && obj.context === 'object') {
ref3 = obj, (ref4 = ref3.variable, idx = ref4.base), obj = ref3.value;
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
} else {
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
idx = isObject ? obj["this"] ? obj.properties[0].name : obj : new Literal(0);
}
acc = IDENTIFIER.test(idx.unwrap().value || 0);
acc = IDENTIFIER.test(idx.unwrap().value);
value = new Value(value);
value.properties.push(new (acc ? Access : Index)(idx));
if (ref5 = obj.unwrap().value, indexOf.call(RESERVED, ref5) >= 0) {
obj.error("assignment to a reserved word: " + (obj.compile(o)));
}
if (defaultValue) {
value = new Op('?', value, defaultValue);
}
return new Assign(obj, value, null, {
param: this.param
}).compileToFragments(o, LEVEL_TOP);
@ -1758,17 +1777,6 @@
for (i = j = 0, len1 = objects.length; j < len1; i = ++j) {
obj = objects[i];
idx = i;
if (isObject) {
if (obj instanceof Assign) {
ref6 = obj, (ref7 = ref6.variable, idx = ref7.base), obj = ref6.value;
} else {
if (obj.base instanceof Parens) {
ref8 = new Value(obj.unwrapAll()).cacheReference(o), obj = ref8[0], idx = ref8[1];
} else {
idx = obj["this"] ? obj.properties[0].name : obj;
}
}
}
if (!expandedIdx && obj instanceof Splat) {
name = obj.name.unwrap().value;
obj = obj.unwrap();
@ -1798,17 +1806,29 @@
}
continue;
} else {
name = obj.unwrap().value;
if (obj instanceof Splat || obj instanceof Expansion) {
obj.error("multiple splats/expansions are disallowed in an assignment");
}
if (typeof idx === 'number') {
idx = new Literal(expandedIdx || idx);
acc = false;
defaultValue = null;
if (obj instanceof Assign && obj.context === 'object') {
ref6 = obj, (ref7 = ref6.variable, idx = ref7.base), obj = ref6.value;
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
} else {
acc = isObject && IDENTIFIER.test(idx.unwrap().value || 0);
if (obj instanceof Assign) {
defaultValue = obj.value;
obj = obj.variable;
}
idx = isObject ? obj["this"] ? obj.properties[0].name : obj : new Literal(expandedIdx || idx);
}
name = obj.unwrap().value;
acc = IDENTIFIER.test(idx.unwrap().value);
val = new Value(new Literal(vvarText), [new (acc ? Access : Index)(idx)]);
if (defaultValue) {
val = new Op('?', val, defaultValue);
}
}
if ((name != null) && indexOf.call(RESERVED, name) >= 0) {
obj.error("assignment to a reserved word: " + (obj.compile(o)));
@ -2129,6 +2149,9 @@
ref3 = name.objects;
for (j = 0, len1 = ref3.length; j < len1; j++) {
obj = ref3[j];
if (obj instanceof Assign && (obj.context == null)) {
obj = obj.variable;
}
if (obj instanceof Assign) {
this.eachName(iterator, obj.value.unwrap());
} else if (obj instanceof Splat) {

File diff suppressed because one or more lines are too long

View File

@ -168,18 +168,27 @@ grammar =
# the ordinary **Assign** is that these allow numbers and strings as keys.
AssignObj: [
o 'ObjAssignable', -> new Value $1
o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value($1)), $3, 'object'
o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value $1), $3, 'object'
o 'ObjAssignable :
INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value($1)), $4, 'object'
INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value $1), $4, 'object'
o 'SimpleObjAssignable = Expression', -> new Assign LOC(1)(new Value $1), $3, null,
operatorToken: LOC(2)(new Literal $2)
o 'SimpleObjAssignable =
INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value $1), $4, null,
operatorToken: LOC(2)(new Literal $2)
o 'Comment'
]
ObjAssignable: [
SimpleObjAssignable: [
o 'Identifier'
o 'AlphaNumeric'
o 'ThisProperty'
]
ObjAssignable: [
o 'SimpleObjAssignable'
o 'AlphaNumeric'
]
# A return statement from a function body.
Return: [
o 'RETURN Expression', -> new Return $2

View File

@ -951,8 +951,11 @@ exports.Obj = class Obj extends Base
',\n'
indent = if prop instanceof Comment then '' else idt
indent += TAB if hasDynamic and i < dynamicIndex
if prop instanceof Assign and prop.variable instanceof Value and prop.variable.hasProperties()
prop.variable.error 'invalid object key'
if prop instanceof Assign
if prop.context isnt 'object'
prop.operatorToken.error "unexpected #{prop.operatorToken.value}"
if prop.variable instanceof Value and prop.variable.hasProperties()
prop.variable.error 'invalid object key'
if prop instanceof Value and prop.this
prop = new Assign prop.properties[0].name, prop, 'object'
if prop not instanceof Comment
@ -1168,9 +1171,8 @@ exports.Class = class Class extends Base
# The **Assign** is used to assign a local variable to value, or to set the
# property of an object -- including within object literals.
exports.Assign = class Assign extends Base
constructor: (@variable, @value, @context, options) ->
@param = options and options.param
@subpattern = options and options.subpattern
constructor: (@variable, @value, @context, options = {}) ->
{@param, @subpattern, @operatorToken} = options
forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED
if forbidden and @context isnt 'object'
@variable.error "variable name may not be \"#{name}\""
@ -1225,8 +1227,6 @@ exports.Assign = class Assign extends Base
# Brief implementation of recursive pattern matching, when assigning array or
# object literals to a value. Peeks at their properties to assign inner names.
# See the [ECMAScript Harmony Wiki](http://wiki.ecmascript.org/doku.php?id=harmony:destructuring)
# for details.
compilePatternMatch: (o) ->
top = o.level is LEVEL_TOP
{value} = this
@ -1239,19 +1239,31 @@ exports.Assign = class Assign extends Base
obj.error 'Destructuring assignment has no target'
isObject = @variable.isObject()
if top and olen is 1 and obj not instanceof Splat
# Unroll simplest cases: `{v} = x` -> `v = x.v`
if obj instanceof Assign
# Pick the property straight off the value when theres just one to pick
# (no need to cache the value into a variable).
defaultValue = null
if obj instanceof Assign and obj.context is 'object'
# A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
else
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
idx = if isObject
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.this then obj.properties[0].name else obj
else
# A regular array pattern-match.
new Literal 0
acc = IDENTIFIER.test idx.unwrap().value or 0
acc = IDENTIFIER.test idx.unwrap().value
value = new Value value
value.properties.push new (if acc then Access else Index) idx
if obj.unwrap().value in RESERVED
obj.error "assignment to a reserved word: #{obj.compile o}"
value = new Op '?', value, defaultValue if defaultValue
return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP
vvar = value.compileToFragments o, LEVEL_LIST
vvarText = fragmentsToText vvar
@ -1263,18 +1275,7 @@ exports.Assign = class Assign extends Base
vvar = [@makeCode ref]
vvarText = ref
for obj, i in objects
# A regular array pattern-match.
idx = i
if isObject
if obj instanceof Assign
# A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj
else
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.base instanceof Parens
[obj, idx] = new Value(obj.unwrapAll()).cacheReference o
else
idx = if obj.this then obj.properties[0].name else obj
if not expandedIdx and obj instanceof Splat
name = obj.name.unwrap().value
obj = obj.unwrap()
@ -1297,15 +1298,29 @@ exports.Assign = class Assign extends Base
assigns.push val.compileToFragments o, LEVEL_LIST
continue
else
name = obj.unwrap().value
if obj instanceof Splat or obj instanceof Expansion
obj.error "multiple splats/expansions are disallowed in an assignment"
if typeof idx is 'number'
idx = new Literal expandedIdx or idx
acc = no
defaultValue = null
if obj instanceof Assign and obj.context is 'object'
# A regular object pattern-match.
{variable: {base: idx}, value: obj} = obj
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
else
acc = isObject and IDENTIFIER.test idx.unwrap().value or 0
if obj instanceof Assign
defaultValue = obj.value
obj = obj.variable
idx = if isObject
# A shorthand `{a, b, @c} = val` pattern-match.
if obj.this then obj.properties[0].name else obj
else
# A regular array pattern-match.
new Literal expandedIdx or idx
name = obj.unwrap().value
acc = IDENTIFIER.test idx.unwrap().value
val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx]
val = new Op '?', val, defaultValue if defaultValue
if name? and name in RESERVED
obj.error "assignment to a reserved word: #{obj.compile o}"
assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST
@ -1503,6 +1518,9 @@ exports.Param = class Param extends Base
# * at-params `@foo`
return atParam name if name instanceof Value
for obj in name.objects
# * destructured parameter with default value
if obj instanceof Assign and not obj.context?
obj = obj.variable
# * assignments within destructured parameters `{foo:bar}`
if obj instanceof Assign
@eachName iterator, obj.value.unwrap()

View File

@ -299,6 +299,99 @@ test "destructuring with dynamic keys", ->
eq 3, c
throws -> CoffeeScript.compile '{"#{a}"} = b'
test "simple array destructuring defaults", ->
[a = 1] = []
eq 1, a
[a = 2] = [undefined]
eq 2, a
[a = 3] = [null]
eq 3, a
[a = 4] = [0]
eq 0, a
arr = [a = 5]
eq 5, a
arrayEq [5], arr
test "simple object destructuring defaults", ->
{b = 1} = {}
eq b, 1
{b = 2} = {b: undefined}
eq b, 2
{b = 3} = {b: null}
eq b, 3
{b = 4} = {b: 0}
eq b, 0
{b: c = 1} = {}
eq c, 1
{b: c = 2} = {b: undefined}
eq c, 2
{b: c = 3} = {b: null}
eq c, 3
{b: c = 4} = {b: 0}
eq c, 0
test "multiple array destructuring defaults", ->
[a = 1, b = 2, c] = [null, 12, 13]
eq a, 1
eq b, 12
eq c, 13
[a, b = 2, c = 3] = [null, 12, 13]
eq a, null
eq b, 12
eq c, 13
[a = 1, b, c = 3] = [11, 12]
eq a, 11
eq b, 12
eq c, 3
test "multiple object destructuring defaults", ->
{a = 1, b: bb = 2, 'c': c = 3, "#{0}": d = 4} = {"#{'b'}": 12}
eq a, 1
eq bb, 12
eq c, 3
eq d, 4
test "array destructuring defaults with splats", ->
[..., a = 9] = []
eq a, 9
[..., b = 9] = [19]
eq b, 19
test "deep destructuring assignment with defaults", ->
[a, [{b = 1, c = 3}] = [c: 2]] = [0]
eq a, 0
eq b, 1
eq c, 2
test "destructuring assignment with context (@) properties and defaults", ->
a={}; b={}; c={}; d={}; e={}
obj =
fn: () ->
local = [a, {b, c: null}, d]
[@a, {b: @b = b, @c = c}, @d, @e = e] = local
eq undefined, obj[key] for key in ['a','b','c','d','e']
obj.fn()
eq a, obj.a
eq b, obj.b
eq c, obj.c
eq d, obj.d
eq e, obj.e
test "destructuring assignment with defaults single evaluation", ->
callCount = 0
fn = -> callCount++
[a = fn()] = []
eq 0, a
eq 1, callCount
[a = fn()] = [10]
eq 10, a
eq 1, callCount
{a = fn(), b: c = fn()} = {a: 20, b: null}
eq 20, a
eq c, 1
eq callCount, 2
# Existential Assignment

View File

@ -776,6 +776,22 @@ test "invalid object keys", ->
@a: 1
^^
'''
assertErrorFormat '''
{a=2}
''', '''
[stdin]:1:3: error: unexpected =
{a=2}
^
'''
test "invalid destructuring default target", ->
assertErrorFormat '''
{'a' = 2} = obj
''', '''
[stdin]:1:6: error: unexpected =
{'a' = 2} = obj
^
'''
test "#4070: lone expansion", ->
assertErrorFormat '''

View File

@ -140,6 +140,33 @@ test "destructuring in function definition", ->
eq 2, c
) {a: [1], c: 2}
context = {}
(([{a: [b, c = 2], @d, e = 4}]...) ->
eq 1, b
eq 2, c
eq @d, 3
eq context.d, 3
eq e, 4
).call context, {a: [1], d: 3}
ajax = (url, {
async = true,
beforeSend = (->),
cache = true,
method = 'get',
data = {}
}) ->
{url, async, beforeSend, cache, method, data}
fn = ->
deepEqual ajax('/home', beforeSend: fn, cache: null, method: 'post'), {
url: '/home', async: true, beforeSend: fn, cache: true, method: 'post', data: {}
}
test "#4005: `([a = {}]..., b) ->` weirdness", ->
fn = ([a = {}]..., b) -> [a, b]
deepEqual fn(5), [{}, 5]
test "default values", ->
nonceA = {}
nonceB = {}

View File

@ -77,12 +77,18 @@ test "duplicate formal parameters are prohibited", ->
strict '(@_,@_)->', 'two @params'
strict '(@case,@case)->', 'two @reserved'
strict '(_,{_})->', 'param, {param}'
strict '(_,{_=true})->', 'param, {param=}'
strict '({_,_})->', '{param, param}'
strict '({_=true,_})->', '{param=, param}'
strict '(_,[_])->', 'param, [param]'
strict '(_,[_=true])->', 'param, [param=]'
strict '([_,_])->', '[param, param]'
strict '([_=true,_])->', '[param=, param]'
strict '(_,[_]=true)->', 'param, [param]='
strict '(_,[_=true]=true)->', 'param, [param=]='
strict '(_,[@_,{_}])->', 'param, [@param, {param}]'
strict '(_,[_,{@_}])->', 'param, [param, {@param}]'
strict '(_,[_,{@_=true}])->', 'param, [param, {@param=}]'
strict '(_,[_,{_}])->', 'param, [param, {param}]'
strict '(_,[_,{__}])->', 'param, [param, {param2}]'
strict '(_,[__,{_}])->', 'param, [param2, {param}]'
@ -95,7 +101,9 @@ test "duplicate formal parameters are prohibited", ->
strictOk '(_,@_ = true)->'
strictOk '(@_,{_})->'
strictOk '({_,@_})->'
strictOk '({_,@_ = true})->'
strictOk '([_,@_])->'
strictOk '([_,@_ = true])->'
strictOk '({},_arg)->'
strictOk '({},{})->'
strictOk '([]...,_arg)->'