diff --git a/src/ng/parse.js b/src/ng/parse.js index 8f8c0f872f94..4376744c8928 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -42,7 +42,7 @@ function ensureSafeObject(obj, fullExpression) { if (obj && obj.constructor === obj) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); - // + // } else if (// isWindow(obj) obj && obj.document && obj.location && obj.alert && obj.setInterval) { throw $parseMinErr('isecwindow', @@ -93,156 +93,193 @@ var OPERATORS = { }; var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; -function lex(text, csp){ - var tokens = [], - token, - index = 0, - json = [], - ch, - lastCh = ':'; // can start regexp - - while (index < text.length) { - ch = text.charAt(index); - if (is('"\'')) { - readString(ch); - } else if (isNumber(ch) || is('.') && isNumber(peek())) { - readNumber(); - } else if (isIdent(ch)) { - readIdent(); - // identifiers can only be if the preceding char was a { or , - if (was('{,') && json[0]=='{' && - (token=tokens[tokens.length-1])) { - token.json = token.text.indexOf('.') == -1; - } - } else if (is('(){}[].,;:?')) { - tokens.push({ - index:index, - text:ch, - json:(was(':[,') && is('{[')) || is('}]:,') - }); - if (is('{[')) json.unshift(ch); - if (is('}]')) json.shift(); - index++; - } else if (isWhitespace(ch)) { - index++; - continue; - } else { - var ch2 = ch + peek(), - ch3 = ch2 + peek(2), - fn = OPERATORS[ch], - fn2 = OPERATORS[ch2], - fn3 = OPERATORS[ch3]; - if (fn3) { - tokens.push({index:index, text:ch3, fn:fn3}); - index += 3; - } else if (fn2) { - tokens.push({index:index, text:ch2, fn:fn2}); - index += 2; - } else if (fn) { - tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')}); - index += 1; + +///////////////////////////////////////// + + +/** + * @constructor + */ +var Lexer = function (csp) { + this.csp = csp; +}; + +Lexer.prototype = { + constructor: Lexer, + + lex: function (text, options) { + this.text = text; + this.options = options || {}; + + this.index = 0; + this.ch; + this.lastCh = ':'; // can start regexp + + this.tokens = []; + + var token; + var json = []; + + while (this.index < this.text.length) { + this.ch = this.text.charAt(this.index); + if (this.is('"\'')) { + this.readString(this.ch); + } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + this.readNumber(); + } else if (this.isIdent(this.ch)) { + this.readIdent(); + // identifiers can only be if the preceding char was a { or , + if (this.was('{,') && json[0] === '{' && + (token = this.tokens[this.tokens.length - 1])) { + token.json = token.text.indexOf('.') === -1; + } + } else if (this.is('(){}[].,;:?')) { + this.tokens.push({ + index: this.index, + text: this.ch, + json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') + }); + if (this.is('{[')) json.unshift(this.ch); + if (this.is('}]')) json.shift(); + this.index++; + } else if (this.isWhitespace(this.ch)) { + this.index++; + continue; } else { - throwError("Unexpected next character ", index, index+1); + var ch2 = this.ch + this.peek(); + var ch3 = ch2 + this.peek(2); + var fn = OPERATORS[this.ch]; + var fn2 = OPERATORS[ch2]; + var fn3 = OPERATORS[ch3]; + if (fn3) { + this.tokens.push({index: this.index, text: ch3, fn: fn3}); + this.index += 3; + } else if (fn2) { + this.tokens.push({index: this.index, text: ch2, fn: fn2}); + this.index += 2; + } else if (fn) { + this.tokens.push({ + index: this.index, + text: this.ch, + fn: fn, + json: (this.was('[,:') && this.is('+-')) + }); + this.index += 1; + } else { + this.throwError('Unexpected next character ', this.index, this.index + 1); + } } + this.lastCh = this.ch; } - lastCh = ch; - } - return tokens; + return this.tokens; + }, - function is(chars) { - return chars.indexOf(ch) != -1; - } + is: function(chars) { + return chars.indexOf(this.ch) !== -1; + }, - function was(chars) { - return chars.indexOf(lastCh) != -1; - } + was: function(chars) { + return chars.indexOf(this.lastCh) !== -1; + }, - function peek(i) { + peek: function(i) { var num = i || 1; - return index + num < text.length ? text.charAt(index + num) : false; - } - function isNumber(ch) { - return '0' <= ch && ch <= '9'; - } - function isWhitespace(ch) { - return ch == ' ' || ch == '\r' || ch == '\t' || - ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0 - } - function isIdent(ch) { - return 'a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' == ch || ch == '$'; - } - function isExpOperator(ch) { - return ch == '-' || ch == '+' || isNumber(ch); - } - - function throwError(error, start, end) { - end = end || index; + return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; + }, + + isNumber: function(ch) { + return ('0' <= ch && ch <= '9'); + }, + + isWhitespace: function(ch) { + return (ch === ' ' || ch === '\r' || ch === '\t' || + ch === '\n' || ch === '\v' || ch === '\u00A0'); // IE treats non-breaking space as \u00A0 + }, + + isIdent: function(ch) { + return ('a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' === ch || ch === '$'); + }, + + isExpOperator: function(ch) { + return (ch === '-' || ch === '+' || this.isNumber(ch)); + }, + + throwError: function(error, start, end) { + end = end || this.index; var colStr = (isDefined(start) ? - "s " + start + "-" + index + " [" + text.substring(start, end) + "]" - : " " + end); - throw $parseMinErr('lexerr', "Lexer Error: {0} at column{1} in expression [{2}].", - error, colStr, text); - } - - function readNumber() { - var number = ""; - var start = index; - while (index < text.length) { - var ch = lowercase(text.charAt(index)); - if (ch == '.' || isNumber(ch)) { + 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' : + ' ' + end); + throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', + error, colStr, this.text); + }, + + readNumber: function() { + var number = ''; + var start = this.index; + while (this.index < this.text.length) { + var ch = lowercase(this.text.charAt(this.index)); + if (ch == '.' || this.isNumber(ch)) { number += ch; } else { - var peekCh = peek(); - if (ch == 'e' && isExpOperator(peekCh)) { + var peekCh = this.peek(); + if (ch == 'e' && this.isExpOperator(peekCh)) { number += ch; - } else if (isExpOperator(ch) && - peekCh && isNumber(peekCh) && + } else if (this.isExpOperator(ch) && + peekCh && this.isNumber(peekCh) && number.charAt(number.length - 1) == 'e') { number += ch; - } else if (isExpOperator(ch) && - (!peekCh || !isNumber(peekCh)) && + } else if (this.isExpOperator(ch) && + (!peekCh || !this.isNumber(peekCh)) && number.charAt(number.length - 1) == 'e') { - throwError('Invalid exponent'); + this.throwError('Invalid exponent'); } else { break; } } - index++; + this.index++; } number = 1 * number; - tokens.push({index:start, text:number, json:true, - fn:function() {return number;}}); - } - function readIdent() { - var ident = "", - start = index, - lastDot, peekIndex, methodName, ch; - - while (index < text.length) { - ch = text.charAt(index); - if (ch == '.' || isIdent(ch) || isNumber(ch)) { - if (ch == '.') lastDot = index; + this.tokens.push({ + index: start, + text: number, + json: true, + fn: function() { return number; } + }); + }, + + readIdent: function() { + var parser = this; + + var ident = ''; + var start = this.index; + + var lastDot, peekIndex, methodName, ch; + + while (this.index < this.text.length) { + ch = this.text.charAt(this.index); + if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { + if (ch === '.') lastDot = this.index; ident += ch; } else { break; } - index++; + this.index++; } //check if this is not a method invocation and if it is back out to last dot if (lastDot) { - peekIndex = index; - while(peekIndex < text.length) { - ch = text.charAt(peekIndex); - if (ch == '(') { + peekIndex = this.index; + while (peekIndex < this.text.length) { + ch = this.text.charAt(peekIndex); + if (ch === '(') { methodName = ident.substr(lastDot - start + 1); ident = ident.substr(0, lastDot - start); - index = peekIndex; + this.index = peekIndex; break; } - if(isWhitespace(ch)) { + if (this.isWhitespace(ch)) { peekIndex++; } else { break; @@ -252,54 +289,55 @@ function lex(text, csp){ var token = { - index:start, - text:ident + index: start, + text: ident }; if (OPERATORS.hasOwnProperty(ident)) { - token.fn = token.json = OPERATORS[ident]; + token.fn = OPERATORS[ident]; + token.json = OPERATORS[ident]; } else { - var getter = getterFn(ident, csp, text); + var getter = getterFn(ident, this.csp, this.text, this.options); token.fn = extend(function(self, locals) { return (getter(self, locals)); }, { assign: function(self, value) { - return setter(self, ident, value, text); + return setter(self, ident, value, parser.text, parser.options); } }); } - tokens.push(token); + this.tokens.push(token); if (methodName) { - tokens.push({ + this.tokens.push({ index:lastDot, text: '.', json: false }); - tokens.push({ + this.tokens.push({ index: lastDot + 1, text: methodName, json: false }); } - } + }, - function readString(quote) { - var start = index; - index++; - var string = ""; + readString: function(quote) { + var start = this.index; + this.index++; + var string = ''; var rawString = quote; var escape = false; - while (index < text.length) { - var ch = text.charAt(index); + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); rawString += ch; if (escape) { - if (ch == 'u') { - var hex = text.substring(index + 1, index + 5); + if (ch === 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); if (!hex.match(/[\da-f]{4}/i)) - throwError( "Invalid unicode escape [\\u" + hex + "]"); - index += 4; + this.throwError('Invalid unicode escape [\\u' + hex + ']'); + this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; @@ -310,172 +348,226 @@ function lex(text, csp){ } } escape = false; - } else if (ch == '\\') { + } else if (ch === '\\') { escape = true; - } else if (ch == quote) { - index++; - tokens.push({ - index:start, - text:rawString, - string:string, - json:true, - fn:function() { return string; } + } else if (ch === quote) { + this.index++; + this.tokens.push({ + index: start, + text: rawString, + string: string, + json: true, + fn: function() { return string; } }); return; } else { string += ch; } - index++; + this.index++; } - throwError("Unterminated quote", start); + this.throwError('Unterminated quote', start); } -} +}; -///////////////////////////////////////// -function parser(text, json, $filter, csp){ - var ZERO = valueFn(0), - value, - tokens = lex(text, csp), - assignment = _assignment, - functionCall = _functionCall, - fieldAccess = _fieldAccess, - objectIndex = _objectIndex, - filterChain = _filterChain; - - if(json){ - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - assignment = logicalOR; - functionCall = - fieldAccess = - objectIndex = - filterChain = - function() { throwError("is not valid json", {text:text, index:0}); }; - value = primary(); - } else { - value = statements(); - } - if (tokens.length !== 0) { - throwError("is an unexpected token", tokens[0]); - } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; +/** + * @constructor + */ +var Parser = function (lexer, $filter, csp) { + this.lexer = lexer; + this.$filter = $filter; + this.csp = csp; +}; - /////////////////////////////////// - function throwError(msg, token) { - throw $parseMinErr('syntax', - "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", - token.text, msg, (token.index + 1), text, text.substring(token.index)); - } +Parser.ZERO = function () { return 0; }; - function peekToken() { - if (tokens.length === 0) - throw $parseMinErr('ueoe', "Unexpected end of expression: {0}", text); - return tokens[0]; - } +Parser.prototype = { + constructor: Parser, + + parse: function (text, json, options) { + this.text = text; + this.json = json; + this.options = options; + + this.tokens = this.lexer.lex(text, options); + + if (json) { + // The extra level of aliasing is here, just in case the lexer misses something, so that + // we prevent any accidental execution in JSON. + this.assignment = this.logicalOR; + + this.functionCall = + this.fieldAccess = + this.objectIndex = + this.filterChain = function() { + throwError('is not valid json', {text: text, index: 0}); + }; + } + + var value = json ? this.primary() : this.statements(); + + if (this.tokens.length !== 0) { + this.throwError('is an unexpected token', this.tokens[0]); + } + + value.literal = !!value.literal; + value.constant = !!value.constant; + + return value; + }, - function peek(e1, e2, e3, e4) { - if (tokens.length > 0) { - var token = tokens[0]; + primary: function () { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.throwError('not a primary expression', token); + } + if (token.json) { + primary.constant = true; + primary.literal = true; + } + } + + var next, context; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = this.functionCall(primary, context); + context = null; + } else if (next.text === '[') { + context = primary; + primary = this.objectIndex(primary); + } else if (next.text === '.') { + context = primary; + primary = this.fieldAccess(primary); + } else { + this.throwError('IMPOSSIBLE'); + } + } + return primary; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + peekToken: function() { + if (this.tokens.length === 0) + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + if (this.tokens.length > 0) { + var token = this.tokens[0]; var t = token.text; - if (t==e1 || t==e2 || t==e3 || t==e4 || + if (t === e1 || t === e2 || t === e3 || t === e4 || (!e1 && !e2 && !e3 && !e4)) { return token; } } return false; - } + }, - function expect(e1, e2, e3, e4){ - var token = peek(e1, e2, e3, e4); + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); if (token) { - if (json && !token.json) { - throwError("is not valid json", token); + if (this.json && !token.json) { + this.throwError('is not valid json', token); } - tokens.shift(); + this.tokens.shift(); return token; } return false; - } + }, - function consume(e1){ - if (!expect(e1)) { - throwError("is unexpected, expecting [" + e1 + "]", peek()); + consume: function(e1){ + if (!this.expect(e1)) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); } - } + }, - function unaryFn(fn, right) { + unaryFn: function(fn, right) { return extend(function(self, locals) { return fn(self, locals, right); }, { constant:right.constant }); - } + }, - function ternaryFn(left, middle, right){ + ternaryFn: function(left, middle, right){ return extend(function(self, locals){ return left(self, locals) ? middle(self, locals) : right(self, locals); }, { constant: left.constant && middle.constant && right.constant }); - } + }, - function binaryFn(left, fn, right) { + binaryFn: function(left, fn, right) { return extend(function(self, locals) { return fn(self, locals, left, right); }, { constant:left.constant && right.constant }); - } + }, - function statements() { + statements: function() { var statements = []; - while(true) { - if (tokens.length > 0 && !peek('}', ')', ';', ']')) - statements.push(filterChain()); - if (!expect(';')) { + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? - return statements.length == 1 - ? statements[0] - : function(self, locals){ + return (statements.length === 1) ? + statements[0] : + function(self, locals) { var value; - for ( var i = 0; i < statements.length; i++) { + for (var i = 0; i < statements.length; i++) { var statement = statements[i]; - if (statement) + if (statement) { value = statement(self, locals); + } } return value; }; } } - } + }, - function _filterChain() { - var left = expression(); + filterChain: function() { + var left = this.expression(); var token; - while(true) { - if ((token = expect('|'))) { - left = binaryFn(left, token.fn, filter()); + while (true) { + if ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } else { return left; } } - } + }, - function filter() { - var token = expect(); - var fn = $filter(token.text); + filter: function() { + var token = this.expect(); + var fn = this.$filter(token.text); var argsFn = []; - while(true) { - if ((token = expect(':'))) { - argsFn.push(expression()); + while (true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); } else { - var fnInvoke = function(self, locals, input){ + var fnInvoke = function(self, locals, input) { var args = [input]; - for ( var i = 0; i < argsFn.length; i++) { + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self, locals)); } return fn.apply(self, args); @@ -485,227 +577,190 @@ function parser(text, json, $filter, csp){ }; } } - } + }, - function expression() { - return assignment(); - } + expression: function() { + return this.assignment(); + }, - function _assignment() { - var left = ternary(); + assignment: function() { + var left = this.ternary(); var right; var token; - if ((token = expect('='))) { + if ((token = this.expect('='))) { if (!left.assign) { - throwError("implies assignment but [" + - text.substring(0, token.index) + "] can not be assigned to", token); + this.throwError('implies assignment but [' + + this.text.substring(0, token.index) + '] can not be assigned to', token); } - right = ternary(); - return function(scope, locals){ + right = this.ternary(); + return function(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; - } else { - return left; } - } + return left; + }, - function ternary() { - var left = logicalOR(); + ternary: function() { + var left = this.logicalOR(); var middle; var token; - if((token = expect('?'))){ - middle = ternary(); - if((token = expect(':'))){ - return ternaryFn(left, middle, ternary()); - } - else { - throwError('expected :', token); + if ((token = this.expect('?'))) { + middle = this.ternary(); + if ((token = this.expect(':'))) { + return this.ternaryFn(left, middle, this.ternary()); + } else { + this.throwError('expected :', token); } - } - else { + } else { return left; } - } + }, - function logicalOR() { - var left = logicalAND(); + logicalOR: function() { + var left = this.logicalAND(); var token; - while(true) { - if ((token = expect('||'))) { - left = binaryFn(left, token.fn, logicalAND()); + while (true) { + if ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } else { return left; } } - } + }, - function logicalAND() { - var left = equality(); + logicalAND: function() { + var left = this.equality(); var token; - if ((token = expect('&&'))) { - left = binaryFn(left, token.fn, logicalAND()); + if ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } return left; - } + }, - function equality() { - var left = relational(); + equality: function() { + var left = this.relational(); var token; - if ((token = expect('==','!=','===','!=='))) { - left = binaryFn(left, token.fn, equality()); + if ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.fn, this.equality()); } return left; - } + }, - function relational() { - var left = additive(); + relational: function() { + var left = this.additive(); var token; - if ((token = expect('<', '>', '<=', '>='))) { - left = binaryFn(left, token.fn, relational()); + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.fn, this.relational()); } return left; - } + }, - function additive() { - var left = multiplicative(); + additive: function() { + var left = this.multiplicative(); var token; - while ((token = expect('+','-'))) { - left = binaryFn(left, token.fn, multiplicative()); + while ((token = this.expect('+','-'))) { + left = this.binaryFn(left, token.fn, this.multiplicative()); } return left; - } + }, - function multiplicative() { - var left = unary(); + multiplicative: function() { + var left = this.unary(); var token; - while ((token = expect('*','/','%'))) { - left = binaryFn(left, token.fn, unary()); + while ((token = this.expect('*','/','%'))) { + left = this.binaryFn(left, token.fn, this.unary()); } return left; - } + }, - function unary() { + unary: function() { var token; - if (expect('+')) { - return primary(); - } else if ((token = expect('-'))) { - return binaryFn(ZERO, token.fn, unary()); - } else if ((token = expect('!'))) { - return unaryFn(token.fn, unary()); + if (this.expect('+')) { + return this.primary(); + } else if ((token = this.expect('-'))) { + return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + } else if ((token = this.expect('!'))) { + return this.unaryFn(token.fn, this.unary()); } else { - return primary(); + return this.primary(); } - } + }, + fieldAccess: function(object) { + var parser = this; + var field = this.expect().text; + var getter = getterFn(field, this.csp, this.text, this.options); - function primary() { - var primary; - if (expect('(')) { - primary = filterChain(); - consume(')'); - } else if (expect('[')) { - primary = arrayDeclaration(); - } else if (expect('{')) { - primary = object(); - } else { - var token = expect(); - primary = token.fn; - if (!primary) { - throwError("not a primary expression", token); - } - if (token.json) { - primary.constant = primary.literal = true; + return extend(function(scope, locals, self) { + return getter(self || object(scope, locals), locals); + }, { + assign: function(scope, value, locals) { + return setter(object(scope, locals), field, value, parser.text, parser.options); } - } + }); + }, - var next, context; - while ((next = expect('(', '[', '.'))) { - if (next.text === '(') { - primary = functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = fieldAccess(primary); - } else { - throwError("IMPOSSIBLE"); - } - } - return primary; - } + objectIndex: function(obj) { + var parser = this; - function _fieldAccess(object) { - var field = expect().text; - var getter = getterFn(field, csp, text); - return extend( - function(scope, locals, self) { - return getter(self || object(scope, locals), locals); - }, - { - assign:function(scope, value, locals) { - return setter(object(scope, locals), field, value, text); - } - } - ); - } + var indexFn = this.expression(); + this.consume(']'); - function _objectIndex(obj) { - var indexFn = expression(); - consume(']'); - return extend( - function(self, locals){ - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; - - if (!o) return undefined; - v = ensureSafeObject(o[i], text); - if (v && v.then) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return v; - }, { - assign:function(self, value, locals){ - var key = indexFn(self, locals); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - return ensureSafeObject(obj(self, locals), text)[key] = value; + return extend(function(self, locals) { + var o = obj(self, locals), + i = indexFn(self, locals), + v, p; + + if (!o) return undefined; + v = ensureSafeObject(o[i], parser.text); + if (v && v.then && parser.options.unwrapPromises) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); } - }); - } + v = v.$$v; + } + return v; + }, { + assign: function(self, value, locals) { + var key = indexFn(self, locals); + // prevent overwriting of Function.constructor which would break ensureSafeObject check + var safe = ensureSafeObject(obj(self, locals), parser.text); + return safe[key] = value; + } + }); + }, - function _functionCall(fn, contextGetter) { + functionCall: function(fn, contextGetter) { var argsFn = []; - if (peekToken().text != ')') { + if (this.peekToken().text !== ')') { do { - argsFn.push(expression()); - } while (expect(',')); + argsFn.push(this.expression()); + } while (this.expect(',')); } - consume(')'); - return function(scope, locals){ - var args = [], - context = contextGetter ? contextGetter(scope, locals) : scope; + this.consume(')'); - for ( var i = 0; i < argsFn.length; i++) { + var parser = this; + + return function(scope, locals) { + var args = []; + var context = contextGetter ? contextGetter(scope, locals) : scope; + + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](scope, locals)); } var fnPtr = fn(scope, locals, context) || noop; - ensureSafeObject(fnPtr, text); + ensureSafeObject(fnPtr, parser.text); // IE stupidity! - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + var v = fnPtr.apply ? + fnPtr.apply(context, args) : + fnPtr(args[0], args[1], args[2], args[3], args[4]); // Check for promise - if (v && v.then) { + if (v && v.then && parser.options.unwrapPromises) { var p = v; if (!('$$v' in v)) { p.$$v = undefined; @@ -714,71 +769,76 @@ function parser(text, json, $filter, csp){ v = v.$$v; } - return ensureSafeObject(v, text); + return ensureSafeObject(v, parser.text); }; - } + }, // This is used with json array declaration - function arrayDeclaration () { + arrayDeclaration: function () { var elementFns = []; var allConstant = true; - if (peekToken().text != ']') { + if (this.peekToken().text !== ']') { do { - var elementFn = expression(); + var elementFn = this.expression(); elementFns.push(elementFn); if (!elementFn.constant) { allConstant = false; } - } while (expect(',')); + } while (this.expect(',')); } - consume(']'); - return extend(function(self, locals){ + this.consume(']'); + + return extend(function(self, locals) { var array = []; - for ( var i = 0; i < elementFns.length; i++) { + for (var i = 0; i < elementFns.length; i++) { array.push(elementFns[i](self, locals)); } return array; }, { - literal:true, - constant:allConstant + literal: true, + constant: allConstant }); - } + }, - function object () { + object: function () { var keyValues = []; var allConstant = true; - if (peekToken().text != '}') { + if (this.peekToken().text !== '}') { do { - var token = expect(), + var token = this.expect(), key = token.string || token.text; - consume(":"); - var value = expression(); - keyValues.push({key:key, value:value}); + this.consume(':'); + var value = this.expression(); + keyValues.push({key: key, value: value}); if (!value.constant) { allConstant = false; } - } while (expect(',')); + } while (this.expect(',')); } - consume('}'); - return extend(function(self, locals){ + this.consume('}'); + + return extend(function(self, locals) { var object = {}; - for ( var i = 0; i < keyValues.length; i++) { + for (var i = 0; i < keyValues.length; i++) { var keyValue = keyValues[i]; object[keyValue.key] = keyValue.value(self, locals); } return object; }, { - literal:true, - constant:allConstant + literal: true, + constant: allConstant }); - } -} + }, +}; + ////////////////////////////////////////////////// // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp) { +function setter(obj, path, setValue, fullExp, options) { + options = options || {}; + var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); @@ -788,7 +848,7 @@ function setter(obj, path, setValue, fullExp) { obj[key] = propertyObj; } obj = propertyObj; - if (obj.then) { + if (obj.then && options.unwrapPromises) { if (!("$$v" in obj)) { (function(promise) { promise.then(function(val) { promise.$$v = val; }); } @@ -812,7 +872,7 @@ var getterFnCache = {}; * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { ensureSafeMemberName(key0, fullExp); ensureSafeMemberName(key1, fullExp); ensureSafeMemberName(key2, fullExp); @@ -825,7 +885,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { if (pathVal === null || pathVal === undefined) return pathVal; pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { + if (pathVal && pathVal.then && options.unwrapPromises) { if (!("$$v" in pathVal)) { promise = pathVal; promise.$$v = undefined; @@ -836,7 +896,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { if (!key1 || pathVal === null || pathVal === undefined) return pathVal; pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { + if (pathVal && pathVal.then && options.unwrapPromises) { if (!("$$v" in pathVal)) { promise = pathVal; promise.$$v = undefined; @@ -847,7 +907,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { if (!key2 || pathVal === null || pathVal === undefined) return pathVal; pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { + if (pathVal && pathVal.then && options.unwrapPromises) { if (!("$$v" in pathVal)) { promise = pathVal; promise.$$v = undefined; @@ -858,7 +918,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { if (!key3 || pathVal === null || pathVal === undefined) return pathVal; pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { + if (pathVal && pathVal.then && options.unwrapPromises) { if (!("$$v" in pathVal)) { promise = pathVal; promise.$$v = undefined; @@ -869,7 +929,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { if (!key4 || pathVal === null || pathVal === undefined) return pathVal; pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { + if (pathVal && pathVal.then && options.unwrapPromises) { if (!("$$v" in pathVal)) { promise = pathVal; promise.$$v = undefined; @@ -881,9 +941,13 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { }; } -function getterFn(path, csp, fullExp) { - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; +function getterFn(path, csp, fullExp, options) { + options = options || {}; + + var cacheKey = path; + cacheKey += '#unwrapPromises:' + (!!options.unwrapPromises).toString(); + if (getterFnCache.hasOwnProperty(cacheKey)) { + return getterFnCache[cacheKey]; } var pathKeys = path.split('.'), @@ -892,12 +956,12 @@ function getterFn(path, csp, fullExp) { if (csp) { fn = (pathKeysLength < 6) - ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp) + ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, options) : function(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn( - pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp + pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp, options )(scope, locals); locals = undefined; // clear after first iteration @@ -916,21 +980,23 @@ function getterFn(path, csp, fullExp) { ? 's' // but if we are first then we check locals first, and if so read it first : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - 'if (s && s.then) {\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n'; + (options.unwrapPromises + ? 'if (s && s.then) {\n' + + ' if (!("$$v" in s)) {\n' + + ' p=s;\n' + + ' p.$$v = undefined;\n' + + ' p.then(function(v) {p.$$v=v;});\n' + + '}\n' + + ' s=s.$$v\n' + + '}\n' + : ''); }); code += 'return s;'; fn = Function('s', 'k', code); // s=scope, k=locals fn.toString = function() { return code; }; } - return getterFnCache[path] = fn; + return getterFnCache[cacheKey] = fn; } /////////////////////////////////// @@ -977,15 +1043,37 @@ function getterFn(path, csp, fullExp) { function $ParseProvider() { var cache = {}; this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { - return function(exp) { - switch(typeof exp) { + return function(exp, options) { + switch (typeof exp) { case 'string': - return cache.hasOwnProperty(exp) - ? cache[exp] - : cache[exp] = parser(exp, false, $filter, $sniffer.csp); + options = options || {}; + options.unwrapPromises = !!options.unwrapPromises; + + var cacheKey = exp; + for (var option in options) { + if (options.hasOwnProperty(option)) { + cacheKey += '#' + option + ':' + options[option].toString(); + } + } + + if (cache.hasOwnProperty(cacheKey)) { + return cache[cacheKey]; + } + + var lexer = new Lexer($sniffer.csp); + var parser = new Parser(lexer, $filter, $sniffer.csp); + return cache[cacheKey] = parser.parse(exp, false, options); + case 'function': + if (options && !equals(options, {})) { + throw new $parseMinErr('options', 'No options available for the $parse() call'); + } return exp; + default: + if (options && !equals(options, {})) { + throw new $parseMinErr('options', 'No options available for the $parse() call'); + } return noop; } }; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index fd892733043c..7aaa8fc59b71 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -356,7 +356,7 @@ function $RootScopeProvider(){ * `oldCollection` object is a copy of the former collection data. * The `scope` refers to the current scope. * - * @returns {function()} Returns a de-registration function for this listener. When the de-registration function + * @returns {function()} Returns a de-registration function for this listener. When the de-registration function * is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { @@ -496,6 +496,7 @@ function $RootScopeProvider(){ var watch, value, last, watchers, asyncQueue = this.$$asyncQueue, + asyncQueueItem, postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, @@ -511,7 +512,8 @@ function $RootScopeProvider(){ while(asyncQueue.length) { try { - current.$eval(asyncQueue.shift()); + asyncQueueItem = asyncQueue.shift(); + current.$eval(asyncQueueItem.expr, asyncQueueItem.options); } catch (e) { $exceptionHandler(e); } @@ -662,8 +664,8 @@ function $RootScopeProvider(){ * * @returns {*} The result of evaluating the expression. */ - $eval: function(expr, locals) { - return $parse(expr)(this, locals); + $eval: function(expr, locals, options) { + return $parse(expr, options)(this, locals); }, /** @@ -693,7 +695,7 @@ function $RootScopeProvider(){ * - `function(scope)`: execute the function with the current `scope` parameter. * */ - $evalAsync: function(expr) { + $evalAsync: function(expr, options) { // if we are outside of an $digest loop and this is the first time we are scheduling async task also schedule // async auto-flush if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { @@ -704,7 +706,7 @@ function $RootScopeProvider(){ }); } - this.$$asyncQueue.push(expr); + this.$$asyncQueue.push({ expr: expr, options: options }); }, $$postDigest : function(expr) { @@ -757,10 +759,10 @@ function $RootScopeProvider(){ * * @returns {*} The result of evaluating the expression. */ - $apply: function(expr) { + $apply: function(expr, options) { try { beginPhase('$apply'); - return this.$eval(expr); + return this.$eval(expr, options); } catch (e) { $exceptionHandler(e); } finally { diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 44b648c521ed..ec370d7eec57 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2,6 +2,15 @@ describe('parser', function() { describe('lexer', function() { + var lex; + + beforeEach(function () { + lex = function () { + var lexer = new Lexer(); + return lexer.lex.apply(lexer, arguments); + }; + }); + it('should tokenize a string', function() { var tokens = lex("a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\""); var i = 0; @@ -838,9 +847,9 @@ describe('parser', function() { it('should evaluated resolved promise and get its value', function() { deferred.resolve('hello!'); scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe('hello!'); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe('hello!'); }); @@ -849,48 +858,48 @@ describe('parser', function() { scope.greeting = promise; expect(scope.$eval('gretting')).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); }); it('should evaluate a promise and eventualy get its value', function() { scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve('hello!'); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe('hello!'); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe('hello!'); }); it('should evaluate a promise and eventualy ignore its rejection', function() { scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); deferred.reject('sorry'); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); + expect(scope.$eval('greeting', {}, { unwrapPromises: true })).toBe(undefined); }); it('should evaluate a function call returning a promise and eventually get its return value', function() { scope.greetingFn = function() { return promise; }; - expect(scope.$eval('greetingFn()')).toBe(undefined); + expect(scope.$eval('greetingFn()', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greetingFn()')).toBe(undefined); + expect(scope.$eval('greetingFn()', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve('hello!'); - expect(scope.$eval('greetingFn()')).toBe(undefined); + expect(scope.$eval('greetingFn()', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greetingFn()')).toBe('hello!'); + expect(scope.$eval('greetingFn()', {}, { unwrapPromises: true })).toBe('hello!'); }); describe('assignment into promises', function() { @@ -900,7 +909,7 @@ describe('parser', function() { scope.person = promise; deferred.resolve({'name': 'Bill Gates'}); - var getter = $parse('person.name'); + var getter = $parse('person.name', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -914,7 +923,7 @@ describe('parser', function() { scope.greeting = promise; deferred.resolve('Salut!'); - var getter = $parse('greeting'); + var getter = $parse('greeting', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -928,7 +937,7 @@ describe('parser', function() { it('should evaluate an unresolved promise and set and remember its value', inject(function($parse) { scope.person = promise; - var getter = $parse('person.name'); + var getter = $parse('person.name', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -939,7 +948,7 @@ describe('parser', function() { expect(getter(scope)).toBe('Bonjour'); - var c1Getter = $parse('person.A.B.C1'); + var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true }); scope.$digest(); expect(c1Getter(scope)).toBe(undefined); c1Getter.assign(scope, 'c1_value'); @@ -947,7 +956,7 @@ describe('parser', function() { expect(c1Getter(scope)).toBe('c1_value'); // Set another property on the person.A.B - var c2Getter = $parse('person.A.B.C2'); + var c2Getter = $parse('person.A.B.C2', { unwrapPromises: true }); scope.$digest(); expect(c2Getter(scope)).toBe(undefined); c2Getter.assign(scope, 'c2_value'); @@ -955,103 +964,136 @@ describe('parser', function() { expect(c2Getter(scope)).toBe('c2_value'); // c1 should be unchanged. - expect($parse('person.A')(scope)).toEqual( + expect($parse('person.A', { unwrapPromises: true })(scope)).toEqual( {B: {C1: 'c1_value', C2: 'c2_value'}}); })); it('should evaluate a resolved promise and overwrite the previous set value in the absense of the getter', - inject(function($parse) { + inject(function($parse) { scope.person = promise; - var c1Getter = $parse('person.A.B.C1'); + var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true }); c1Getter.assign(scope, 'c1_value'); // resolving the promise should update the tree. deferred.resolve({A: {B: {C1: 'resolved_c1'}}}); scope.$digest(); expect(c1Getter(scope)).toEqual('resolved_c1'); })); + + + describe('do-not-unwrap-promises flag', function () { + it('should return the promise object instead of evaluating it', inject(function ($parse) { + scope.person = promise; + scope.getPerson = function () { return promise; }; + + var getter = $parse('person', { unwrapPromises: false }); + var fnGetter = $parse('getPerson()', { unwrapPromises: false }); + + expect(getter(scope)).toBe(promise); + expect(fnGetter(scope)).toBe(promise); + + scope.$digest(); + expect(getter(scope)).toBe(promise); + expect(fnGetter(scope)).toBe(promise); + })); + + it('promise unwrapping should be disabled by default', inject(function ($parse) { + scope.person = promise; + scope.getPerson = function () { return promise; }; + + var getter = $parse('person'); + var fnGetter = $parse('getPerson()'); + + expect(getter(scope)).toBe(promise); + expect(fnGetter(scope)).toBe(promise); + + scope.$digest(); + expect(getter(scope)).toBe(promise); + expect(fnGetter(scope)).toBe(promise); + })); + }); }); }); describe('dereferencing', function() { it('should evaluate and dereference properties leading to and from a promise', function() { scope.obj = {greeting: promise}; - expect(scope.$eval('obj.greeting')).toBe(undefined); - expect(scope.$eval('obj.greeting.polite')).toBe(undefined); + expect(scope.$eval('obj.greeting', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('obj.greeting.polite', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('obj.greeting')).toBe(undefined); - expect(scope.$eval('obj.greeting.polite')).toBe(undefined); + expect(scope.$eval('obj.greeting', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('obj.greeting.polite', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve({polite: 'Good morning!'}); scope.$digest(); - expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'}); - expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!'); + expect(scope.$eval('obj.greeting', {}, { unwrapPromises: true })).toEqual({polite: 'Good morning!'}); + expect(scope.$eval('obj.greeting.polite', {}, { unwrapPromises: true })).toBe('Good morning!'); }); it('should evaluate and dereference properties leading to and from a promise via bracket ' + 'notation', function() { scope.obj = {greeting: promise}; - expect(scope.$eval('obj["greeting"]')).toBe(undefined); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); + expect(scope.$eval('obj["greeting"]', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('obj["greeting"]["polite"]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('obj["greeting"]')).toBe(undefined); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); + expect(scope.$eval('obj["greeting"]', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('obj["greeting"]["polite"]', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve({polite: 'Good morning!'}); scope.$digest(); - expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'}); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!'); + expect(scope.$eval('obj["greeting"]', {}, { unwrapPromises: true })).toEqual({polite: 'Good morning!'}); + expect(scope.$eval('obj["greeting"]["polite"]', {}, { unwrapPromises: true })).toBe('Good morning!'); }); it('should evaluate and dereference array references leading to and from a promise', function() { scope.greetings = [promise]; - expect(scope.$eval('greetings[0]')).toBe(undefined); - expect(scope.$eval('greetings[0][0]')).toBe(undefined); + expect(scope.$eval('greetings[0]', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('greetings[0][0]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('greetings[0]')).toBe(undefined); - expect(scope.$eval('greetings[0][0]')).toBe(undefined); + expect(scope.$eval('greetings[0]', {}, { unwrapPromises: true })).toBe(undefined); + expect(scope.$eval('greetings[0][0]', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve(['Hi!', 'Cau!']); scope.$digest(); - expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); - expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); + expect(scope.$eval('greetings[0]', {}, { unwrapPromises: true })).toEqual(['Hi!', 'Cau!']); + expect(scope.$eval('greetings[0][0]', {}, { unwrapPromises: true })).toBe('Hi!'); }); it('should evaluate and dereference promises used as function arguments', function() { scope.greet = function(name) { return 'Hi ' + name + '!'; }; scope.name = promise; - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + expect(scope.$eval('greet(name)', {}, { unwrapPromises: true })).toBe('Hi undefined!'); scope.$digest(); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + expect(scope.$eval('greet(name)', {}, { unwrapPromises: true })).toBe('Hi undefined!'); deferred.resolve('Veronica'); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + expect(scope.$eval('greet(name)', {}, { unwrapPromises: true })).toBe('Hi undefined!'); scope.$digest(); - expect(scope.$eval('greet(name)')).toBe('Hi Veronica!'); + expect(scope.$eval('greet(name)', {}, { unwrapPromises: true })).toBe('Hi Veronica!'); }); it('should evaluate and dereference promises used as array indexes', function() { scope.childIndex = promise; scope.kids = ['Adam', 'Veronica', 'Elisa']; - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + expect(scope.$eval('kids[childIndex]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + expect(scope.$eval('kids[childIndex]', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve(1); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + expect(scope.$eval('kids[childIndex]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe('Veronica'); + expect(scope.$eval('kids[childIndex]', {}, { unwrapPromises: true })).toBe('Veronica'); }); @@ -1059,16 +1101,16 @@ describe('parser', function() { scope.childKey = promise; scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'}; - expect(scope.$eval('kids[childKey]')).toBe(undefined); + expect(scope.$eval('kids[childKey]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + expect(scope.$eval('kids[childKey]', {}, { unwrapPromises: true })).toBe(undefined); deferred.resolve('v'); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + expect(scope.$eval('kids[childKey]', {}, { unwrapPromises: true })).toBe(undefined); scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe('Veronica'); + expect(scope.$eval('kids[childKey]', {}, { unwrapPromises: true })).toBe('Veronica'); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 656385e9681c..6b3ad34294e5 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -777,7 +777,9 @@ describe('Scope', function() { expect(childScope.$$asyncQueue).toBe($rootScope.$$asyncQueue); expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue); - expect($rootScope.$$asyncQueue).toEqual(['rootExpression', 'childExpression', 'isolateExpression']); + + var queueExpressions = map($rootScope.$$asyncQueue, function (item) { return item.expr; }); + expect(queueExpressions).toEqual(['rootExpression', 'childExpression', 'isolateExpression']); }));