diff --git a/src/ng/parse.js b/src/ng/parse.js index 8f5922db5705..53ba9039c41e 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -591,13 +591,28 @@ AST.prototype = { property = {type: AST.Property, kind: 'init'}; if (this.peek().constant) { property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); } else if (this.peek().identifier) { property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); } else { this.throwError("invalid key", this.peek()); } - this.consume(':'); - property.value = this.expression(); properties.push(property); } while (this.expect(',')); } @@ -766,7 +781,7 @@ function findConstantAndWatchExpressions(ast, $filter) { argsToWatch = []; forEach(ast.properties, function(property) { findConstantAndWatchExpressions(property.value, $filter); - allConstants = allConstants && property.value.constant; + allConstants = allConstants && property.value.constant && !property.computed; if (!property.value.constant) { argsToWatch.push.apply(argsToWatch, property.value.toWatch); } @@ -938,7 +953,7 @@ ASTCompiler.prototype = { }, recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var left, right, self = this, args, expression; + var left, right, self = this, args, expression, computed; recursionFn = recursionFn || noop; if (!skipWatchIdCheck && isDefined(ast.watchId)) { intoId = intoId || this.nextId(); @@ -1135,16 +1150,40 @@ ASTCompiler.prototype = { break; case AST.ObjectExpression: args = []; + computed = false; forEach(ast.properties, function(property) { - self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { - args.push(self.escape( - property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + - ':' + expr); - }); + if (property.computed) { + computed = true; + } }); - expression = '{' + args.join(',') + '}'; - this.assign(intoId, expression); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } recursionFn(intoId || expression); break; case AST.ThisExpression: @@ -1471,16 +1510,28 @@ ASTInterpreter.prototype = { case AST.ObjectExpression: args = []; forEach(ast.properties, function(property) { - args.push({key: property.key.type === AST.Identifier ? - property.key.name : - ('' + property.key.value), - value: self.recurse(property.value) - }); + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } }); return function(scope, locals, assign, inputs) { var value = {}; for (var i = 0; i < args.length; ++i) { - value[args[i].key] = args[i].value(scope, locals, assign, inputs); + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } } return context ? {value: value} : value; }; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index eea0b361c57c..b6f8d26f94f8 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1250,6 +1250,7 @@ describe('parser', function() { type: 'Property', kind: 'init', key: { type: 'Identifier', name: 'foo' }, + computed: false, value: { type: 'Identifier', name: 'bar' } } ] @@ -1271,6 +1272,7 @@ describe('parser', function() { type: 'Property', kind: 'init', key: { type: 'Identifier', name: 'foo' }, + computed: false, value: { type: 'Identifier', name: 'bar' } } ] @@ -1292,18 +1294,21 @@ describe('parser', function() { type: 'Property', kind: 'init', key: { type: 'Identifier', name: 'foo' }, + computed: false, value: { type: 'Identifier', name: 'bar' } }, { type: 'Property', kind: 'init', key: { type: 'Literal', value: 'man' }, + computed: false, value: { type: 'Literal', value: 'shell' } }, { type: 'Property', kind: 'init', key: { type: 'Literal', value: 42 }, + computed: false, value: { type: 'Literal', value: 23 } } ] @@ -1325,18 +1330,21 @@ describe('parser', function() { type: 'Property', kind: 'init', key: { type: 'Identifier', name: 'foo' }, + computed: false, value: { type: 'Identifier', name: 'bar' } }, { type: 'Property', kind: 'init', key: { type: 'Literal', value: 'man' }, + computed: false, value: { type: 'Literal', value: 'shell' } }, { type: 'Property', kind: 'init', key: { type: 'Literal', value: 42 }, + computed: false, value: { type: 'Literal', value: 23 } } ] @@ -1347,6 +1355,97 @@ describe('parser', function() { ); }); + it('should understand ES6 object initializer', function() { + // Shorthand properties definitions. + expect(createAst('{x, y, z}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'x' }, + computed: false, + value: { type: 'Identifier', name: 'x' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'y' }, + computed: false, + value: { type: 'Identifier', name: 'y' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'z' }, + computed: false, + value: { type: 'Identifier', name: 'z' } + } + ] + } + } + ] + } + ); + expect(function() { createAst('{"foo"}'); }).toThrow(); + + // Computed properties + expect(createAst('{[x]: x}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'x' }, + computed: true, + value: { type: 'Identifier', name: 'x' } + } + ] + } + } + ] + } + ); + expect(createAst('{[x + 1]: x}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { + type: 'BinaryExpression', + operator: '+', + left: { type: 'Identifier', name: 'x' }, + right: { type: 'Literal', value: 1 } + }, + computed: true, + value: { type: 'Identifier', name: 'x' } + } + ] + } + } + ] + } + ); + }); it('should understand multiple expressions', function() { expect(createAst('foo = bar; man = shell')).toEqual( @@ -1626,6 +1725,7 @@ describe('parser', function() { type: 'Property', kind: 'init', key: { type: 'Identifier', name: 'foo' }, + computed: false, value: { type: 'AssignmentExpression', left: { type: 'Identifier', name: 'bar' }, @@ -2091,11 +2191,18 @@ describe('parser', function() { expect(scope.$eval("{false:1}")).toEqual({false:1}); expect(scope.$eval("{'false':1}")).toEqual({false:1}); expect(scope.$eval("{'':1,}")).toEqual({"":1}); + + // ES6 object initializers. + expect(scope.$eval('{x, y}', {x: 'foo', y: 'bar'})).toEqual({x: 'foo', y: 'bar'}); + expect(scope.$eval('{[x]: x}', {x: 'foo'})).toEqual({foo: 'foo'}); + expect(scope.$eval('{[x + "z"]: x}', {x: 'foo'})).toEqual({fooz: 'foo'}); + expect(scope.$eval('{x, 1: x, [x = x + 1]: x, 3: x + 1, [x = x + 2]: x, 5: x + 1}', {x: 1})) + .toEqual({x: 1, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5}); }); it('should throw syntax error exception for non constant/identifier JSON keys', function() { expect(function() { scope.$eval("{[:0}"); }).toThrowMinErr("$parse", "syntax", - "Syntax Error: Token '[' invalid key at column 2 of the expression [{[:0}] starting at [[:0}]"); + "Syntax Error: Token ':' not a primary expression at column 3 of the expression [{[:0}] starting at [:0}]"); expect(function() { scope.$eval("{{:0}"); }).toThrowMinErr("$parse", "syntax", "Syntax Error: Token '{' invalid key at column 2 of the expression [{{:0}] starting at [{:0}]"); expect(function() { scope.$eval("{?:0}"); }).toThrowMinErr("$parse", "syntax", @@ -3654,6 +3761,7 @@ describe('parser', function() { expect($parse('"foo" + "bar"').constant).toBe(true); expect($parse('5 != null').constant).toBe(true); expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true); + expect($parse('{[standard]: 4/3, wide: 16/9}').constant).toBe(false); })); it('should not mark any expression involving variables or function calls as constant', inject(function($parse) {