Skip to content

Commit b5a0c8d

Browse files
committed
feat($parse): Add support for ES6 object initializers
- Add support for computed property names - Add support for short notation in properties definitions Eg. The expressions `{x}` and `{[x + 'foo']: 1}` are now valid.
1 parent c1eaf34 commit b5a0c8d

File tree

2 files changed

+178
-19
lines changed

2 files changed

+178
-19
lines changed

src/ng/parse.js

+69-18
Original file line numberDiff line numberDiff line change
@@ -591,13 +591,28 @@ AST.prototype = {
591591
property = {type: AST.Property, kind: 'init'};
592592
if (this.peek().constant) {
593593
property.key = this.constant();
594+
property.computed = false;
595+
this.consume(':');
596+
property.value = this.expression();
594597
} else if (this.peek().identifier) {
595598
property.key = this.identifier();
599+
property.computed = false;
600+
if (this.peek(':')) {
601+
this.consume(':');
602+
property.value = this.expression();
603+
} else {
604+
property.value = property.key;
605+
}
606+
} else if (this.peek('[')) {
607+
this.consume('[');
608+
property.key = this.expression();
609+
this.consume(']');
610+
property.computed = true;
611+
this.consume(':');
612+
property.value = this.expression();
596613
} else {
597614
this.throwError("invalid key", this.peek());
598615
}
599-
this.consume(':');
600-
property.value = this.expression();
601616
properties.push(property);
602617
} while (this.expect(','));
603618
}
@@ -766,7 +781,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
766781
argsToWatch = [];
767782
forEach(ast.properties, function(property) {
768783
findConstantAndWatchExpressions(property.value, $filter);
769-
allConstants = allConstants && property.value.constant;
784+
allConstants = allConstants && property.value.constant && !property.computed;
770785
if (!property.value.constant) {
771786
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
772787
}
@@ -938,7 +953,7 @@ ASTCompiler.prototype = {
938953
},
939954

940955
recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
941-
var left, right, self = this, args, expression;
956+
var left, right, self = this, args, expression, computed;
942957
recursionFn = recursionFn || noop;
943958
if (!skipWatchIdCheck && isDefined(ast.watchId)) {
944959
intoId = intoId || this.nextId();
@@ -1135,16 +1150,40 @@ ASTCompiler.prototype = {
11351150
break;
11361151
case AST.ObjectExpression:
11371152
args = [];
1153+
computed = false;
11381154
forEach(ast.properties, function(property) {
1139-
self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) {
1140-
args.push(self.escape(
1141-
property.key.type === AST.Identifier ? property.key.name :
1142-
('' + property.key.value)) +
1143-
':' + expr);
1144-
});
1155+
if (property.computed) {
1156+
computed = true;
1157+
}
11451158
});
1146-
expression = '{' + args.join(',') + '}';
1147-
this.assign(intoId, expression);
1159+
if (computed) {
1160+
intoId = intoId || this.nextId();
1161+
this.assign(intoId, '{}');
1162+
forEach(ast.properties, function(property) {
1163+
if (property.computed) {
1164+
left = self.nextId();
1165+
self.recurse(property.key, left);
1166+
} else {
1167+
left = property.key.type === AST.Identifier ?
1168+
property.key.name :
1169+
('' + property.key.value);
1170+
}
1171+
right = self.nextId();
1172+
self.recurse(property.value, right);
1173+
self.assign(self.member(intoId, left, property.computed), right);
1174+
});
1175+
} else {
1176+
forEach(ast.properties, function(property) {
1177+
self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) {
1178+
args.push(self.escape(
1179+
property.key.type === AST.Identifier ? property.key.name :
1180+
('' + property.key.value)) +
1181+
':' + expr);
1182+
});
1183+
});
1184+
expression = '{' + args.join(',') + '}';
1185+
this.assign(intoId, expression);
1186+
}
11481187
recursionFn(intoId || expression);
11491188
break;
11501189
case AST.ThisExpression:
@@ -1471,16 +1510,28 @@ ASTInterpreter.prototype = {
14711510
case AST.ObjectExpression:
14721511
args = [];
14731512
forEach(ast.properties, function(property) {
1474-
args.push({key: property.key.type === AST.Identifier ?
1475-
property.key.name :
1476-
('' + property.key.value),
1477-
value: self.recurse(property.value)
1478-
});
1513+
if (property.computed) {
1514+
args.push({key: self.recurse(property.key),
1515+
computed: true,
1516+
value: self.recurse(property.value)
1517+
});
1518+
} else {
1519+
args.push({key: property.key.type === AST.Identifier ?
1520+
property.key.name :
1521+
('' + property.key.value),
1522+
computed: false,
1523+
value: self.recurse(property.value)
1524+
});
1525+
}
14791526
});
14801527
return function(scope, locals, assign, inputs) {
14811528
var value = {};
14821529
for (var i = 0; i < args.length; ++i) {
1483-
value[args[i].key] = args[i].value(scope, locals, assign, inputs);
1530+
if (args[i].computed) {
1531+
value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs);
1532+
} else {
1533+
value[args[i].key] = args[i].value(scope, locals, assign, inputs);
1534+
}
14841535
}
14851536
return context ? {value: value} : value;
14861537
};

