diff --git a/src/ng/parse.js b/src/ng/parse.js index 09b751d3bb6d..f500a32eddc0 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -82,57 +82,8 @@ function ensureSafeFunction(obj, fullExpression) { } } -//Keyword constants -var CONSTANTS = createMap(); -forEach({ - 'null': function() { return null; }, - 'true': function() { return true; }, - 'false': function() { return false; }, - 'undefined': function() {} -}, function(constantGetter, name) { - constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; - CONSTANTS[name] = constantGetter; -}); - -//Not quite a constant, but can be lex/parsed the same -CONSTANTS['this'] = function(self) { return self; }; -CONSTANTS['this'].sharedGetter = true; - - -//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter -var OPERATORS = extend(createMap(), { - '+':function(self, locals, a, b) { - a=a(self, locals); b=b(self, locals); - if (isDefined(a)) { - if (isDefined(b)) { - return a + b; - } - return a; - } - return isDefined(b) ? b : undefined;}, - '-':function(self, locals, a, b) { - a=a(self, locals); b=b(self, locals); - return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0); - }, - '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);}, - '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);}, - '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);}, - '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);}, - '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);}, - '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);}, - '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);}, - '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);}, - '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);}, - '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);}, - '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);}, - '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);}, - '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);}, - '!':function(self, locals, a) {return !a(self, locals);}, - - //Tokenized as operators but parsed as assignment/filters - '=':true, - '|':true -}); +var OPERATORS = createMap(); +forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -313,46 +264,155 @@ Lexer.prototype = { } }; - -function isConstant(exp) { - return exp.constant; -} - -/** - * @constructor - */ -var Parser = function(lexer, $filter, options) { +var AST = function(lexer, options) { this.lexer = lexer; - this.$filter = $filter; this.options = options; }; -Parser.ZERO = extend(function() { - return 0; -}, { - sharedGetter: true, - constant: true -}); - -Parser.prototype = { - constructor: Parser, - - parse: function(text) { +AST.Program = 'Program'; +AST.ExpressionStatement = 'ExpressionStatement'; +AST.AssignmentExpression = 'AssignmentExpression'; +AST.ConditionalExpression = 'ConditionalExpression'; +AST.LogicalExpression = 'LogicalExpression'; +AST.BinaryExpression = 'BinaryExpression'; +AST.UnaryExpression = 'UnaryExpression'; +AST.CallExpression = 'CallExpression'; +AST.MemberExpression = 'MemberExpression'; +AST.Identifier = 'Identifier'; +AST.Literal = 'Literal'; +AST.ArrayExpression = 'ArrayExpression'; +AST.Property = 'Property'; +AST.ObjectExpression = 'ObjectExpression'; +AST.ThisExpression = 'ThisExpression'; + +// Internal use only +AST.NGValueParameter = 'NGValueParameter'; + +AST.prototype = { + ast: function(text) { this.text = text; this.tokens = this.lexer.lex(text); - var value = this.statements(); + var value = this.program(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; }, + program: function() { + var body = []; + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + body.push(this.expressionStatement()); + if (!this.expect(';')) { + return { type: AST.Program, body: body}; + } + } + }, + + expressionStatement: function() { + return { type: AST.ExpressionStatement, expression: this.filterChain() }; + }, + + filterChain: function() { + var left = this.expression(); + var token; + while ((token = this.expect('|'))) { + left = this.filter(left); + } + return left; + }, + + expression: function() { + return this.assignment(); + }, + + assignment: function() { + var result = this.ternary(); + if (this.expect('=')) { + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; + } + return result; + }, + + ternary: function() { + var test = this.logicalOR(); + var alternate; + var consequent; + if (this.expect('?')) { + alternate = this.expression(); + if (this.consume(':')) { + consequent = this.expression(); + return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent}; + } + } + return test; + }, + + logicalOR: function() { + var left = this.logicalAND(); + while (this.expect('||')) { + left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; + } + return left; + }, + + logicalAND: function() { + var left = this.equality(); + while (this.expect('&&')) { + left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; + } + return left; + }, + + equality: function() { + var left = this.relational(); + var token; + while ((token = this.expect('==','!=','===','!=='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; + } + return left; + }, + + relational: function() { + var left = this.additive(); + var token; + while ((token = this.expect('<', '>', '<=', '>='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; + } + return left; + }, + + additive: function() { + var left = this.multiplicative(); + var token; + while ((token = this.expect('+','-'))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; + } + return left; + }, + + multiplicative: function() { + var left = this.unary(); + var token; + while ((token = this.expect('*','/','%'))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; + } + return left; + }, + + unary: function() { + var token; + if ((token = this.expect('+', '-', '!'))) { + return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; + } else { + return this.primary(); + } + }, + primary: function() { var primary; if (this.expect('(')) { @@ -362,8 +422,8 @@ Parser.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.peek().identifier && this.peek().text in CONSTANTS) { - primary = CONSTANTS[this.consume().text]; + } else if (this.constants.hasOwnProperty(this.peek().text)) { + primary = copy(this.constants[this.consume().text]); } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -372,17 +432,16 @@ Parser.prototype = { this.throwError('not a primary expression', this.peek()); } - var next, context; + var next; while ((next = this.expect('(', '[', '.'))) { if (next.text === '(') { - primary = this.functionCall(primary, context); - context = null; + primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() }; + this.consume(')'); } else if (next.text === '[') { - context = primary; - primary = this.objectIndex(primary); + primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true }; + this.consume(']'); } else if (next.text === '.') { - context = primary; - primary = this.fieldAccess(primary); + primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false }; } else { this.throwError('IMPOSSIBLE'); } @@ -390,12 +449,100 @@ Parser.prototype = { return primary; }, + filter: function(baseExpression) { + var args = [baseExpression]; + var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; + + while (this.expect(':')) { + args.push(this.expression()); + } + + return result; + }, + + parseArguments: function() { + var args = []; + if (this.peekToken().text !== ')') { + do { + args.push(this.expression()); + } while (this.expect(',')); + } + return args; + }, + + identifier: function() { + var token = this.consume(); + if (!token.identifier) { + this.throwError('is not a valid identifier', token); + } + return { type: AST.Identifier, name: token.text }; + }, + + constant: function() { + // TODO check that it is a constant + return { type: AST.Literal, value: this.consume().value }; + }, + + arrayDeclaration: function() { + var elements = []; + if (this.peekToken().text !== ']') { + do { + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } + elements.push(this.expression()); + } while (this.expect(',')); + } + this.consume(']'); + + return { type: AST.ArrayExpression, elements: elements }; + }, + + object: function() { + var properties = [], property; + if (this.peekToken().text !== '}') { + do { + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; + } + property = {type: AST.Property, kind: 'init'}; + if (this.peek().constant) { + property.key = this.constant(); + } else if (this.peek().identifier) { + property.key = this.identifier(); + } else { + this.throwError("invalid key", this.peek()); + } + this.consume(':'); + property.value = this.expression(); + properties.push(property); + } while (this.expect(',')); + } + this.consume('}'); + + return {type: AST.ObjectExpression, properties: properties }; + }, + 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)); }, + consume: function(e1) { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } + + var token = this.expect(e1); + if (!token) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); + } + return token; + }, + peekToken: function() { if (this.tokens.length === 0) throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); @@ -405,6 +552,7 @@ Parser.prototype = { peek: function(e1, e2, e3, e4) { return this.peekAhead(0, e1, e2, e3, e4); }, + peekAhead: function(i, e1, e2, e3, e4) { if (this.tokens.length > i) { var token = this.tokens[i]; @@ -426,375 +574,1024 @@ Parser.prototype = { return false; }, - consume: function(e1) { - if (this.tokens.length === 0) { - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - } - var token = this.expect(e1); - if (!token) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - return token; - }, + /* `undefined` is not a constant, it is an identifier, + * but using it as an identifier is not supported + */ + constants: { + 'true': { type: AST.Literal, value: true }, + 'false': { type: AST.Literal, value: false }, + 'null': { type: AST.Literal, value: null }, + 'undefined': {type: AST.Literal, value: undefined }, + 'this': {type: AST.ThisExpression } + } +}; - unaryFn: function(op, right) { - var fn = OPERATORS[op]; - return extend(function $parseUnaryFn(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant, - inputs: [right] - }); - }, +function ifDefined(v, d) { + return typeof v !== 'undefined' ? v : d; +} + +function plusFn(l, r) { + if (typeof l === 'undefined') return r; + if (typeof r === 'undefined') return l; + return l + r; +} + +function isStateless($filter, filterName) { + var fn = $filter(filterName); + return !fn.$stateful; +} - binaryFn: function(left, op, right, isBranching) { - var fn = OPERATORS[op]; - return extend(function $parseBinaryFn(self, locals) { - return fn(self, locals, left, right); - }, { - constant: left.constant && right.constant, - inputs: !isBranching && [left, right] +function findConstantAndWatchExpressions(ast, $filter) { + var allConstants; + var argsToWatch; + switch (ast.type) { + case AST.Program: + allConstants = true; + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter); + allConstants = allConstants && expr.expression.constant; }); - }, + ast.constant = allConstants; + break; + case AST.Literal: + ast.constant = true; + ast.toWatch = []; + break; + case AST.UnaryExpression: + findConstantAndWatchExpressions(ast.argument, $filter); + ast.constant = ast.argument.constant; + ast.toWatch = ast.argument.toWatch; + break; + case AST.BinaryExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); + break; + case AST.LogicalExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.ConditionalExpression: + findConstantAndWatchExpressions(ast.test, $filter); + findConstantAndWatchExpressions(ast.alternate, $filter); + findConstantAndWatchExpressions(ast.consequent, $filter); + ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.Identifier: + ast.constant = false; + ast.toWatch = [ast]; + break; + case AST.MemberExpression: + findConstantAndWatchExpressions(ast.object, $filter); + if (ast.computed) { + findConstantAndWatchExpressions(ast.property, $filter); + } + ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); + ast.toWatch = [ast]; + break; + case AST.CallExpression: + allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + argsToWatch = []; + forEach(ast.arguments, function(expr) { + findConstantAndWatchExpressions(expr, $filter); + allConstants = allConstants && expr.constant; + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + break; + case AST.AssignmentExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = [ast]; + break; + case AST.ArrayExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.elements, function(expr) { + findConstantAndWatchExpressions(expr, $filter); + allConstants = allConstants && expr.constant; + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ObjectExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.properties, function(property) { + findConstantAndWatchExpressions(property.value, $filter); + allConstants = allConstants && property.value.constant; + if (!property.value.constant) { + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ThisExpression: + ast.constant = false; + ast.toWatch = []; + break; + } +} - identifier: function() { - var id = this.consume().text; +function getInputs(body) { + if (body.length != 1) return; + var lastExpression = body[0].expression; + var candidate = lastExpression.toWatch; + if (candidate.length !== 1) return candidate; + return candidate[0] !== lastExpression ? candidate : undefined; +} + +function isAssignable(ast) { + return ast.type === AST.Identifier || ast.type === AST.MemberExpression; +} + +function assignableAST(ast) { + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}; + } +} + +function isLiteral(ast) { + return ast.body.length === 0 || + ast.body.length === 1 && ( + ast.body[0].expression.type === AST.Literal || + ast.body[0].expression.type === AST.ArrayExpression || + ast.body[0].expression.type === AST.ObjectExpression); +} + +function isConstant(ast) { + return ast.constant; +} + +function ASTCompiler(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} - //Continue reading each `.identifier` unless it is a method invocation - while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) { - id += this.consume().text + this.consume().text; +ASTCompiler.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.state = { + nextId: 0, + filters: {}, + expensiveChecks: expensiveChecks, + fn: {vars: [], body: [], own: {}}, + assign: {vars: [], body: [], own: {}}, + inputs: [] + }; + findConstantAndWatchExpressions(ast, self.$filter); + var extra = ''; + var assignable; + this.stage = 'assign'; + if ((assignable = assignableAST(ast))) { + this.state.computing = 'assign'; + var result = this.nextId(); + this.recurse(assignable, result); + extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); } + var toWatch = getInputs(ast.body); + self.stage = 'inputs'; + forEach(toWatch, function(watch, key) { + var fnKey = 'fn' + key; + self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state.computing = fnKey; + var intoId = self.nextId(); + self.recurse(watch, intoId); + self.return(intoId); + self.state.inputs.push(fnKey); + watch.watchId = key; + }); + this.state.computing = 'fn'; + this.stage = 'main'; + this.recurse(ast); + var fnString = + // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. + // This is a workaround for this until we do a better job at only removing the prefix only when we should. + '"' + this.USE + ' ' + this.STRICT + '";\n' + + this.filterPrefix() + + 'var fn=' + this.generateFunction('fn', 's,l,a,i') + + extra + + this.watchFns() + + 'return fn;'; - return getterFn(id, this.options, this.text); + /* jshint -W054 */ + var fn = (new Function('$filter', + 'ensureSafeMemberName', + 'ensureSafeObject', + 'ensureSafeFunction', + 'ifDefined', + 'plus', + 'text', + fnString))( + this.$filter, + ensureSafeMemberName, + ensureSafeObject, + ensureSafeFunction, + ifDefined, + plusFn, + expression); + /* jshint +W054 */ + this.state = this.stage = undefined; + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; }, - constant: function() { - var value = this.consume().value; + USE: 'use', - return extend(function $parseConstant() { - return value; - }, { - constant: true, - literal: true + STRICT: 'strict', + + watchFns: function() { + var result = []; + var fns = this.state.inputs; + var self = this; + forEach(fns, function(name) { + result.push('var ' + name + '=' + self.generateFunction(name, 's')); }); + if (fns.length) { + result.push('fn.inputs=[' + fns.join(',') + '];'); + } + return result.join(''); }, - statements: function() { - var statements = []; - 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 $parseStatements(self, locals) { - var value; - for (var i = 0, ii = statements.length; i < ii; i++) { - value = statements[i](self, locals); - } - return value; - }; - } - } + generateFunction: function(name, params) { + return 'function(' + params + '){' + + this.varsPrefix(name) + + this.body(name) + + '};'; }, - filterChain: function() { - var left = this.expression(); - var token; - while ((token = this.expect('|'))) { - left = this.filter(left); - } - return left; + filterPrefix: function() { + var parts = []; + var self = this; + forEach(this.state.filters, function(id, filter) { + parts.push(id + '=$filter(' + self.escape(filter) + ')'); + }); + if (parts.length) return 'var ' + parts.join(',') + ';'; + return ''; + }, + + varsPrefix: function(section) { + return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; }, - filter: function(inputFn) { - var fn = this.$filter(this.consume().text); - var argsFn; - var args; + body: function(section) { + return this.state[section].body.join(''); + }, - if (this.peek(':')) { - argsFn = []; - args = []; // we can safely reuse the array - while (this.expect(':')) { - argsFn.push(this.expression()); + recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var left, right, self = this, args, expression; + recursionFn = recursionFn || noop; + if (!skipWatchIdCheck && isDefined(ast.watchId)) { + intoId = intoId || this.nextId(); + this.if('i', + this.lazyAssign(intoId, this.computedMember('i', ast.watchId)), + this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) + ); + return; + } + switch (ast.type) { + case AST.Program: + forEach(ast.body, function(expression, pos) { + self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; }); + if (pos !== ast.body.length - 1) { + self.current().body.push(right, ';'); + } else { + self.return(right); + } + }); + break; + case AST.Literal: + expression = this.escape(ast.value); + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.UnaryExpression: + this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); + expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.BinaryExpression: + this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; }); + this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); + if (ast.operator === '+') { + expression = this.plus(left, right); + } else if (ast.operator === '-') { + expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); + } else { + expression = '(' + left + ')' + ast.operator + '(' + right + ')'; + } + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.LogicalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.left, intoId); + self.if(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); + recursionFn(intoId); + break; + case AST.ConditionalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.test, intoId); + self.if(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); + recursionFn(intoId); + break; + case AST.Identifier: + intoId = intoId || this.nextId(); + if (nameId) { + nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); + nameId.computed = false; + nameId.name = ast.name; } + ensureSafeMemberName(ast.name); + self.if(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), + function() { + self.if(self.stage === 'inputs' || 's', function() { + if (create && create !== 1) { + self.if( + self.not(self.getHasOwnProperty('s', ast.name)), + self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); + } + self.assign(intoId, self.nonComputedMember('s', ast.name)); + }); + }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) + ); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { + self.addEnsureSafeObject(intoId); + } + recursionFn(intoId); + break; + case AST.MemberExpression: + left = nameId && (nameId.context = this.nextId()) || this.nextId(); + intoId = intoId || this.nextId(); + self.recurse(ast.object, left, undefined, function() { + self.if(self.notNull(left), function() { + if (ast.computed) { + right = self.nextId(); + self.recurse(ast.property, right); + self.addEnsureSafeMemberName(right); + if (create && create !== 1) { + self.if(self.not(right + ' in ' + left), self.lazyAssign(self.computedMember(left, right), '{}')); + } + expression = self.ensureSafeObject(self.computedMember(left, right)); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = true; + nameId.name = right; + } + } else { + ensureSafeMemberName(ast.property.name); + if (create && create !== 1) { + self.if(self.not(self.escape(ast.property.name) + ' in ' + left), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + } + expression = self.nonComputedMember(left, ast.property.name); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { + expression = self.ensureSafeObject(expression); + } + self.assign(intoId, expression); + if (nameId) { + nameId.computed = false; + nameId.name = ast.property.name; + } + } + recursionFn(intoId); + }); + }, !!create); + break; + case AST.CallExpression: + intoId = intoId || this.nextId(); + if (ast.filter) { + right = self.filter(ast.callee.name); + args = []; + forEach(ast.arguments, function(expr) { + var argument = self.nextId(); + self.recurse(expr, argument); + args.push(argument); + }); + expression = right + '(' + args.join(',') + ')'; + self.assign(intoId, expression); + recursionFn(intoId); + } else { + right = self.nextId(); + left = {}; + args = []; + self.recurse(ast.callee, right, left, function() { + self.if(self.notNull(right), function() { + self.addEnsureSafeFunction(right); + forEach(ast.arguments, function(expr) { + self.recurse(expr, undefined, undefined, function(argument) { + args.push(self.ensureSafeObject(argument)); + }); + }); + if (left.name) { + if (!self.state.expensiveChecks) { + self.addEnsureSafeObject(left.context); + } + expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; + } else { + expression = right + '(' + args.join(',') + ')'; + } + expression = self.ensureSafeObject(expression); + self.assign(intoId, expression); + recursionFn(intoId); + }); + }); + } + break; + case AST.AssignmentExpression: + right = this.nextId(); + left = {}; + if (!isAssignable(ast.left)) { + throw $parseMinErr('lval', 'Trying to assing a value to a non l-value'); + } + this.recurse(ast.left, undefined, left, function() { + self.if(self.notNull(left.context), function() { + self.recurse(ast.right, right); + self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); + expression = self.member(left.context, left.name, left.computed) + ast.operator + right; + self.assign(intoId, expression); + recursionFn(expression); + }); + }, 1); + break; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + self.recurse(expr, undefined, undefined, function(argument) { + args.push(argument); + }); + }); + expression = '[' + args.join(',') + ']'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.ObjectExpression: + args = []; + forEach(ast.properties, function(property) { + self.recurse(property.value, undefined, 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(expression); + break; + case AST.ThisExpression: + this.assign(intoId, 's'); + recursionFn('s'); + break; + case AST.NGValueParameter: + this.assign(intoId, 'v'); + recursionFn('v'); + break; } + }, - var inputs = [inputFn].concat(argsFn || []); + getHasOwnProperty: function(element, property) { + var key = element + '.' + property; + var own = this.current().own; + if (!own.hasOwnProperty(key)) { + own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')'); + } + return own[key]; + }, - return extend(function $parseFilter(self, locals) { - var input = inputFn(self, locals); - if (args) { - args[0] = input; + assign: function(id, value) { + if (!id) return; + this.current().body.push(id, '=', value, ';'); + return id; + }, - var i = argsFn.length; - while (i--) { - args[i + 1] = argsFn[i](self, locals); - } + filter: function(filterName) { + if (!this.state.filters.hasOwnProperty(filterName)) { + this.state.filters[filterName] = this.nextId(true); + } + return this.state.filters[filterName]; + }, - return fn.apply(undefined, args); - } + ifDefined: function(id, defaultValue) { + return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; + }, - return fn(input); - }, { - constant: !fn.$stateful && inputs.every(isConstant), - inputs: !fn.$stateful && inputs - }); + plus: function(left, right) { + return 'plus(' + left + ',' + right + ')'; }, - expression: function() { - return this.assignment(); + 'return': function(id) { + this.current().body.push('return ', id, ';'); }, - assignment: function() { - var left = this.ternary(); - var right; - var token; - if ((token = this.expect('='))) { - if (!left.assign) { - this.throwError('implies assignment but [' + - this.text.substring(0, token.index) + '] can not be assigned to', token); + 'if': function(test, alternate, consequent) { + if (test === true) { + alternate(); + } else { + var body = this.current().body; + body.push('if(', test, '){'); + alternate(); + body.push('}'); + if (consequent) { + body.push('else{'); + consequent(); + body.push('}'); } - right = this.ternary(); - return extend(function $parseAssignment(scope, locals) { - return left.assign(scope, right(scope, locals), locals); - }, { - inputs: [left, right] - }); } - return left; }, - ternary: function() { - var left = this.logicalOR(); - var middle; - var token; - if ((token = this.expect('?'))) { - middle = this.assignment(); - if (this.consume(':')) { - var right = this.assignment(); + not: function(expression) { + return '!(' + expression + ')'; + }, - return extend(function $parseTernary(self, locals) { - return left(self, locals) ? middle(self, locals) : right(self, locals); - }, { - constant: left.constant && middle.constant && right.constant - }); - } - } + notNull: function(expression) { + return expression + '!=null'; + }, - return left; + nonComputedMember: function(left, right) { + return left + '.' + right; }, - logicalOR: function() { - var left = this.logicalAND(); - var token; - while ((token = this.expect('||'))) { - left = this.binaryFn(left, token.text, this.logicalAND(), true); - } - return left; + computedMember: function(left, right) { + return left + '[' + right + ']'; }, - logicalAND: function() { - var left = this.equality(); - var token; - while ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.text, this.equality(), true); - } - return left; + member: function(left, right, computed) { + if (computed) return this.computedMember(left, right); + return this.nonComputedMember(left, right); }, - equality: function() { - var left = this.relational(); - var token; - while ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.text, this.relational()); - } - return left; + addEnsureSafeObject: function(item) { + this.current().body.push(this.ensureSafeObject(item), ';'); }, - relational: function() { - var left = this.additive(); - var token; - while ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.text, this.additive()); - } - return left; + addEnsureSafeMemberName: function(item) { + this.current().body.push(this.ensureSafeMemberName(item), ';'); }, - additive: function() { - var left = this.multiplicative(); - var token; - while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.text, this.multiplicative()); - } - return left; + addEnsureSafeFunction: function(item) { + this.current().body.push(this.ensureSafeFunction(item), ';'); }, - multiplicative: function() { - var left = this.unary(); - var token; - while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.text, this.unary()); - } - return left; + ensureSafeObject: function(item) { + return 'ensureSafeObject(' + item + ',text)'; }, - unary: function() { - var token; - if (this.expect('+')) { - return this.primary(); - } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.text, this.unary()); - } else if ((token = this.expect('!'))) { - return this.unaryFn(token.text, this.unary()); - } else { - return this.primary(); - } + ensureSafeMemberName: function(item) { + return 'ensureSafeMemberName(' + item + ',text)'; }, - fieldAccess: function(object) { - var getter = this.identifier(); + ensureSafeFunction: function(item) { + return 'ensureSafeFunction(' + item + ',text)'; + }, - return extend(function $parseFieldAccess(scope, locals, self) { - var o = self || object(scope, locals); - return (o == null) ? undefined : getter(o); - }, { - assign: function(scope, value, locals) { - var o = object(scope, locals); - if (!o) object.assign(scope, o = {}); - return getter.assign(o, value); - } - }); + lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var self = this; + return function() { + self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); + }; }, - objectIndex: function(obj) { - var expression = this.text; + lazyAssign: function(id, value) { + var self = this; + return function() { + self.assign(id, value); + }; + }, - var indexFn = this.expression(); - this.consume(']'); + stringEscapeRegex: /[^ a-zA-Z0-9]/g, - return extend(function $parseObjectIndex(self, locals) { - var o = obj(self, locals), - i = indexFn(self, locals), - v; - - ensureSafeMemberName(i, expression); - if (!o) return undefined; - v = ensureSafeObject(o[i], expression); - return v; - }, { - assign: function(self, value, locals) { - var key = ensureSafeMemberName(indexFn(self, locals), expression); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - var o = ensureSafeObject(obj(self, locals), expression); - if (!o) obj.assign(self, o = {}); - return o[key] = value; - } - }); + stringEscapeFn: function(c) { + return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }, - functionCall: function(fnGetter, contextGetter) { - var argsFn = []; - if (this.peekToken().text !== ')') { - do { - argsFn.push(this.expression()); - } while (this.expect(',')); - } - this.consume(')'); + escape: function(value) { + if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + if (isNumber(value)) return value.toString(); + if (value === true) return 'true'; + if (value === false) return 'false'; + if (value === null) return 'null'; + if (typeof value === 'undefined') return 'undefined'; - var expressionText = this.text; - // we can safely reuse the array across invocations - var args = argsFn.length ? [] : null; + throw $parseMinErr('esc', 'IMPOSSIBLE'); + }, - return function $parseFunctionCall(scope, locals) { - var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; - var fn = fnGetter(scope, locals, context) || noop; + nextId: function(skip, init) { + var id = 'v' + (this.state.nextId++); + if (!skip) { + this.current().vars.push(id + (init ? '=' + init : '')); + } + return id; + }, - if (args) { - var i = argsFn.length; - while (i--) { - args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); - } - } + current: function() { + return this.state[this.state.computing]; + } +}; - ensureSafeObject(context, expressionText); - ensureSafeFunction(fn, expressionText); - // IE doesn't have apply for some native functions - var v = fn.apply - ? fn.apply(context, args) - : fn(args[0], args[1], args[2], args[3], args[4]); +function ASTInterpreter(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} - return ensureSafeObject(v, expressionText); +ASTInterpreter.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.expression = expression; + this.expensiveChecks = expensiveChecks; + findConstantAndWatchExpressions(ast, self.$filter); + var assignable; + var assign; + if ((assignable = assignableAST(ast))) { + assign = this.recurse(assignable); + } + var toWatch = getInputs(ast.body); + var inputs; + if (toWatch) { + inputs = []; + forEach(toWatch, function(watch, key) { + var input = self.recurse(watch); + watch.input = input; + inputs.push(input); + watch.watchId = key; + }); + } + var expressions = []; + forEach(ast.body, function(expression) { + expressions.push(self.recurse(expression.expression)); + }); + var fn = ast.body.length === 0 ? function() {} : + ast.body.length === 1 ? expressions[0] : + function(scope, locals) { + var lastValue; + forEach(expressions, function(exp) { + lastValue = exp(scope, locals); + }); + return lastValue; + }; + if (assign) { + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); }; + } + if (inputs) { + fn.inputs = inputs; + } + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; }, - // This is used with json array declaration - arrayDeclaration: function() { - var elementFns = []; - if (this.peekToken().text !== ']') { - do { - if (this.peek(']')) { - // Support trailing commas per ES5.1. - break; - } - elementFns.push(this.expression()); - } while (this.expect(',')); + recurse: function(ast, context, create) { + var left, right, self = this, args, expression; + if (ast.input) { + return this.inputs(ast.input, ast.watchId); } - this.consume(']'); - - return extend(function $parseArrayLiteral(self, locals) { - var array = []; - for (var i = 0, ii = elementFns.length; i < ii; i++) { - array.push(elementFns[i](self, locals)); + switch (ast.type) { + case AST.Literal: + return this.value(ast.value, context); + case AST.UnaryExpression: + right = this.recurse(ast.argument); + return this['unary' + ast.operator](right, context); + case AST.BinaryExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.LogicalExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.ConditionalExpression: + return this['ternary?:']( + this.recurse(ast.test), + this.recurse(ast.alternate), + this.recurse(ast.consequent), + context + ); + case AST.Identifier: + ensureSafeMemberName(ast.name, self.expression); + return self.identifier(ast.name, + self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), + context, create, self.expression); + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) { + ensureSafeMemberName(ast.property.name, self.expression); + right = ast.property.name; } - return array; - }, { - literal: true, - constant: elementFns.every(isConstant), - inputs: elementFns - }); - }, - - object: function() { - var keys = [], valueFns = []; - if (this.peekToken().text !== '}') { - do { - if (this.peek('}')) { - // Support trailing commas per ES5.1. - break; + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + this.computedMember(left, right, context, create, self.expression) : + this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + case AST.CallExpression: + args = []; + forEach(ast.arguments, function(expr) { + args.push(self.recurse(expr)); + }); + if (ast.filter) right = this.$filter(ast.callee.name); + if (!ast.filter) right = this.recurse(ast.callee, true); + return ast.filter ? + function(scope, locals, assign, inputs) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + var value = right.apply(undefined, values, inputs); + return context ? {context: undefined, name: undefined, value: value} : value; + } : + function(scope, locals, assign, inputs) { + var rhs = right(scope, locals, assign, inputs); + var value; + if (rhs.value != null) { + ensureSafeObject(rhs.context, self.expression); + ensureSafeFunction(rhs.value, self.expression); + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + } + value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + } + return context ? {value: value} : value; + }; + case AST.AssignmentExpression: + left = this.recurse(ast.left, true, 1); + right = this.recurse(ast.right); + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + ensureSafeObject(lhs.value, self.expression); + lhs.context[lhs.name] = rhs; + return context ? {value: rhs} : rhs; + }; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + args.push(self.recurse(expr)); + }); + return function(scope, locals, assign, inputs) { + var value = []; + for (var i = 0; i < args.length; ++i) { + value.push(args[i](scope, locals, assign, inputs)); } - var token = this.consume(); - if (token.constant) { - keys.push(token.value); - } else if (token.identifier) { - keys.push(token.text); - } else { - this.throwError("invalid key", token); + return context ? {value: value} : value; + }; + 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) + }); + }); + 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); } - this.consume(':'); - valueFns.push(this.expression()); - } while (this.expect(',')); + return context ? {value: value} : value; + }; + case AST.ThisExpression: + return function(scope) { + return context ? {value: scope} : scope; + }; + case AST.NGValueParameter: + return function(scope, locals, assign, inputs) { + return context ? {value: assign} : assign; + }; } - this.consume('}'); + }, - return extend(function $parseObjectLiteral(self, locals) { - var object = {}; - for (var i = 0, ii = valueFns.length; i < ii; i++) { - object[keys[i]] = valueFns[i](self, locals); + 'unary+': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = +arg; + } else { + arg = 0; } - return object; - }, { - literal: true, - constant: valueFns.every(isConstant), - inputs: valueFns - }); + return context ? {value: arg} : arg; + }; + }, + 'unary-': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = -arg; + } else { + arg = 0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary!': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = !argument(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary+': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = plusFn(lhs, rhs); + return context ? {value: arg} : arg; + }; + }, + 'binary-': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); + return context ? {value: arg} : arg; + }; + }, + 'binary*': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary/': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary%': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary===': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary&&': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary||': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'ternary?:': function(test, alternate, consequent, context) { + return function(scope, locals, assign, inputs) { + var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + value: function(value, context) { + return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; + }, + identifier: function(name, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var base = locals && (name in locals) ? locals : scope; + if (create && create !== 1 && base && !(name in base)) { + base[name] = {}; + } + var value = base ? base[name] : undefined; + if (expensiveChecks) { + ensureSafeObject(value, expression); + } + if (context) { + return {context: base, name: name, value: value}; + } else { + return value; + } + }; + }, + computedMember: function(left, right, context, create, expression) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs; + var value; + if (lhs != null) { + rhs = right(scope, locals, assign, inputs); + ensureSafeMemberName(rhs, expression); + if (create && create !== 1 && lhs && !(rhs in lhs)) { + lhs[rhs] = {}; + } + value = lhs[rhs]; + ensureSafeObject(value, expression); + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + }; + }, + nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + if (create && create !== 1 && lhs && !(right in lhs)) { + lhs[right] = {}; + } + var value = lhs != null ? lhs[right] : undefined; + if (expensiveChecks || isPossiblyDangerousMemberName(right)) { + ensureSafeObject(value, expression); + } + if (context) { + return {context: lhs, name: right, value: value}; + } else { + return value; + } + }; + }, + inputs: function(input, watchId) { + return function(scope, value, locals, inputs) { + if (inputs) return inputs[watchId]; + return input(scope, value, locals); + }; } }; +/** + * @constructor + */ +var Parser = function(lexer, $filter, options) { + this.lexer = lexer; + this.$filter = $filter; + this.options = options; + this.ast = new AST(this.lexer); + this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : + new ASTCompiler(this.ast, $filter); +}; + +Parser.prototype = { + constructor: Parser, + + parse: function(text) { + return this.astCompiler.compile(text, this.options.expensiveChecks); + } +}; ////////////////////////////////////////////////// // Parser helper functions @@ -826,125 +1623,6 @@ function isPossiblyDangerousMemberName(name) { return name == 'constructor'; } -/** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - ensureSafeMemberName(key2, fullExp); - ensureSafeMemberName(key3, fullExp); - ensureSafeMemberName(key4, fullExp); - var eso = function(o) { - return ensureSafeObject(o, fullExp); - }; - var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity; - var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity; - var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity; - var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity; - var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity; - - return function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = eso0(pathVal[key0]); - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso1(pathVal[key1]); - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso2(pathVal[key2]); - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso3(pathVal[key3]); - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso4(pathVal[key4]); - - return pathVal; - }; -} - -function getterFnWithEnsureSafeObject(fn, fullExpression) { - return function(s, l) { - return fn(s, l, ensureSafeObject, fullExpression); - }; -} - -function getterFn(path, options, fullExp) { - var expensiveChecks = options.expensiveChecks; - var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); - var fn = getterFnCache[path]; - if (fn) return fn; - - - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length; - - // http://jsperf.com/angularjs-parse-getter/6 - if (options.csp) { - if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks); - } else { - fn = function cspSafeGetter(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, expensiveChecks)(scope, locals); - - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - }; - } - } else { - var code = ''; - if (expensiveChecks) { - code += 's = eso(s, fe);\nl = eso(l, fe);\n'; - } - var needsEnsureSafeObject = expensiveChecks; - forEach(pathKeys, function(key, index) { - ensureSafeMemberName(key, fullExp); - var lookupJs = (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key; - if (expensiveChecks || isPossiblyDangerousMemberName(key)) { - lookupJs = 'eso(' + lookupJs + ', fe)'; - needsEnsureSafeObject = true; - } - code += 'if(s == null) return undefined;\n' + - 's=' + lookupJs + ';\n'; - }); - code += 'return s;'; - - /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject - /* jshint +W054 */ - evaledFnGetter.toString = valueFn(code); - if (needsEnsureSafeObject) { - evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp); - } - fn = evaledFnGetter; - } - - fn.sharedGetter = true; - fn.assign = function(self, value) { - return setter(self, path, value, path); - }; - getterFnCache[path] = fn; - return fn; -} - var objectValueOf = Object.prototype.valueOf; function getValueOf(value) { @@ -1006,8 +1684,6 @@ function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); - - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { var $parseOptions = { csp: $sniffer.csp, @@ -1018,27 +1694,13 @@ function $ParseProvider() { expensiveChecks: true }; - function wrapSharedExpression(exp) { - var wrapped = exp; - - if (exp.sharedGetter) { - wrapped = function $parseWrapper(self, locals) { - return exp(self, locals); - }; - wrapped.literal = exp.literal; - wrapped.constant = exp.constant; - wrapped.assign = exp.assign; - } - - return wrapped; - } - return function $parse(exp, interceptorFn, expensiveChecks) { var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': - cacheKey = exp = exp.trim(); + exp = exp.trim(); + cacheKey = exp; var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; @@ -1048,24 +1710,18 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); parsedExpression = parser.parse(exp); - if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { - //oneTime is not part of the exp passed to the Parser so we may have to - //wrap the parsedExpression before adding a $$watchDelegate - parsedExpression = wrapSharedExpression(parsedExpression); parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -1078,21 +1734,6 @@ function $ParseProvider() { } }; - function collectExpressionInputs(inputs, list) { - for (var i = 0, ii = inputs.length; i < ii; i++) { - var input = inputs[i]; - if (!input.constant) { - if (input.inputs) { - collectExpressionInputs(input.inputs, list); - } else if (list.indexOf(input) === -1) { // TODO(perf) can we do better? - list.push(input); - } - } - } - - return list; - } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined @@ -1118,28 +1759,28 @@ function $ParseProvider() { return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } - function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var inputExpressions = parsedExpression.$$inputs || - (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); - + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var inputExpressions = parsedExpression.inputs; var lastResult; if (inputExpressions.length === 1) { - var oldInputValue = expressionInputDirtyCheck; // init to something unique so that equals check fails + var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { - lastResult = parsedExpression(scope); - oldInputValue = newInputValue && getValueOf(newInputValue); + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); + oldInputValueOf = newInputValue && getValueOf(newInputValue); } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } var oldInputValueOfValues = []; + var oldInputValues = []; for (var i = 0, ii = inputExpressions.length; i < ii; i++) { oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + oldInputValues[i] = null; } return scope.$watch(function expressionInputsWatch(scope) { @@ -1148,16 +1789,17 @@ function $ParseProvider() { for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } } if (changed) { - lastResult = parsedExpression(scope); + lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { @@ -1224,11 +1866,11 @@ function $ParseProvider() { watchDelegate !== oneTimeLiteralWatchDelegate && watchDelegate !== oneTimeWatchDelegate; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals) { - var value = parsedExpression(scope, locals); + var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals) { - var value = parsedExpression(scope, locals); + } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); var result = interceptorFn(value, scope, locals); // we only return the interceptor's result if the // initial value is defined (for bind-once) @@ -1243,7 +1885,7 @@ function $ParseProvider() { // If there is an interceptor, but no watchDelegate then treat the interceptor like // we treat filters - it is assumed to be a pure function unless flagged with $stateful fn.$$watchDelegate = inputsWatchDelegate; - fn.inputs = [parsedExpression]; + fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } return fn; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index be150faf1c15..99dd3e00d42a 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -356,11 +356,11 @@ function $RootScopeProvider() { * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ - $watch: function(watchExp, listener, objectEquality) { + $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { var get = $parse(watchExp); if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get); + return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, @@ -368,7 +368,7 @@ function $RootScopeProvider() { fn: listener, last: initWatchVal, get: get, - exp: watchExp, + exp: prettyPrintExpression || watchExp, eq: !!objectEquality }; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 9ad86559fe6e..c690bd61cfec 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -225,6 +225,1450 @@ describe('parser', function() { }); }); + describe('ast', function() { + var createAst; + + beforeEach(function() { + /* global AST: false */ + createAst = function() { + var lexer = new Lexer({csp: false}); + var ast = new AST(lexer, {csp: false}); + return ast.ast.apply(ast, arguments); + }; + }); + + it('should handle an empty list of tokens', function() { + expect(createAst('')).toEqual({type: 'Program', body: []}); + }); + + + it('should understand identifiers', function() { + expect(createAst('foo')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: 'foo' } + } + ] + } + ); + }); + + + it('should understand non-computed member expressions', function() { + expect(createAst('foo.bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo'}, + property: {type: 'Identifier', name: 'bar'}, + computed: false + } + } + ] + } + ); + }); + + + it('should associate non-computed member expressions left-to-right', function() { + expect(createAst('foo.bar.baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo'}, + property: { type: 'Identifier', name: 'bar' }, + computed: false + }, + property: {type: 'Identifier', name: 'baz'}, + computed: false + } + } + ] + } + ); + }); + + + it('should understand computed member expressions', function() { + expect(createAst('foo[bar]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo'}, + property: {type: 'Identifier', name: 'bar'}, + computed: true + } + } + ] + } + ); + }); + + + it('should associate computed member expressions left-to-right', function() { + expect(createAst('foo[bar][baz]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: true + }, + property: { type: 'Identifier', name: 'baz' }, + computed: true + } + } + ] + } + ); + }); + + + it('should understand call expressions', function() { + expect(createAst('foo()')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo'}, + arguments: [] + } + } + ] + } + ); + }); + + + it('should parse call expression arguments', function() { + expect(createAst('foo(bar, baz)')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo'}, + arguments: [ + { type: 'Identifier', name: 'bar' }, + { type: 'Identifier', name: 'baz' } + ] + } + } + ] + } + ); + }); + + + it('should parse call expression left-to-right', function() { + expect(createAst('foo(bar, baz)(man, shell)')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo' }, + arguments: [ + { type: 'Identifier', name: 'bar' }, + { type: 'Identifier', name: 'baz' } + ] + }, + arguments: [ + { type: 'Identifier', name: 'man' }, + { type: 'Identifier', name: 'shell' } + ] + } + } + ] + } + ); + }); + + + it('should keep the context when having superfluous parenthesis', function() { + expect(createAst('(foo)(bar, baz)')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo'}, + arguments: [ + { type: 'Identifier', name: 'bar' }, + { type: 'Identifier', name: 'baz' } + ] + } + } + ] + } + ); + }); + + + it('should treat member expressions and call expression with the same precedence', function() { + expect(createAst('foo.bar[baz]()')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: false + }, + property: { type: 'Identifier', name: 'baz' }, + computed: true + }, + arguments: [] + } + } + ] + } + ); + expect(createAst('foo[bar]().baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: true + }, + arguments: [] + }, + property: { type: 'Identifier', name: 'baz' }, + computed: false + } + } + ] + } + ); + expect(createAst('foo().bar[baz]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo' }, + arguments: [] }, + property: { type: 'Identifier', name: 'bar' }, + computed: false + }, + property: { type: 'Identifier', name: 'baz' }, + computed: true + } + } + ] + } + ); + }); + + + it('should understand literals', function() { + // In a strict sense, `undefined` is not a literal but an identifier + forEach({'123': 123, '"123"': '123', 'true': true, 'false': false, 'null': null, 'undefined': undefined}, function(value, expression) { + expect(createAst(expression)).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Literal', value: value } + } + ] + } + ); + }); + }); + + + it('should understand the `this` expression', function() { + expect(createAst('this')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'ThisExpression' } + } + ] + } + ); + }); + + + it('should not confuse `this`, `undefined`, `true`, `false`, `null` when used as identfiers', function() { + forEach(['this', 'undefined', 'true', 'false', 'null'], function(identifier) { + expect(createAst('foo.' + identifier)).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: identifier }, + computed: false + } + } + ] + } + ); + }); + }); + + + it('should throw when trying to use non-identifiers as identifiers', function() { + expect(function() { createAst('foo.)'); }).toThrowMinErr('$parse', 'syntax', + "Syntax Error: Token ')' is not a valid identifier at column 5 of the expression [foo.)"); + }); + + + it('should throw when all tokens are not consumed', function() { + expect(function() { createAst('foo bar'); }).toThrowMinErr('$parse', 'syntax', + "Syntax Error: Token 'bar' is an unexpected token at column 5 of the expression [foo bar] starting at [bar]"); + }); + + + it('should understand the unary operators `-`, `+` and `!`', function() { + forEach(['-', '+', '!'], function(operator) { + expect(createAst(operator + 'foo')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'UnaryExpression', + operator: operator, + prefix: true, + argument: { type: 'Identifier', name: 'foo' } + } + } + ] + } + ); + }); + }); + + + it('should handle all unary operators with the same precedence', function() { + forEach([['+', '-', '!'], ['-', '!', '+'], ['!', '+', '-']], function(operators) { + expect(createAst(operators.join('') + 'foo')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'UnaryExpression', + operator: operators[0], + prefix: true, + argument: { + type: 'UnaryExpression', + operator: operators[1], + prefix: true, + argument: { + type: 'UnaryExpression', + operator: operators[2], + prefix: true, + argument: { type: 'Identifier', name: 'foo' } + } + } + } + } + ] + } + ); + }); + }); + + + it('should be able to understand binary operators', function() { + forEach(['*', '/', '%', '+', '-', '<', '>', '<=', '>=', '==','!=','===','!=='], function(operator) { + expect(createAst('foo' + operator + 'bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'BinaryExpression', + operator: operator, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + } + } + ] + } + ); + }); + }); + + + it('should associate binary operators with the same precendence left-to-right', function() { + var operatorsByPrecedence = [['*', '/', '%'], ['+', '-'], ['<', '>', '<=', '>='], ['==','!=','===','!==']]; + forEach(operatorsByPrecedence, function(operators) { + forEach(operators, function(op1) { + forEach(operators, function(op2) { + expect(createAst('foo' + op1 + 'bar' + op2 + 'baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'BinaryExpression', + operator: op2, + left: { + type: 'BinaryExpression', + operator: op1, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + right: { type: 'Identifier', name: 'baz' } + } + } + ] + } + ); + }); + }); + }); + }); + + + it('should give higher prcedence to member calls than to unary expressions', function() { + forEach(['!', '+', '-'], function(operator) { + expect(createAst(operator + 'foo()')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'UnaryExpression', + operator: operator, + prefix: true, + argument: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo' }, + arguments: [] + } + } + } + ] + } + ); + expect(createAst(operator + 'foo.bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'UnaryExpression', + operator: operator, + prefix: true, + argument: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: false + } + } + } + ] + } + ); + expect(createAst(operator + 'foo[bar]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'UnaryExpression', + operator: operator, + prefix: true, + argument: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: true + } + } + } + ] + } + ); + }); + }); + + + it('should give higher precedence to unary operators over multiplicative operators', function() { + forEach(['!', '+', '-'], function(op1) { + forEach(['*', '/', '%'], function(op2) { + expect(createAst(op1 + 'foo' + op2 + op1 + 'bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'BinaryExpression', + operator: op2, + left: { + type: 'UnaryExpression', + operator: op1, + prefix: true, + argument: { type: 'Identifier', name: 'foo' } + }, + right: { + type: 'UnaryExpression', + operator: op1, + prefix: true, + argument: { type: 'Identifier', name: 'bar' } + } + } + } + ] + } + ); + }); + }); + }); + + + it('should give binary operators their right precedence', function() { + var operatorsByPrecedence = [['*', '/', '%'], ['+', '-'], ['<', '>', '<=', '>='], ['==','!=','===','!==']]; + for (var i = 0; i < operatorsByPrecedence.length - 1; ++i) { + forEach(operatorsByPrecedence[i], function(op1) { + forEach(operatorsByPrecedence[i + 1], function(op2) { + expect(createAst('foo' + op1 + 'bar' + op2 + 'baz' + op1 + 'man')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'BinaryExpression', + operator: op2, + left: { + type: 'BinaryExpression', + operator: op1, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + right: { + type: 'BinaryExpression', + operator: op1, + left: { type: 'Identifier', name: 'baz' }, + right: { type: 'Identifier', name: 'man' } + } + } + } + ] + } + ); + }); + }); + } + }); + + + + it('should understand logical operators', function() { + forEach(['||', '&&'], function(operator) { + expect(createAst('foo' + operator + 'bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'LogicalExpression', + operator: operator, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + } + } + ] + } + ); + }); + }); + + + it('should associate logical operators left-to-right', function() { + forEach(['||', '&&'], function(op) { + expect(createAst('foo' + op + 'bar' + op + 'baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'LogicalExpression', + operator: op, + left: { + type: 'LogicalExpression', + operator: op, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + right: { type: 'Identifier', name: 'baz' } + } + } + ] + } + ); + }); + }); + + + + it('should understand ternary operators', function() { + expect(createAst('foo?bar:baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'foo' }, + alternate: { type: 'Identifier', name: 'bar' }, + consequent: { type: 'Identifier', name: 'baz' } + } + } + ] + } + ); + }); + + + it('should associate the conditional operator right-to-left', function() { + expect(createAst('foo0?foo1:foo2?bar0?bar1:bar2:man0?man1:man2')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'foo0' }, + alternate: { type: 'Identifier', name: 'foo1' }, + consequent: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'foo2' }, + alternate: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'bar0' }, + alternate: { type: 'Identifier', name: 'bar1' }, + consequent: { type: 'Identifier', name: 'bar2' } + }, + consequent: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'man0' }, + alternate: { type: 'Identifier', name: 'man1' }, + consequent: { type: 'Identifier', name: 'man2' } + } + } + } + } + ] + } + ); + }); + + + it('should understand assignment operator', function() { + // Currently, only `=` is supported + expect(createAst('foo=bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' }, + operator: '=' + } + } + ] + } + ); + }); + + + it('should associate assignments right-to-left', function() { + // Currently, only `=` is supported + expect(createAst('foo=bar=man')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'bar' }, + right: { type: 'Identifier', name: 'man' }, + operator: '=' + }, + operator: '=' + } + } + ] + } + ); + }); + + + it('should give higher precedence to equality than to the logical `and` operator', function() { + forEach(['==','!=','===','!=='], function(operator) { + expect(createAst('foo' + operator + 'bar && man' + operator + 'shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'LogicalExpression', + operator: '&&', + left: { + type: 'BinaryExpression', + operator: operator, + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + right: { + type: 'BinaryExpression', + operator: operator, + left: { type: 'Identifier', name: 'man' }, + right: { type: 'Identifier', name: 'shell' } + } + } + } + ] + } + ); + }); + }); + + + it('should give higher precedence to logical `and` than to logical `or`', function() { + expect(createAst('foo&&bar||man&&shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'LogicalExpression', + operator: '||', + left: { + type: 'LogicalExpression', + operator: '&&', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + right: { + type: 'LogicalExpression', + operator: '&&', + left: { type: 'Identifier', name: 'man' }, + right: { type: 'Identifier', name: 'shell' } + } + } + } + ] + } + ); + }); + + + + it('should give higher precedence to the logical `or` than to the conditional operator', function() { + expect(createAst('foo||bar?man:shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ConditionalExpression', + test: { + type: 'LogicalExpression', + operator: '||', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + alternate: { type: 'Identifier', name: 'man' }, + consequent: { type: 'Identifier', name: 'shell' } + } + } + ] + } + ); + }); + + + it('should give higher precedence to the conditional operator than to assignment operators', function() { + expect(createAst('foo=bar?man:shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { + type: 'ConditionalExpression', + test: { type: 'Identifier', name: 'bar' }, + alternate: { type: 'Identifier', name: 'man' }, + consequent: { type: 'Identifier', name: 'shell' } + }, + operator: '=' + } + } + ] + } + ); + }); + + + it('should understand array literals', function() { + expect(createAst('[]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [] + } + } + ] + } + ); + expect(createAst('[foo]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [ + { type: 'Identifier', name: 'foo' } + ] + } + } + ] + } + ); + expect(createAst('[foo,]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [ + { type: 'Identifier', name: 'foo' } + ] + } + } + ] + } + ); + expect(createAst('[foo,bar,man,shell]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [ + { type: 'Identifier', name: 'foo' }, + { type: 'Identifier', name: 'bar' }, + { type: 'Identifier', name: 'man' }, + { type: 'Identifier', name: 'shell' } + ] + } + } + ] + } + ); + expect(createAst('[foo,bar,man,shell,]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [ + { type: 'Identifier', name: 'foo' }, + { type: 'Identifier', name: 'bar' }, + { type: 'Identifier', name: 'man' }, + { type: 'Identifier', name: 'shell' } + ] + } + } + ] + } + ); + }); + + + it('should understand objects', function() { + expect(createAst('{}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [] + } + } + ] + } + ); + expect(createAst('{foo: bar}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'foo' }, + value: { type: 'Identifier', name: 'bar' } + } + ] + } + } + ] + } + ); + expect(createAst('{foo: bar,}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'foo' }, + value: { type: 'Identifier', name: 'bar' } + } + ] + } + } + ] + } + ); + expect(createAst('{foo: bar, "man": "shell", 42: 23}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'foo' }, + value: { type: 'Identifier', name: 'bar' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Literal', value: 'man' }, + value: { type: 'Literal', value: 'shell' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Literal', value: 42 }, + value: { type: 'Literal', value: 23 } + } + ] + } + } + ] + } + ); + expect(createAst('{foo: bar, "man": "shell", 42: 23,}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'foo' }, + value: { type: 'Identifier', name: 'bar' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Literal', value: 'man' }, + value: { type: 'Literal', value: 'shell' } + }, + { + type: 'Property', + kind: 'init', + key: { type: 'Literal', value: 42 }, + value: { type: 'Literal', value: 23 } + } + ] + } + } + ] + } + ); + }); + + + it('should understand multiple expressions', function() { + expect(createAst('foo = bar; man = shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' }, + operator: '=' + } + }, + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'man' }, + right: { type: 'Identifier', name: 'shell' }, + operator: '=' + } + } + ] + } + ); + }); + + + // This is non-standard syntax + it('should understand filters', function() { + expect(createAst('foo | bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'bar'}, + arguments: [ + { type: 'Identifier', name: 'foo' } + ], + filter: true + } + } + ] + } + ); + }); + + + it('should understand filters with extra parameters', function() { + expect(createAst('foo | bar:baz')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'bar'}, + arguments: [ + { type: 'Identifier', name: 'foo' }, + { type: 'Identifier', name: 'baz' } + ], + filter: true + } + } + ] + } + ); + }); + + + it('should associate filters right-to-left', function() { + expect(createAst('foo | bar:man | shell')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'shell' }, + arguments: [ + { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'bar' }, + arguments: [ + { type: 'Identifier', name: 'foo' }, + { type: 'Identifier', name: 'man' } + ], + filter: true + } + ], + filter: true + } + } + ] + } + ); + }); + + it('should give higher precedence to assignments over filters', function() { + expect(createAst('foo=bar | man')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'man' }, + arguments: [ + { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' }, + operator: '=' + } + ], + filter: true + } + } + ] + } + ); + }); + + it('should accept expression as filters parameters', function() { + expect(createAst('foo | bar:baz=man')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'bar' }, + arguments: [ + { type: 'Identifier', name: 'foo' }, + { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'baz' }, + right: { type: 'Identifier', name: 'man' }, + operator: '=' + } + ], + filter: true + } + } + ] + } + ); + }); + + it('should accept expression as computer members', function() { + expect(createAst('foo[a = 1]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'a' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + }, + computed: true + } + } + ] + } + ); + }); + + it('should accept expression in function arguments', function() { + expect(createAst('foo(a = 1)')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'foo' }, + arguments: [ + { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'a' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + } + ] + } + } + ] + } + ); + }); + + it('should accept expression as part of ternary operators', function() { + expect(createAst('foo || bar ? man = 1 : shell = 1')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ConditionalExpression', + test: { + type: 'LogicalExpression', + operator: '||', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + alternate: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'man' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + }, + consequent: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'shell' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + } + } + } + ] + } + ); + }); + + it('should accept expression as part of array literals', function() { + expect(createAst('[foo = 1]')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: [ + { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + } + ] + } + } + ] + } + ); + }); + + it('should accept expression as part of object literals', function() { + expect(createAst('{foo: bar = 1}')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + kind: 'init', + key: { type: 'Identifier', name: 'foo' }, + value: { + type: 'AssignmentExpression', + left: { type: 'Identifier', name: 'bar' }, + right: { type: 'Literal', value: 1 }, + operator: '=' + } + } + ] + } + } + ] + } + ); + }); + + it('should be possible to use parenthesis to indicate precedence', function() { + expect(createAst('(foo + bar).man')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { + type: 'BinaryExpression', + operator: '+', + left: { type: 'Identifier', name: 'foo' }, + right: { type: 'Identifier', name: 'bar' } + }, + property: { type: 'Identifier', name: 'man' }, + computed: false + } + } + ] + } + ); + }); + + it('should skip empty expressions', function() { + expect(createAst('foo;;;;bar')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: 'foo' } + }, + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: 'bar' } + } + ] + } + ); + expect(createAst(';foo')).toEqual( + { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: 'foo' } + } + ] + } + ); + expect(createAst('foo;')).toEqual({ + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: 'foo' } + } + ] + }); + expect(createAst(';;;;')).toEqual({type: 'Program', body: []}); + expect(createAst('')).toEqual({type: 'Program', body: []}); + }); + }); + var $filterProvider, scope; beforeEach(module(['$filterProvider', function(filterProvider) { @@ -257,6 +1701,21 @@ describe('parser', function() { expect(scope.$eval("1/2*3")).toEqual(1 / 2 * 3); }); + it('should parse unary', function() { + expect(scope.$eval("+1")).toEqual(+1); + expect(scope.$eval("-1")).toEqual(-1); + expect(scope.$eval("+'1'")).toEqual(+'1'); + expect(scope.$eval("-'1'")).toEqual(-'1'); + expect(scope.$eval("+undefined")).toEqual(0); + expect(scope.$eval("-undefined")).toEqual(0); + expect(scope.$eval("+null")).toEqual(+null); + expect(scope.$eval("-null")).toEqual(-null); + expect(scope.$eval("+false")).toEqual(+false); + expect(scope.$eval("-false")).toEqual(-false); + expect(scope.$eval("+true")).toEqual(+true); + expect(scope.$eval("-true")).toEqual(-true); + }); + it('should parse comparison', function() { /* jshint -W041 */ expect(scope.$eval("false")).toBeFalsy(); @@ -439,6 +1898,16 @@ describe('parser', function() { expect(scope.$eval('b')).toBeUndefined(); expect(scope.$eval('a.x')).toBeUndefined(); expect(scope.$eval('a.b.c.d')).toBeUndefined(); + scope.a = undefined; + expect(scope.$eval('a - b')).toBe(0); + expect(scope.$eval('a + b')).toBe(undefined); + scope.a = 0; + expect(scope.$eval('a - b')).toBe(0); + expect(scope.$eval('a + b')).toBe(0); + scope.a = undefined; + scope.b = 0; + expect(scope.$eval('a - b')).toBe(0); + expect(scope.$eval('a + b')).toBe(0); }); it('should support property names that collide with native object properties', function() { @@ -756,7 +2225,6 @@ describe('parser', function() { }); it('should NOT allow access to Function constructor in getter', function() { - expect(function() { scope.$eval('{}.toString.constructor("alert(1)")'); }).toThrowMinErr( @@ -771,7 +2239,7 @@ describe('parser', function() { scope.$eval('{}.toString.constructor.a = 1'); }).toThrowMinErr( '$parse', 'isecfn','Referencing Function in Angular expressions is disallowed! ' + - 'Expression: toString.constructor.a'); + 'Expression: {}.toString.constructor.a = 1'); expect(function() { scope.$eval('{}.toString["constructor"]["constructor"] = 1'); @@ -839,18 +2307,26 @@ describe('parser', function() { }).toThrow(); }); - it('should NOT allow access to Function constructor that has been aliased', function() { + it('should NOT allow access to Function constructor that has been aliased in getters', function() { scope.foo = { "bar": Function }; expect(function() { scope.$eval('foo["bar"]'); }).toThrowMinErr( '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + 'Expression: foo["bar"]'); + }); + it('should NOT allow access to Function constructor that has been aliased in setters', function() { + scope.foo = { "bar": Function }; + expect(function() { + scope.$eval('foo["bar"] = 1'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: foo["bar"] = 1'); }); describe('expensiveChecks', function() { - it('should block access to window object even when aliased', inject(function($parse, $window) { + it('should block access to window object even when aliased in getters', inject(function($parse, $window) { scope.foo = {w: $window}; // This isn't blocked for performance. expect(scope.$eval($parse('foo.w'))).toBe($window); @@ -861,7 +2337,23 @@ describe('parser', function() { }).toThrowMinErr( '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + 'Expression: foo.w'); + })); + it('should block access to window object even when aliased in setters', inject(function($parse, $window) { + scope.foo = {w: $window}; + // This is blocked as it points to `window`. + expect(function() { + expect(scope.$eval($parse('foo.w = 1'))).toBe($window); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + + 'Expression: foo.w = 1'); + // Event handlers use the more expensive path for better protection since they expose + // the $event object on the scope. + expect(function() { + scope.$eval($parse('foo.w = 1', null, true)); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + + 'Expression: foo.w = 1'); })); }); }); @@ -918,7 +2410,7 @@ describe('parser', function() { describe('Object constructor', function() { - it('should NOT allow access to Object constructor that has been aliased', function() { + it('should NOT allow access to Object constructor that has been aliased in getters', function() { scope.foo = { "bar": Object }; expect(function() { @@ -933,6 +2425,22 @@ describe('parser', function() { '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + 'Expression: foo["bar"]["keys"](foo)'); }); + + it('should NOT allow access to Object constructor that has been aliased in setters', function() { + scope.foo = { "bar": Object }; + + expect(function() { + scope.$eval('foo.bar.keys(foo).bar = 1'); + }).toThrowMinErr( + '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + + 'Expression: foo.bar.keys(foo).bar = 1'); + + expect(function() { + scope.$eval('foo["bar"]["keys"](foo).bar = 1'); + }).toThrowMinErr( + '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + + 'Expression: foo["bar"]["keys"](foo).bar = 1'); + }); }); describe('Window and $element/node', function() { @@ -949,6 +2457,16 @@ describe('parser', function() { }).toThrowMinErr( '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + 'disallowed! Expression: wrap["d"]'); + expect(function() { + scope.$eval('wrap["w"] = 1', scope); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + + 'disallowed! Expression: wrap["w"] = 1'); + expect(function() { + scope.$eval('wrap["d"] = 1', scope); + }).toThrowMinErr( + '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + + 'disallowed! Expression: wrap["d"] = 1'); })); it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) { @@ -1106,6 +2624,9 @@ describe('parser', function() { }); it('should NOT allow access to __proto__', function() { + expect(function() { + scope.$eval('__proto__'); + }).toThrowMinErr('$parse', 'isecfld'); expect(function() { scope.$eval('{}.__proto__'); }).toThrowMinErr('$parse', 'isecfld'); @@ -1345,6 +2866,13 @@ describe('parser', function() { })); describe('literal expressions', function() { + it('should mark an empty expressions as literal', inject(function($parse) { + expect($parse('').literal).toBe(true); + expect($parse(' ').literal).toBe(true); + expect($parse('::').literal).toBe(true); + expect($parse(':: ').literal).toBe(true); + })); + it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) { var fn = $parse('::{foo: foo, bar: bar}'); $rootScope.$watch(fn, function(value) { log(value); }, true); @@ -1516,6 +3044,7 @@ describe('parser', function() { return v; } scope.$watch($parse("a", interceptor)); + scope.$watch($parse("a + b", interceptor)); scope.a = scope.b = 0; scope.$digest(); expect(called).toBe(true); @@ -1612,10 +3141,11 @@ describe('parser', function() { var filterCalls = 0; $filterProvider.register('foo', valueFn(function(input) { filterCalls++; + expect(input instanceof Date).toBe(true); return input; })); - var parsed = $parse('date | foo'); + var parsed = $parse('date | foo:a'); var date = scope.date = new Date(); var watcherCalls = 0; @@ -1638,10 +3168,11 @@ describe('parser', function() { var filterCalls = 0; $filterProvider.register('foo', valueFn(function(input) { filterCalls++; + expect(input instanceof Date).toBe(true); return input; })); - var parsed = $parse('date | foo'); + var parsed = $parse('date | foo:a'); var date = scope.date = new Date(); var watcherCalls = 0; @@ -1683,6 +3214,68 @@ describe('parser', function() { scope.$digest(); expect(called).toBe(true); })); + + it('should continue with the evaluation of the expression without invoking computed parts', + inject(function($parse) { + var value = 'foo'; + var spy = jasmine.createSpy(); + + spy.andCallFake(function() { return value; }); + scope.foo = spy; + scope.$watch("foo() | uppercase"); + scope.$digest(); + expect(spy.calls.length).toEqual(2); + scope.$digest(); + expect(spy.calls.length).toEqual(3); + value = 'bar'; + scope.$digest(); + expect(spy.calls.length).toEqual(5); + })); + + it('should invoke all statements in multi-statement expressions', inject(function($parse) { + var lastVal = NaN; + var listener = function(val) { lastVal = val; }; + + scope.setBarToOne = false; + scope.bar = 0; + scope.two = 2; + scope.foo = function() { if (scope.setBarToOne) scope.bar = 1; }; + scope.$watch("foo(); bar + two", listener); + + scope.$digest(); + expect(lastVal).toBe(2); + + scope.bar = 2; + scope.$digest(); + expect(lastVal).toBe(4); + + scope.setBarToOne = true; + scope.$digest(); + expect(lastVal).toBe(3); + })); + + it('should watch the left side of assignments', inject(function($parse) { + var lastVal = NaN; + var listener = function(val) { lastVal = val; }; + + var objA = {}; + var objB = {}; + + scope.$watch("curObj.value = input", noop); + + scope.curObj = objA; + scope.input = 1; + scope.$digest(); + expect(objA.value).toBe(scope.input); + + scope.curObj = objB; + scope.$digest(); + expect(objB.value).toBe(scope.input); + + scope.input = 2; + scope.$digest(); + expect(objB.value).toBe(scope.input); + })); }); describe('locals', function() { @@ -1704,6 +3297,23 @@ describe('parser', function() { expect($parse('a[0][0].b')({a: [[{b: 'scope'}]]}, {b: 'locals'})).toBe('scope'); expect($parse('a[0].b.c')({a: [{b: {c: 'scope'}}] }, {b: {c: 'locals'} })).toBe('scope'); })); + + it('should assign directly to locals when the local property exists', inject(function($parse) { + var s = {}, l = {}; + + $parse("a = 1")(s, l); + expect(s.a).toBe(1); + expect(l.a).toBeUndefined(); + + l.a = 2; + $parse("a = 0")(s, l); + expect(s.a).toBe(1); + expect(l.a).toBe(0); + + $parse("toString = 1")(s, l); + expect(isFunction(s.toString)).toBe(true); + expect(l.toString).toBe(1); + })); }); describe('literal', function() { @@ -1736,6 +3346,13 @@ describe('parser', function() { }); describe('constant', function() { + it('should mark an empty expressions as constant', inject(function($parse) { + expect($parse('').constant).toBe(true); + expect($parse(' ').constant).toBe(true); + expect($parse('::').constant).toBe(true); + expect($parse(':: ').constant).toBe(true); + })); + it('should mark scalar value expressions as constant', inject(function($parse) { expect($parse('12.3').constant).toBe(true); expect($parse('"string"').constant).toBe(true);