test/ng/parseSpec.js

+109-1
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,7 @@ describe('parser', function() {
12501250
type: 'Property',
12511251
kind: 'init',
12521252
key: { type: 'Identifier', name: 'foo' },
1253+
computed: false,
12531254
value: { type: 'Identifier', name: 'bar' }
12541255
}
12551256
]
@@ -1271,6 +1272,7 @@ describe('parser', function() {
12711272
type: 'Property',
12721273
kind: 'init',
12731274
key: { type: 'Identifier', name: 'foo' },
1275+
computed: false,
12741276
value: { type: 'Identifier', name: 'bar' }
12751277
}
12761278
]
@@ -1292,18 +1294,21 @@ describe('parser', function() {
12921294
type: 'Property',
12931295
kind: 'init',
12941296
key: { type: 'Identifier', name: 'foo' },
1297+
computed: false,
12951298
value: { type: 'Identifier', name: 'bar' }
12961299
},
12971300
{
12981301
type: 'Property',
12991302
kind: 'init',
13001303
key: { type: 'Literal', value: 'man' },
1304+
computed: false,
13011305
value: { type: 'Literal', value: 'shell' }
13021306
},
13031307
{
13041308
type: 'Property',
13051309
kind: 'init',
13061310
key: { type: 'Literal', value: 42 },
1311+
computed: false,
13071312
value: { type: 'Literal', value: 23 }
13081313
}
13091314
]
@@ -1325,18 +1330,21 @@ describe('parser', function() {
13251330
type: 'Property',
13261331
kind: 'init',
13271332
key: { type: 'Identifier', name: 'foo' },
1333+
computed: false,
13281334
value: { type: 'Identifier', name: 'bar' }
13291335
},
13301336
{
13311337
type: 'Property',
13321338
kind: 'init',
13331339
key: { type: 'Literal', value: 'man' },
1340+
computed: false,
13341341
value: { type: 'Literal', value: 'shell' }
13351342
},
13361343
{
13371344
type: 'Property',
13381345
kind: 'init',
13391346
key: { type: 'Literal', value: 42 },
1347+
computed: false,
13401348
value: { type: 'Literal', value: 23 }
13411349
}
13421350
]
@@ -1347,6 +1355,97 @@ describe('parser', function() {
13471355
);
13481356
});
13491357

1358+
it('should understand ES6 object initializer', function() {
1359+
// Shorthand properties definitions.
1360+
expect(createAst('{x, y, z}')).toEqual(
1361+
{
1362+
type: 'Program',
1363+
body: [
1364+
{
1365+
type: 'ExpressionStatement',
1366+
expression: {
1367+
type: 'ObjectExpression',
1368+
properties: [
1369+
{
1370+
type: 'Property',
1371+
kind: 'init',
1372+
key: { type: 'Identifier', name: 'x' },
1373+
computed: false,
1374+
value: { type: 'Identifier', name: 'x' }
1375+
},
1376+
{
1377+
type: 'Property',
1378+
kind: 'init',
1379+
key: { type: 'Identifier', name: 'y' },
1380+
computed: false,
1381+
value: { type: 'Identifier', name: 'y' }
1382+
},
1383+
{
1384+
type: 'Property',
1385+
kind: 'init',
1386+
key: { type: 'Identifier', name: 'z' },
1387+
computed: false,
1388+
value: { type: 'Identifier', name: 'z' }
1389+
}
1390+
]
1391+
}
1392+
}
1393+
]
1394+
}
1395+
);
1396+
expect(function() { createAst('{"foo"}'); }).toThrow();
1397+
1398+
// Computed properties
1399+
expect(createAst('{[x]: x}')).toEqual(
1400+
{
1401+
type: 'Program',
1402+
body: [
1403+
{
1404+
type: 'ExpressionStatement',
1405+
expression: {
1406+
type: 'ObjectExpression',
1407+
properties: [
1408+
{
1409+
type: 'Property',
1410+
kind: 'init',
1411+
key: { type: 'Identifier', name: 'x' },
1412+
computed: true,
1413+
value: { type: 'Identifier', name: 'x' }
1414+
}
1415+
]
1416+
}
1417+
}
1418+
]
1419+
}
1420+
);
1421+
expect(createAst('{[x + 1]: x}')).toEqual(
1422+
{
1423+
type: 'Program',
1424+
body: [
1425+
{
1426+
type: 'ExpressionStatement',
1427+
expression: {
1428+
type: 'ObjectExpression',
1429+
properties: [
1430+
{
1431+
type: 'Property',
1432+
kind: 'init',
1433+
key: {
1434+
type: 'BinaryExpression',
1435+
operator: '+',
1436+
left: { type: 'Identifier', name: 'x' },
1437+
right: { type: 'Literal', value: 1 }
1438+
},
1439+
computed: true,
1440+
value: { type: 'Identifier', name: 'x' }
1441+
}
1442+
]
1443+
}
1444+
}
1445+
]
1446+
}
1447+
);
1448+
});
13501449

13511450
it('should understand multiple expressions', function() {
13521451
expect(createAst('foo = bar; man = shell')).toEqual(
@@ -1626,6 +1725,7 @@ describe('parser', function() {
16261725
type: 'Property',
16271726
kind: 'init',
16281727
key: { type: 'Identifier', name: 'foo' },
1728+
computed: false,
16291729
value: {
16301730
type: 'AssignmentExpression',
16311731
left: { type: 'Identifier', name: 'bar' },
@@ -2091,11 +2191,18 @@ describe('parser', function() {
20912191
expect(scope.$eval("{false:1}")).toEqual({false:1});
20922192
expect(scope.$eval("{'false':1}")).toEqual({false:1});
20932193
expect(scope.$eval("{'':1,}")).toEqual({"":1});
2194+
2195+
// ES6 object initializers.
2196+
expect(scope.$eval('{x, y}', {x: 'foo', y: 'bar'})).toEqual({x: 'foo', y: 'bar'});
2197+
expect(scope.$eval('{[x]: x}', {x: 'foo'})).toEqual({foo: 'foo'});
2198+
expect(scope.$eval('{[x + "z"]: x}', {x: 'foo'})).toEqual({fooz: 'foo'});
2199+
expect(scope.$eval('{x, 1: x, [x = x + 1]: x, 3: x + 1, [x = x + 2]: x, 5: x + 1}', {x: 1}))
2200+
.toEqual({x: 1, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5});
20942201
});
20952202

20962203
it('should throw syntax error exception for non constant/identifier JSON keys', function() {
20972204
expect(function() { scope.$eval("{[:0}"); }).toThrowMinErr("$parse", "syntax",
2098-
"Syntax Error: Token '[' invalid key at column 2 of the expression [{[:0}] starting at [[:0}]");
2205+
"Syntax Error: Token ':' not a primary expression at column 3 of the expression [{[:0}] starting at [:0}]");
20992206
expect(function() { scope.$eval("{{:0}"); }).toThrowMinErr("$parse", "syntax",
21002207
"Syntax Error: Token '{' invalid key at column 2 of the expression [{{:0}] starting at [{:0}]");
21012208
expect(function() { scope.$eval("{?:0}"); }).toThrowMinErr("$parse", "syntax",
@@ -3654,6 +3761,7 @@ describe('parser', function() {
36543761
expect($parse('"foo" + "bar"').constant).toBe(true);
36553762
expect($parse('5 != null').constant).toBe(true);
36563763
expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true);
3764+
expect($parse('{[standard]: 4/3, wide: 16/9}').constant).toBe(false);
36573765
}));
36583766

36593767
it('should not mark any expression involving variables or function calls as constant', inject(function($parse) {

0 commit comments

Comments
 (0)