From c6a09eb755d2672a9748fa08965bd4f31a491412 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sun, 23 Nov 2014 23:08:33 +0100 Subject: [PATCH 01/20] refactor($parse): new and more performant $parse Change the way parse works from the old mechanism to a multiple stages parsing and code generation. The new parse is a four stages parsing * Lexer * AST building * AST processing * Cacheing, one-time binding and `$watch` optimizations The Lexer phase remains unchanged. AST building phase follows Mozilla Parse API [1] and generates an AST that is compatible. The only exception was needed for `filters` as JavaScript does not support filters, in this case, a filter is transformed into a `CallExpression` that has an extra property named `filter` with the value of `true`. The AST processing phase transforms the AST into a function that can be executed to evaluate the expression. The logic for expressions remains unchanged. The AST processing phase works in two different ways depending if csp is enabled or disabled. If csp is enabled, the processing phase returns pre-generated function that interpret specific parts of the AST. When csp is disabled, then the entire expression is compiled into a single function that is later evaluated using `Function`. In both cases, the returning function has the properties `constant`, `literal` and `inputs` as in the previous implementation. These are used in the next phase to perform different optimizations. The cacheing, one-time binding and `$watch` optimizations phase remains mostly unchanged. [1] https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API --- src/ng/parse.js | 1725 ++++++++++++++++++++++++++++-------------- src/ng/rootScope.js | 6 +- test/ng/parseSpec.js | 1451 ++++++++++++++++++++++++++++++++++- 3 files changed, 2618 insertions(+), 564 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 09b751d3bb6d..072c048b80b3 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,999 @@ Parser.prototype = { return false; }, - consume: function(e1) { - if (this.tokens.length === 0) { - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + + /* `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 } + } +}; + +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; +} + +function findConstantAndWatchExpressions(ast, $filter) { + var allConstants; + var argsToWatch; + switch (ast.type) { + 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; + 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.right.toWatch; + break; + case AST.ArrayExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.elements, function(expr) { + findConstantAndWatchExpressions(expr, $filter); + allConstants = allConstants && 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; + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ThisExpression: + ast.constant = false; + ast.toWatch = []; + break; + } +} - var token = this.expect(e1); - if (!token) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); +function useInputs(body) { + if (!body.length) return false; + var lastExpression = body[body.length - 1]; + if (lastExpression.expression.toWatch.length !== 1) return true; + return lastExpression.expression.toWatch[0] !== lastExpression.expression; +} + +function isAssignable(ast) { + return ast.type === AST.Identifier || ast.type === AST.MemberExpression; +} + +function ASTCompiler(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} + +ASTCompiler.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.state = { + nextId: 0, + filters: {}, + expensiveChecks: expensiveChecks, + closure: {vars: ['clean'], fns: {}}, + fn: {vars: [], body: [], own: {}}, + assign: {vars: [], body: [], own: {}} + }; + var lastExpression; + var i; + for (i = 0; i < ast.body.length; ++i) { + findConstantAndWatchExpressions(ast.body[i].expression, this.$filter); } - return token; + var toWatch = useInputs(ast.body) ? ast.body[ast.body.length - 1].expression.toWatch : []; + forEach(toWatch, function(watch, key) { + var fnKey = 'fn' + key; + self.state.computing = 'closure'; + watch.fixedId = self.nextId(); + watch.skipClean = true; + self.state.closure.fns[fnKey] = self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state.computing = fnKey; + self.recurse(watch); + self.assign('clean', true); + self.return(watch.fixedId); + watch.skipClean = false; + }); + this.state.computing = 'fn'; + for (i = 0; i < ast.body.length; ++i) { + if (lastExpression) this.current().body.push(lastExpression, ';'); + this.recurse(ast.body[i].expression, undefined, undefined, function(expr) { lastExpression = expr; }); + } + this.assign('clean', false); + if (lastExpression) this.return(lastExpression); + var extra = ''; + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + this.state.computing = 'assign'; + var result = this.nextId(); + this.recurse({type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}, result); + extra = 'fn.assign=function(s,v,l){' + + this.varsPrefix('assign') + + this.body('assign') + + '};'; + } + 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 woraround for this until we do a better job at only removing the prefix only when we should. + '"' + this.USE + ' ' + this.STRICT + '";\n' + + this.filterPrefix() + + 'return function(){' + + this.varsPrefix('closure') + + 'var fn = function(s,l){' + + this.varsPrefix('fn') + + this.body('fn') + + '};' + + extra + + this.watchFns() + + 'fn.literal=literal;fn.constant=constant;' + + 'return fn;' + + '};'; + + var isLiteral = 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); + var isConstant = ast.body.length === 1 && ast.body[0].expression.constant; + /* jshint -W054 */ + var fn = (new Function('$filter', + 'ensureSafeMemberName', + 'ensureSafeObject', + 'ensureSafeFunction', + 'isPossiblyDangerousMemberName', + 'ifDefined', + 'plus', + 'text', + 'literal', + 'constant', + fnString))( + this.$filter, + ensureSafeMemberName, + ensureSafeObject, + ensureSafeFunction, + isPossiblyDangerousMemberName, + ifDefined, + plusFn, + expression, + isLiteral, + isConstant); + /* jshint +W054 */ + this.state = undefined; + return fn; }, - unaryFn: function(op, right) { - var fn = OPERATORS[op]; - return extend(function $parseUnaryFn(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant, - inputs: [right] + USE: 'use', + + STRICT: 'strict', + + watchFns: function() { + var result = []; + var fns = []; + var self = this; + forEach(this.state.closure.fns, function(_, name) { + fns.push(name); + result.push( + 'var ' + name + ' = function(s,l){' + + self.varsPrefix(name) + + self.body(name) + + '};'); }); + if (fns.length) { + result.push('fn.inputs=[' + fns.join(',') + '];'); + } + return result.join(''); }, - 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] + filterPrefix: function() { + var parts = []; + var checks = []; + 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 ''; }, - identifier: function() { - var id = this.consume().text; - - //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; - } - - return getterFn(id, this.options, this.text); + varsPrefix: function(section) { + return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; }, - constant: function() { - var value = this.consume().value; - - return extend(function $parseConstant() { - return value; - }, { - constant: true, - literal: true - }); + body: function(section) { + return this.state[section].body.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); + recurse: function(ast, intoId, nameId, recursionFn, create) { + var left, right, self = this, args, expression; + recursionFn = recursionFn || noop; + switch (ast.type) { + 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 + '(' + right + ')'; + 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(intoId, left, right); + } else { + if (ast.operator === '-') { + left = this.ifDefined(left, 0); + right = this.ifDefined(right, 0); + } + expression = '(' + left + ')' + ast.operator + '(' + right + ')'; + } + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.LogicalExpression: + intoId = intoId || this.nextId(); + this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + self.recurse(ast.left, intoId); + self.if(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); + recursionFn(intoId); + self.assign(ast.fixedId, intoId); + }, function() { + self.assign(intoId, ast.fixedId); + }); + break; + case AST.ConditionalExpression: + intoId = intoId || this.nextId(); + this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + self.recurse(ast.test, intoId); + self.if(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); + recursionFn(intoId); + self.assign(ast.fixedId, intoId); + }, function() { + self.assign(intoId, ast.fixedId); + }); + break; + case AST.Identifier: + intoId = intoId || this.nextId(); + if (nameId) { + nameId.context = this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name), '?l:s'); + nameId.computed = false; + nameId.name = ast.name; + } + this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + ensureSafeMemberName(ast.name); + self.if(self.not(self.getHasOwnProperty('l', ast.name)), + function() { + self.if('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); + self.assign(ast.fixedId, intoId); + }, function() { + self.assign(intoId, ast.fixedId); + }); + break; + case AST.MemberExpression: + left = nameId && (nameId.context = this.nextId()) || this.nextId(); + intoId = intoId || this.nextId(); + this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + 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); + self.assign(ast.fixedId, intoId); + }, function() { + self.assign(intoId, ast.fixedId); + }); + break; + case AST.CallExpression: + intoId = intoId || this.nextId(); + this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + 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); } - return value; - }; + 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); + }); + }); + } + self.assign(ast.fixedId, intoId); + }, function() { + self.assign(intoId, ast.fixedId); + }); + 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; } }, - filterChain: function() { - var left = this.expression(); - var token; - while ((token = this.expect('|'))) { - left = this.filter(left); + 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 left; + return own[key]; }, - filter: function(inputFn) { - var fn = this.$filter(this.consume().text); - var argsFn; - var args; + assign: function(id) { + if (!id) return; + var body = this.current().body; + body.push(id, '='); + body.push.apply( + this.current().body, + Array.prototype.slice.call(arguments, 1)); + body.push(';'); + return id; + }, - if (this.peek(':')) { - argsFn = []; - args = []; // we can safely reuse the array - while (this.expect(':')) { - argsFn.push(this.expression()); - } + filter: function(filterName) { + if (!this.state.filters.hasOwnProperty(filterName)) { + this.state.filters[filterName] = this.nextId(true); } + return this.state.filters[filterName]; + }, - var inputs = [inputFn].concat(argsFn || []); - - return extend(function $parseFilter(self, locals) { - var input = inputFn(self, locals); - if (args) { - args[0] = input; - - var i = argsFn.length; - while (i--) { - args[i + 1] = argsFn[i](self, locals); - } - - return fn.apply(undefined, args); - } + ifDefined: function(id, defaultValue, nextId) { + var expression = 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; + this.assign(nextId, expression); + return expression; + }, - return fn(input); - }, { - constant: !fn.$stateful && inputs.every(isConstant), - inputs: !fn.$stateful && inputs - }); + plus: function(intoId, left, right) { + var expression = 'plus(' + left + ',' + right + ')'; + this.assign(intoId, expression); + return expression; }, - 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) { + var self = this; + return function() { + self.recurse(ast, intoId, nameId, recursionFn, create); + }; }, - objectIndex: function(obj) { - var expression = this.text; + lazyAssign: function() { + var self = this; + var args = arguments; + return function() { + self.assign.apply(self, args); + }; + }, - var indexFn = this.expression(); - this.consume(']'); + stringEscapeRegex: new RegExp('[^ 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; + forEach(ast.body, function(expression) { + findConstantAndWatchExpressions(expression.expression, self.$filter); + }); + 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; + }; + var toWatch = useInputs(ast.body) ? ast.body[ast.body.length - 1].expression.toWatch : []; + if (toWatch.length) { + var inputs = []; + forEach(toWatch, function(watch, key) { + inputs.push(self.recurse(watch)); + }); + fn.inputs = inputs; + } + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + var assign = this.recurse({type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}); + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); }; - }, - - // 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(',')); } - 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)); - } - return array; - }, { - literal: true, - constant: elementFns.every(isConstant), - inputs: elementFns - }); + fn.literal = 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); + fn.constant = ast.body.length === 1 && ast.body[0].expression.constant; + return valueFn(fn); }, - object: function() { - var keys = [], valueFns = []; - if (this.peekToken().text !== '}') { - do { - if (this.peek('}')) { - // Support trailing commas per ES5.1. - break; + recurse: function(ast, context, create) { + var left, right, self = this, args, expression; + switch (ast.type) { + case AST.Literal: + return function() { return context ? {context: undefined, name: undefined, value: ast.value} : ast.value; }; + 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); + return function(scope, locals, assign) { + var base = locals && locals.hasOwnProperty(ast.name) ? locals : scope; + if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { + ensureSafeObject(value, self.expression); + } + if (create && create !== 1 && base && !(ast.name in base)) { + base[ast.name] = {}; } - var token = this.consume(); - if (token.constant) { - keys.push(token.value); - } else if (token.identifier) { - keys.push(token.text); + var value = base ? base[ast.name] : undefined; + if (context) { + return {context: base, name: ast.name, value: value}; } else { - this.throwError("invalid key", token); + return value; } - this.consume(':'); - valueFns.push(this.expression()); - } while (this.expect(',')); + }; + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) ensureSafeMemberName(ast.property.name, self.expression); + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + function(scope, locals, assign) { + var lhs = left(scope, locals, assign); + var rhs; + var value; + if (lhs != null) { + rhs = right(scope, locals, assign); + ensureSafeMemberName(rhs, self.expression); + if (create && create !== 1 && lhs && !(rhs in lhs)) { + lhs[rhs] = {}; + } + value = lhs[rhs]; + ensureSafeObject(value, self.expression); + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + } : + function(scope, locals, assign) { + var lhs = left(scope, locals, assign); + if (create && create !== 1 && lhs && !(ast.property.name in lhs)) { + lhs[ast.property.name] = {}; + } + var value = lhs != null ? lhs[ast.property.name] : undefined; + if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { + ensureSafeObject(value, self.expression); + } + if (context) { + return {context: lhs, name: ast.property.name, value: value}; + } else { + return value; + } + }; + 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) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign)); + } + var value = right.apply(undefined, values); + return context ? {context: undefined, name: undefined, value: value} : value; + } : + function(scope, locals, assign) { + var rhs = right(scope, locals, assign); + 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), 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) { + var lhs = left(scope, locals, assign); + var rhs = right(scope, locals, assign); + ensureSafeObject(lhs.value); + 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) { + var value = []; + for (var i = 0; i < args.length; ++i) { + value.push(args[i](scope, locals, assign)); + } + 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) { + var value = {}; + for (var i = 0; i < args.length; ++i) { + value[args[i].key] = args[i].value(scope, locals, assign); + } + return context ? {value: value} : value; + }; + case AST.ThisExpression: + return function(scope) { + return context ? {value: scope} : scope; + }; + case AST.NGValueParameter: + return function(scope, locals, assign) { + 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) { + var arg = argument(scope, locals, assign); + if (arg != null) { + arg = +arg; } - return object; - }, { - literal: true, - constant: valueFns.every(isConstant), - inputs: valueFns - }); + return context ? {value: arg} : arg; + }; + }, + 'unary-': function(argument, context) { + return function(scope, locals, assign) { + var arg = argument(scope, locals, assign); + if (arg != null) { + arg = -arg; + } + return context ? {value: arg} : arg; + }; + }, + 'unary!': function(argument, context) { + return function(scope, locals, assign) { + var arg = !argument(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary+': function(left, right, context) { + return function(scope, locals, assign) { + var lhs = left(scope, locals, assign); + var rhs = right(scope, locals, assign); + var arg = plusFn(lhs, rhs); + return context ? {value: arg} : arg; + }; + }, + 'binary-': function(left, right, context) { + return function(scope, locals, assign) { + var lhs = left(scope, locals, assign); + var rhs = right(scope, locals, assign); + 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) { + var arg = left(scope, locals, assign) * right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary/': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) / right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary%': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) % right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary===': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) === right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary!==': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) !== right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary==': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) == right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary!=': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) != right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary<': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) < right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary>': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) > right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary<=': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) <= right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary>=': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) >= right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary&&': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) && right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'binary||': function(left, right, context) { + return function(scope, locals, assign) { + var arg = left(scope, locals, assign) || right(scope, locals, assign); + return context ? {value: arg} : arg; + }; + }, + 'ternary?:': function(test, alternate, consequent, context) { + return function(scope, locals, assign) { + var arg = test(scope, locals, assign) ? alternate(scope, locals, assign) : consequent(scope, locals, assign); + return context ? {value: arg} : arg; + }; } }; +/** + * @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 +1598,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 +1659,6 @@ function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); - - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { var $parseOptions = { csp: $sniffer.csp, @@ -1018,55 +1669,36 @@ 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; + var expressionFactory, 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]; - - if (!parsedExpression) { - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } + expressionFactory = cache[cacheKey]; + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + if (!expressionFactory) { 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 ? + expressionFactory = parser.parse(exp); + cache[cacheKey] = expressionFactory; + } + parsedExpression = expressionFactory(); + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (oneTime) { + parsedExpression.$$watchDelegate = parsedExpression.literal ? oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; - } else if (parsedExpression.inputs) { - parsedExpression.$$watchDelegate = inputsWatchDelegate; - } - - cache[cacheKey] = parsedExpression; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; } return addInterceptor(parsedExpression, interceptorFn); @@ -1078,21 +1710,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,10 +1735,8 @@ 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) { @@ -1134,7 +1749,7 @@ function $ParseProvider() { oldInputValue = newInputValue && getValueOf(newInputValue); } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } var oldInputValueOfValues = []; @@ -1157,7 +1772,7 @@ function $ParseProvider() { } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { 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..cbcc1617ef75 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) { @@ -771,7 +2215,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'); @@ -1266,11 +2710,6 @@ describe('parser', function() { }); describe('one-time binding', function() { - it('should always use the cache', inject(function($parse) { - expect($parse('foo')).toBe($parse('foo')); - expect($parse('::foo')).toBe($parse('::foo')); - })); - it('should not affect calling the parseFn directly', inject(function($parse, $rootScope) { var fn = $parse('::foo'); $rootScope.$watch(fn); From 3850637f0a92b7c29ec33d78810b4ff0f1958008 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Mon, 29 Dec 2014 21:01:36 +0100 Subject: [PATCH 02/20] refactor($parse): remove second closure Removed second closure for `inputs` and use `inputsWatchDelegate` to know when the computation is partial --- src/ng/parse.js | 61 ++++++++++++++++++++------------------------ test/ng/parseSpec.js | 5 ++++ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 072c048b80b3..f5374c7bf290 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -716,9 +716,9 @@ ASTCompiler.prototype = { nextId: 0, filters: {}, expensiveChecks: expensiveChecks, - closure: {vars: ['clean'], fns: {}}, fn: {vars: [], body: [], own: {}}, - assign: {vars: [], body: [], own: {}} + assign: {vars: [], body: [], own: {}}, + inputs: [] }; var lastExpression; var i; @@ -728,22 +728,19 @@ ASTCompiler.prototype = { var toWatch = useInputs(ast.body) ? ast.body[ast.body.length - 1].expression.toWatch : []; forEach(toWatch, function(watch, key) { var fnKey = 'fn' + key; - self.state.computing = 'closure'; - watch.fixedId = self.nextId(); - watch.skipClean = true; - self.state.closure.fns[fnKey] = self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state[fnKey] = {vars: [], body: [], own: {}}; self.state.computing = fnKey; - self.recurse(watch); - self.assign('clean', true); - self.return(watch.fixedId); - watch.skipClean = false; + var intoId = self.nextId(); + self.recurse(watch, intoId); + self.return(intoId); + self.state.inputs.push(fnKey); + watch.watchId = key; }); this.state.computing = 'fn'; for (i = 0; i < ast.body.length; ++i) { if (lastExpression) this.current().body.push(lastExpression, ';'); this.recurse(ast.body[i].expression, undefined, undefined, function(expr) { lastExpression = expr; }); } - this.assign('clean', false); if (lastExpression) this.return(lastExpression); var extra = ''; if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { @@ -757,20 +754,17 @@ ASTCompiler.prototype = { } 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 woraround for this until we do a better job at only removing the prefix only when we should. + // 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() + - 'return function(){' + - this.varsPrefix('closure') + - 'var fn = function(s,l){' + + 'var fn = function(s,l,i){' + this.varsPrefix('fn') + this.body('fn') + '};' + extra + this.watchFns() + 'fn.literal=literal;fn.constant=constant;' + - 'return fn;' + - '};'; + 'return fn;'; var isLiteral = ast.body.length === 1 && ( ast.body[0].expression.type === AST.Literal || @@ -810,10 +804,9 @@ ASTCompiler.prototype = { watchFns: function() { var result = []; - var fns = []; + var fns = this.state.inputs; var self = this; - forEach(this.state.closure.fns, function(_, name) { - fns.push(name); + forEach(fns, function(name) { result.push( 'var ' + name + ' = function(s,l){' + self.varsPrefix(name) + @@ -877,24 +870,24 @@ ASTCompiler.prototype = { break; case AST.LogicalExpression: intoId = intoId || this.nextId(); - this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + this.if(isDefined(ast.watchId) ? '!i' : true, function() { self.recurse(ast.left, intoId); self.if(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); recursionFn(intoId); self.assign(ast.fixedId, intoId); }, function() { - self.assign(intoId, ast.fixedId); + self.assign(intoId, self.computedMember('i', ast.watchId)); }); break; case AST.ConditionalExpression: intoId = intoId || this.nextId(); - this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + this.if(isDefined(ast.watchId) ? '!i' : true, function() { self.recurse(ast.test, intoId); self.if(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); recursionFn(intoId); self.assign(ast.fixedId, intoId); }, function() { - self.assign(intoId, ast.fixedId); + self.assign(intoId, self.computedMember('i', ast.watchId)); }); break; case AST.Identifier: @@ -904,7 +897,7 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + this.if(isDefined(ast.watchId) ? '!i' : true, function() { ensureSafeMemberName(ast.name); self.if(self.not(self.getHasOwnProperty('l', ast.name)), function() { @@ -924,13 +917,13 @@ ASTCompiler.prototype = { recursionFn(intoId); self.assign(ast.fixedId, intoId); }, function() { - self.assign(intoId, ast.fixedId); + self.assign(intoId, self.computedMember('i', ast.watchId)); }); break; case AST.MemberExpression: left = nameId && (nameId.context = this.nextId()) || this.nextId(); intoId = intoId || this.nextId(); - this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + this.if(isDefined(ast.watchId) ? '!i' : true, function() { self.recurse(ast.object, left, undefined, function() { self.if(self.notNull(left), function() { if (ast.computed) { @@ -966,12 +959,12 @@ ASTCompiler.prototype = { }, !!create); self.assign(ast.fixedId, intoId); }, function() { - self.assign(intoId, ast.fixedId); + self.assign(intoId, self.computedMember('i', ast.watchId)); }); break; case AST.CallExpression: intoId = intoId || this.nextId(); - this.if(ast.fixedId && !ast.skipClean ? '!clean' : true, function() { + this.if(isDefined(ast.watchId) ? '!i' : true, function() { if (ast.filter) { right = self.filter(ast.callee.name); args = []; @@ -1011,7 +1004,7 @@ ASTCompiler.prototype = { } self.assign(ast.fixedId, intoId); }, function() { - self.assign(intoId, ast.fixedId); + self.assign(intoId, self.computedMember('i', ast.watchId)); }); break; case AST.AssignmentExpression: @@ -1261,7 +1254,7 @@ ASTInterpreter.prototype = { ast.body[0].expression.type === AST.ArrayExpression || ast.body[0].expression.type === AST.ObjectExpression); fn.constant = ast.body.length === 1 && ast.body[0].expression.constant; - return valueFn(fn); + return fn; }, recurse: function(ast, context, create) { @@ -1691,7 +1684,7 @@ function $ParseProvider() { expressionFactory = parser.parse(exp); cache[cacheKey] = expressionFactory; } - parsedExpression = expressionFactory(); + parsedExpression = expressionFactory; if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { @@ -1745,7 +1738,7 @@ function $ParseProvider() { return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { - lastResult = parsedExpression(scope); + lastResult = parsedExpression(scope, undefined, [newInputValue]); oldInputValue = newInputValue && getValueOf(newInputValue); } return lastResult; @@ -1768,7 +1761,7 @@ function $ParseProvider() { } if (changed) { - lastResult = parsedExpression(scope); + lastResult = parsedExpression(scope, undefined, oldInputValueOfValues); } return lastResult; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index cbcc1617ef75..d4e3a736ce87 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2710,6 +2710,11 @@ describe('parser', function() { }); describe('one-time binding', function() { + it('should always use the cache', inject(function($parse) { + expect($parse('foo')).toBe($parse('foo')); + expect($parse('::foo')).toBe($parse('::foo')); + })); + it('should not affect calling the parseFn directly', inject(function($parse, $rootScope) { var fn = $parse('::foo'); $rootScope.$watch(fn); From 72a936383dfe4a4572fac06fd160eeadbd885179 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Tue, 30 Dec 2014 13:08:13 +0100 Subject: [PATCH 03/20] chore($parse): cleanup code Cleanup several odd parst of the code --- src/ng/parse.js | 157 ++++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 84 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index f5374c7bf290..cd969072e940 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -692,17 +692,35 @@ function findConstantAndWatchExpressions(ast, $filter) { } } -function useInputs(body) { - if (!body.length) return false; - var lastExpression = body[body.length - 1]; - if (lastExpression.expression.toWatch.length !== 1) return true; - return lastExpression.expression.toWatch[0] !== lastExpression.expression; +function getInputs(body) { + if (!body.length) return; + var lastExpression = body[body.length - 1].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 === 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.body.length === 1 && ast.body[0].expression.constant; +} + function ASTCompiler(astBuilder, $filter) { this.astBuilder = astBuilder; this.$filter = $filter; @@ -721,11 +739,24 @@ ASTCompiler.prototype = { inputs: [] }; var lastExpression; - var i; - for (i = 0; i < ast.body.length; ++i) { - findConstantAndWatchExpressions(ast.body[i].expression, this.$filter); + forEach(ast.body, function(expression) { + findConstantAndWatchExpressions(expression.expression, self.$filter); + }); + this.state.computing = 'fn'; + forEach(ast.body, function(expression) { + if (lastExpression) self.current().body.push(lastExpression, ';'); + self.recurse(expression.expression, undefined, undefined, function(expr) { lastExpression = expr; }); + }); + if (lastExpression) this.return(lastExpression); + var extra = ''; + var assignable; + 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 = useInputs(ast.body) ? ast.body[ast.body.length - 1].expression.toWatch : []; + var toWatch = getInputs(ast.body); forEach(toWatch, function(watch, key) { var fnKey = 'fn' + key; self.state[fnKey] = {vars: [], body: [], own: {}}; @@ -736,41 +767,16 @@ ASTCompiler.prototype = { self.state.inputs.push(fnKey); watch.watchId = key; }); - this.state.computing = 'fn'; - for (i = 0; i < ast.body.length; ++i) { - if (lastExpression) this.current().body.push(lastExpression, ';'); - this.recurse(ast.body[i].expression, undefined, undefined, function(expr) { lastExpression = expr; }); - } - if (lastExpression) this.return(lastExpression); - var extra = ''; - if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { - this.state.computing = 'assign'; - var result = this.nextId(); - this.recurse({type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}, result); - extra = 'fn.assign=function(s,v,l){' + - this.varsPrefix('assign') + - this.body('assign') + - '};'; - } 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 = function(s,l,i){' + - this.varsPrefix('fn') + - this.body('fn') + - '};' + + 'var fn=' + this.generateFunction('fn', 's,l,i') + extra + this.watchFns() + - 'fn.literal=literal;fn.constant=constant;' + 'return fn;'; - var isLiteral = 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); - var isConstant = ast.body.length === 1 && ast.body[0].expression.constant; /* jshint -W054 */ var fn = (new Function('$filter', 'ensureSafeMemberName', @@ -780,8 +786,6 @@ ASTCompiler.prototype = { 'ifDefined', 'plus', 'text', - 'literal', - 'constant', fnString))( this.$filter, ensureSafeMemberName, @@ -790,11 +794,11 @@ ASTCompiler.prototype = { isPossiblyDangerousMemberName, ifDefined, plusFn, - expression, - isLiteral, - isConstant); + expression); /* jshint +W054 */ this.state = undefined; + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); return fn; }, @@ -807,11 +811,7 @@ ASTCompiler.prototype = { var fns = this.state.inputs; var self = this; forEach(fns, function(name) { - result.push( - 'var ' + name + ' = function(s,l){' + - self.varsPrefix(name) + - self.body(name) + - '};'); + result.push('var ' + name + '=' + self.generateFunction(name, 's,l')); }); if (fns.length) { result.push('fn.inputs=[' + fns.join(',') + '];'); @@ -819,6 +819,13 @@ ASTCompiler.prototype = { return result.join(''); }, + generateFunction: function(name, params) { + return 'function(' + params + '){' + + this.varsPrefix(name) + + this.body(name) + + '};'; + }, + filterPrefix: function() { var parts = []; var checks = []; @@ -857,12 +864,10 @@ ASTCompiler.prototype = { 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(intoId, left, right); + expression = this.plus(left, right); + } else if (ast.operator === '=') { + expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); } else { - if (ast.operator === '-') { - left = this.ifDefined(left, 0); - right = this.ifDefined(right, 0); - } expression = '(' + left + ')' + ast.operator + '(' + right + ')'; } this.assign(intoId, expression); @@ -874,7 +879,6 @@ ASTCompiler.prototype = { self.recurse(ast.left, intoId); self.if(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); recursionFn(intoId); - self.assign(ast.fixedId, intoId); }, function() { self.assign(intoId, self.computedMember('i', ast.watchId)); }); @@ -885,7 +889,6 @@ ASTCompiler.prototype = { self.recurse(ast.test, intoId); self.if(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); recursionFn(intoId); - self.assign(ast.fixedId, intoId); }, function() { self.assign(intoId, self.computedMember('i', ast.watchId)); }); @@ -893,7 +896,7 @@ ASTCompiler.prototype = { case AST.Identifier: intoId = intoId || this.nextId(); if (nameId) { - nameId.context = this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name), '?l:s'); + nameId.context = this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); nameId.computed = false; nameId.name = ast.name; } @@ -915,7 +918,6 @@ ASTCompiler.prototype = { self.addEnsureSafeObject(intoId); } recursionFn(intoId); - self.assign(ast.fixedId, intoId); }, function() { self.assign(intoId, self.computedMember('i', ast.watchId)); }); @@ -957,7 +959,6 @@ ASTCompiler.prototype = { recursionFn(intoId); }); }, !!create); - self.assign(ast.fixedId, intoId); }, function() { self.assign(intoId, self.computedMember('i', ast.watchId)); }); @@ -1002,7 +1003,6 @@ ASTCompiler.prototype = { }); }); } - self.assign(ast.fixedId, intoId); }, function() { self.assign(intoId, self.computedMember('i', ast.watchId)); }); @@ -1040,7 +1040,8 @@ ASTCompiler.prototype = { self.recurse(property.value, undefined, undefined, function(expr) { args.push(self.escape( property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + ':' + expr); + ('' + property.key.value)) + + ':' + expr); }); }); expression = '{' + args.join(',') + '}'; @@ -1067,14 +1068,9 @@ ASTCompiler.prototype = { return own[key]; }, - assign: function(id) { + assign: function(id, value) { if (!id) return; - var body = this.current().body; - body.push(id, '='); - body.push.apply( - this.current().body, - Array.prototype.slice.call(arguments, 1)); - body.push(';'); + this.current().body.push(id, '=', value, ';'); return id; }, @@ -1085,16 +1081,12 @@ ASTCompiler.prototype = { return this.state.filters[filterName]; }, - ifDefined: function(id, defaultValue, nextId) { - var expression = 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; - this.assign(nextId, expression); - return expression; + ifDefined: function(id, defaultValue) { + return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; }, - plus: function(intoId, left, right) { - var expression = 'plus(' + left + ',' + right + ')'; - this.assign(intoId, expression); - return expression; + plus: function(left, right) { + return 'plus(' + left + ',' + right + ')'; }, 'return': function(id) { @@ -1169,11 +1161,10 @@ ASTCompiler.prototype = { }; }, - lazyAssign: function() { + lazyAssign: function(id, value) { var self = this; - var args = arguments; return function() { - self.assign.apply(self, args); + self.assign(id, value); }; }, @@ -1235,25 +1226,23 @@ ASTInterpreter.prototype = { }); return lastValue; }; - var toWatch = useInputs(ast.body) ? ast.body[ast.body.length - 1].expression.toWatch : []; - if (toWatch.length) { + var toWatch = getInputs(ast.body); + if (toWatch) { var inputs = []; forEach(toWatch, function(watch, key) { inputs.push(self.recurse(watch)); }); fn.inputs = inputs; } - if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { - var assign = this.recurse({type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}); + var assignable; + if ((assignable = assignableAST(ast))) { + var assign = this.recurse(assignable); fn.assign = function(scope, value, locals) { return assign(scope, locals, value); }; } - fn.literal = 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); - fn.constant = ast.body.length === 1 && ast.body[0].expression.constant; + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); return fn; }, From c439a1132e6b5ff257e37af038472a671f47097d Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 30 Dec 2014 01:05:23 -0800 Subject: [PATCH 04/20] refactor($parse): remove support for locals in input watchers Conflicts: src/ng/parse.js --- src/ng/parse.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index cd969072e940..22dc1ede02af 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -743,6 +743,7 @@ ASTCompiler.prototype = { findConstantAndWatchExpressions(expression.expression, self.$filter); }); this.state.computing = 'fn'; + this.stage = 'main'; forEach(ast.body, function(expression) { if (lastExpression) self.current().body.push(lastExpression, ';'); self.recurse(expression.expression, undefined, undefined, function(expr) { lastExpression = expr; }); @@ -750,6 +751,7 @@ ASTCompiler.prototype = { if (lastExpression) this.return(lastExpression); var extra = ''; var assignable; + this.stage = 'assign'; if ((assignable = assignableAST(ast))) { this.state.computing = 'assign'; var result = this.nextId(); @@ -757,6 +759,7 @@ ASTCompiler.prototype = { 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: {}}; @@ -796,7 +799,7 @@ ASTCompiler.prototype = { plusFn, expression); /* jshint +W054 */ - this.state = undefined; + this.state = this.stage = undefined; fn.literal = isLiteral(ast); fn.constant = isConstant(ast); return fn; @@ -896,13 +899,13 @@ ASTCompiler.prototype = { case AST.Identifier: intoId = intoId || this.nextId(); if (nameId) { - nameId.context = this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); + nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); nameId.computed = false; nameId.name = ast.name; } this.if(isDefined(ast.watchId) ? '!i' : true, function() { ensureSafeMemberName(ast.name); - self.if(self.not(self.getHasOwnProperty('l', ast.name)), + self.if(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if('s', function() { if (create && create !== 1) { From 5859d560ff7138ba3512bb4b87adff4c591639e4 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 30 Dec 2014 02:16:10 -0800 Subject: [PATCH 05/20] refactor($parse): remove scope null check on input watchers --- src/ng/parse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 22dc1ede02af..93040d190b22 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -907,7 +907,7 @@ ASTCompiler.prototype = { ensureSafeMemberName(ast.name); self.if(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { - self.if('s', function() { + self.if(self.stage === 'inputs' || 's', function() { if (create && create !== 1) { self.if( self.not(self.getHasOwnProperty('s', ast.name)), From 7d9cf9d04211160172e8892e3c1eb0b0332eed80 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Tue, 30 Dec 2014 13:26:38 +0100 Subject: [PATCH 06/20] chore($parse): remove unused variable Remove the use of `expressionFactory` as the second context was removed --- src/ng/parse.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 93040d190b22..d975fa4af55b 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1655,7 +1655,7 @@ function $ParseProvider() { }; return function $parse(exp, interceptorFn, expensiveChecks) { - var expressionFactory, parsedExpression, oneTime, cacheKey; + var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': @@ -1663,27 +1663,26 @@ function $ParseProvider() { cacheKey = exp; var cache = (expensiveChecks ? cacheExpensive : cacheDefault); - expressionFactory = cache[cacheKey]; + parsedExpression = cache[cacheKey]; if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { oneTime = true; exp = exp.substring(2); } - if (!expressionFactory) { + if (!parsedExpression) { var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); - expressionFactory = parser.parse(exp); - cache[cacheKey] = expressionFactory; - } - parsedExpression = expressionFactory; - if (parsedExpression.constant) { - parsedExpression.$$watchDelegate = constantWatchDelegate; - } else if (oneTime) { - parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; - } else if (parsedExpression.inputs) { - parsedExpression.$$watchDelegate = inputsWatchDelegate; + parsedExpression = parser.parse(exp); + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (oneTime) { + parsedExpression.$$watchDelegate = parsedExpression.literal ? + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; + } + cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); From f53c13ddc2b0cf17a1c2f65f49c0cf750d0ba3b0 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Tue, 30 Dec 2014 16:17:33 +0100 Subject: [PATCH 07/20] chore($parse): delegate the function bundling to the recursion function Delegate the function building to the recursion function --- src/ng/parse.js | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index d975fa4af55b..060ec072ab4b 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -606,6 +606,11 @@ function findConstantAndWatchExpressions(ast, $filter) { var allConstants; var argsToWatch; switch (ast.type) { + case AST.Program: + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter); + }); + break; case AST.Literal: ast.constant = true; ast.toWatch = []; @@ -738,17 +743,7 @@ ASTCompiler.prototype = { assign: {vars: [], body: [], own: {}}, inputs: [] }; - var lastExpression; - forEach(ast.body, function(expression) { - findConstantAndWatchExpressions(expression.expression, self.$filter); - }); - this.state.computing = 'fn'; - this.stage = 'main'; - forEach(ast.body, function(expression) { - if (lastExpression) self.current().body.push(lastExpression, ';'); - self.recurse(expression.expression, undefined, undefined, function(expr) { lastExpression = expr; }); - }); - if (lastExpression) this.return(lastExpression); + findConstantAndWatchExpressions(ast, self.$filter); var extra = ''; var assignable; this.stage = 'assign'; @@ -770,6 +765,9 @@ ASTCompiler.prototype = { 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. @@ -852,6 +850,16 @@ ASTCompiler.prototype = { var left, right, self = this, args, expression; recursionFn = recursionFn || noop; 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); @@ -1213,9 +1221,7 @@ ASTInterpreter.prototype = { var ast = this.astBuilder.ast(expression); this.expression = expression; this.expensiveChecks = expensiveChecks; - forEach(ast.body, function(expression) { - findConstantAndWatchExpressions(expression.expression, self.$filter); - }); + findConstantAndWatchExpressions(ast, self.$filter); var expressions = []; forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); @@ -1229,6 +1235,13 @@ ASTInterpreter.prototype = { }); return lastValue; }; + var assignable; + if ((assignable = assignableAST(ast))) { + var assign = this.recurse(assignable); + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); + }; + } var toWatch = getInputs(ast.body); if (toWatch) { var inputs = []; @@ -1237,13 +1250,6 @@ ASTInterpreter.prototype = { }); fn.inputs = inputs; } - var assignable; - if ((assignable = assignableAST(ast))) { - var assign = this.recurse(assignable); - fn.assign = function(scope, value, locals) { - return assign(scope, locals, value); - }; - } fn.literal = isLiteral(ast); fn.constant = isConstant(ast); return fn; From aba10b0dbb393abaab6c6eb31c2817da299bd6ae Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Wed, 31 Dec 2014 11:59:48 +0100 Subject: [PATCH 08/20] refactor($parse): implement resumed evaluations when csp is enabled Implements resumed evaluation of expressions when CSP is enabled --- src/ng/parse.js | 167 ++++++++++++++++++++++++------------------- test/ng/parseSpec.js | 17 +++++ 2 files changed, 111 insertions(+), 73 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 060ec072ab4b..7c169af33001 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -773,7 +773,7 @@ ASTCompiler.prototype = { // 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,i') + + 'var fn=' + this.generateFunction('fn', 's,l,a,i') + extra + this.watchFns() + 'return fn;'; @@ -1222,6 +1222,22 @@ ASTInterpreter.prototype = { 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)); @@ -1235,19 +1251,12 @@ ASTInterpreter.prototype = { }); return lastValue; }; - var assignable; - if ((assignable = assignableAST(ast))) { - var assign = this.recurse(assignable); + if (assign) { fn.assign = function(scope, value, locals) { return assign(scope, locals, value); }; } - var toWatch = getInputs(ast.body); - if (toWatch) { - var inputs = []; - forEach(toWatch, function(watch, key) { - inputs.push(self.recurse(watch)); - }); + if (inputs) { fn.inputs = inputs; } fn.literal = isLiteral(ast); @@ -1257,9 +1266,12 @@ ASTInterpreter.prototype = { recurse: function(ast, context, create) { var left, right, self = this, args, expression; + if (ast.input) { + return this.inputs(ast.input, ast.watchId); + } switch (ast.type) { case AST.Literal: - return function() { return context ? {context: undefined, name: undefined, value: ast.value} : ast.value; }; + return this.value(ast.value, context); case AST.UnaryExpression: right = this.recurse(ast.argument); return this['unary' + ast.operator](right, context); @@ -1280,7 +1292,7 @@ ASTInterpreter.prototype = { ); case AST.Identifier: ensureSafeMemberName(ast.name); - return function(scope, locals, assign) { + return function(scope, locals, assign, inputs) { var base = locals && locals.hasOwnProperty(ast.name) ? locals : scope; if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { ensureSafeObject(value, self.expression); @@ -1300,12 +1312,12 @@ ASTInterpreter.prototype = { if (!ast.computed) ensureSafeMemberName(ast.property.name, self.expression); if (ast.computed) right = this.recurse(ast.property); return ast.computed ? - function(scope, locals, assign) { - var lhs = left(scope, locals, assign); + function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); var rhs; var value; if (lhs != null) { - rhs = right(scope, locals, assign); + rhs = right(scope, locals, assign, inputs); ensureSafeMemberName(rhs, self.expression); if (create && create !== 1 && lhs && !(rhs in lhs)) { lhs[rhs] = {}; @@ -1319,8 +1331,8 @@ ASTInterpreter.prototype = { return value; } } : - function(scope, locals, assign) { - var lhs = left(scope, locals, assign); + function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); if (create && create !== 1 && lhs && !(ast.property.name in lhs)) { lhs[ast.property.name] = {}; } @@ -1342,23 +1354,23 @@ ASTInterpreter.prototype = { 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) { + function(scope, locals, assign, inputs) { var values = []; for (var i = 0; i < args.length; ++i) { - values.push(args[i](scope, locals, assign)); + values.push(args[i](scope, locals, assign, inputs)); } - var value = right.apply(undefined, values); + var value = right.apply(undefined, values, inputs); return context ? {context: undefined, name: undefined, value: value} : value; } : - function(scope, locals, assign) { - var rhs = right(scope, locals, assign); + 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), self.expression)); + values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); } value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); } @@ -1367,9 +1379,9 @@ ASTInterpreter.prototype = { case AST.AssignmentExpression: left = this.recurse(ast.left, true, 1); right = this.recurse(ast.right); - return function(scope, locals, assign) { - var lhs = left(scope, locals, assign); - var rhs = right(scope, locals, assign); + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); ensureSafeObject(lhs.value); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; @@ -1379,10 +1391,10 @@ ASTInterpreter.prototype = { forEach(ast.elements, function(expr) { args.push(self.recurse(expr)); }); - return function(scope, locals, assign) { + return function(scope, locals, assign, inputs) { var value = []; for (var i = 0; i < args.length; ++i) { - value.push(args[i](scope, locals, assign)); + value.push(args[i](scope, locals, assign, inputs)); } return context ? {value: value} : value; }; @@ -1395,10 +1407,10 @@ ASTInterpreter.prototype = { value: self.recurse(property.value) }); }); - return function(scope, locals, assign) { + 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); + value[args[i].key] = args[i].value(scope, locals, assign, inputs); } return context ? {value: value} : value; }; @@ -1407,15 +1419,15 @@ ASTInterpreter.prototype = { return context ? {value: scope} : scope; }; case AST.NGValueParameter: - return function(scope, locals, assign) { + return function(scope, locals, assign, inputs) { return context ? {value: assign} : assign; }; } }, 'unary+': function(argument, context) { - return function(scope, locals, assign) { - var arg = argument(scope, locals, assign); + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); if (arg != null) { arg = +arg; } @@ -1423,8 +1435,8 @@ ASTInterpreter.prototype = { }; }, 'unary-': function(argument, context) { - return function(scope, locals, assign) { - var arg = argument(scope, locals, assign); + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); if (arg != null) { arg = -arg; } @@ -1432,110 +1444,119 @@ ASTInterpreter.prototype = { }; }, 'unary!': function(argument, context) { - return function(scope, locals, assign) { - var arg = !argument(scope, locals, assign); + 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) { - var lhs = left(scope, locals, assign); - var rhs = right(scope, locals, assign); + 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) { - var lhs = left(scope, locals, assign); - var rhs = right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) * right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) / right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) % right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) === right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) !== right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) == right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) != right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) < right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) > right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) <= right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) >= right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) && right(scope, locals, assign); + 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) { - var arg = left(scope, locals, assign) || right(scope, locals, assign); + 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) { - var arg = test(scope, locals, assign) ? alternate(scope, locals, assign) : consequent(scope, locals, assign); + 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; }; + }, + inputs: function(input, watchId) { + return function(scope, value, locals, inputs) { + if (inputs) return inputs[watchId]; + return input(scope, value, locals); + }; } }; @@ -1735,7 +1756,7 @@ function $ParseProvider() { return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { - lastResult = parsedExpression(scope, undefined, [newInputValue]); + lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); oldInputValue = newInputValue && getValueOf(newInputValue); } return lastResult; @@ -1758,7 +1779,7 @@ function $ParseProvider() { } if (changed) { - lastResult = parsedExpression(scope, undefined, oldInputValueOfValues); + lastResult = parsedExpression(scope, undefined, undefined, oldInputValueOfValues); } return lastResult; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index d4e3a736ce87..1416aea0b9a0 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3127,6 +3127,23 @@ 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); + })); }); describe('locals', function() { From c7e044375944f6f7c757366a23fe2c2869e66280 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Thu, 1 Jan 2015 14:39:01 +0100 Subject: [PATCH 09/20] perf($parse): remove reference to the ast when csp is enabled Remove reference to the ast on the generated function when CSP is enabled --- src/ng/parse.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 7c169af33001..f854ce57b1dd 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1309,7 +1309,10 @@ ASTInterpreter.prototype = { }; case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); - if (!ast.computed) ensureSafeMemberName(ast.property.name, self.expression); + if (!ast.computed) { + ensureSafeMemberName(ast.property.name, self.expression); + right = ast.property.name; + } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? function(scope, locals, assign, inputs) { @@ -1333,15 +1336,15 @@ ASTInterpreter.prototype = { } : function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); - if (create && create !== 1 && lhs && !(ast.property.name in lhs)) { - lhs[ast.property.name] = {}; + if (create && create !== 1 && lhs && !(right in lhs)) { + lhs[right] = {}; } - var value = lhs != null ? lhs[ast.property.name] : undefined; - if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { + var value = lhs != null ? lhs[right] : undefined; + if (self.expensiveChecks || isPossiblyDangerousMemberName(right)) { ensureSafeObject(value, self.expression); } if (context) { - return {context: lhs, name: ast.property.name, value: value}; + return {context: lhs, name: right, value: value}; } else { return value; } From 5429f080d77a840dc6b69b9cfc79331236dc5a28 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Thu, 1 Jan 2015 15:30:52 +0100 Subject: [PATCH 10/20] perf($parse): do not watch constant expressions at arrays and objects When breaking up a watch on an array, object or stateless filter, do not watch constant expressions --- src/ng/parse.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index f854ce57b1dd..0868ab444f61 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -657,7 +657,9 @@ function findConstantAndWatchExpressions(ast, $filter) { forEach(ast.arguments, function(expr) { findConstantAndWatchExpressions(expr, $filter); allConstants = allConstants && expr.constant; - argsToWatch.push.apply(argsToWatch, expr.toWatch); + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } }); ast.constant = allConstants; ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; @@ -674,7 +676,9 @@ function findConstantAndWatchExpressions(ast, $filter) { forEach(ast.elements, function(expr) { findConstantAndWatchExpressions(expr, $filter); allConstants = allConstants && expr.constant; - argsToWatch.push.apply(argsToWatch, expr.toWatch); + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } }); ast.constant = allConstants; ast.toWatch = argsToWatch; @@ -685,7 +689,9 @@ function findConstantAndWatchExpressions(ast, $filter) { forEach(ast.properties, function(property) { findConstantAndWatchExpressions(property.value, $filter); allConstants = allConstants && property.value.constant; - argsToWatch.push.apply(argsToWatch, property.value.toWatch); + if (!property.value.constant) { + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + } }); ast.constant = allConstants; ast.toWatch = argsToWatch; From b2069480383002f479175ecf9a738d0257c787fa Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Thu, 1 Jan 2015 15:51:58 +0100 Subject: [PATCH 11/20] chore($parse): removed unnecessary variables Removed two unnecessary variables --- src/ng/parse.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 0868ab444f61..f338ed408bcc 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -818,7 +818,7 @@ ASTCompiler.prototype = { var fns = this.state.inputs; var self = this; forEach(fns, function(name) { - result.push('var ' + name + '=' + self.generateFunction(name, 's,l')); + result.push('var ' + name + '=' + self.generateFunction(name, 's')); }); if (fns.length) { result.push('fn.inputs=[' + fns.join(',') + '];'); @@ -835,7 +835,6 @@ ASTCompiler.prototype = { filterPrefix: function() { var parts = []; - var checks = []; var self = this; forEach(this.state.filters, function(id, filter) { parts.push(id + '=$filter(' + self.escape(filter) + ')'); From dac9780f218c268560b4436965b27cb33227deb5 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Fri, 2 Jan 2015 15:58:51 +0100 Subject: [PATCH 12/20] fix($parse): make the expression `+undefined` evaluate to zero Make the expression `+undefined` evaluate to zero. Fix an inconsistency between csp enabled and disabled with the expression `+null`. Make it work in both cases as in it works in JavaScript --- src/ng/parse.js | 10 +++++++--- test/ng/parseSpec.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index f338ed408bcc..e69856f9a7c0 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -872,7 +872,7 @@ ASTCompiler.prototype = { break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); - expression = ast.operator + '(' + right + ')'; + expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; this.assign(intoId, expression); recursionFn(expression); break; @@ -1436,8 +1436,10 @@ ASTInterpreter.prototype = { 'unary+': function(argument, context) { return function(scope, locals, assign, inputs) { var arg = argument(scope, locals, assign, inputs); - if (arg != null) { + if (isDefined(arg)) { arg = +arg; + } else { + arg = 0; } return context ? {value: arg} : arg; }; @@ -1445,8 +1447,10 @@ ASTInterpreter.prototype = { 'unary-': function(argument, context) { return function(scope, locals, assign, inputs) { var arg = argument(scope, locals, assign, inputs); - if (arg != null) { + if (isDefined(arg)) { arg = -arg; + } else { + arg = 0; } return context ? {value: arg} : arg; }; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 1416aea0b9a0..c86c99470f35 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1701,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(); From 597b560c0868865d3121df96001665fa0e020981 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 2 Jan 2015 23:41:45 -0800 Subject: [PATCH 13/20] test($parse): add multistatement and assignment $watch tests --- test/ng/parseSpec.js | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index c86c99470f35..d71eaf0c0808 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3159,6 +3159,51 @@ describe('parser', function() { 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() { From 0cde2997b90175103f229eefb2e8e5ffe07ac003 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sat, 3 Jan 2015 12:45:27 +0100 Subject: [PATCH 14/20] fix($parse): watch the assignment expression and multi-expressions When watching an assignment expression, the previous behavior used to watch only the right hand side of the expression. The change is to watch the entire expression. When watching multi-expressions, the previous behavior used to watch the last expression. The change is to watch all the expressions. --- src/ng/parse.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index e69856f9a7c0..3526c19c01e5 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -668,7 +668,7 @@ function findConstantAndWatchExpressions(ast, $filter) { findConstantAndWatchExpressions(ast.left, $filter); findConstantAndWatchExpressions(ast.right, $filter); ast.constant = ast.left.constant && ast.right.constant; - ast.toWatch = ast.right.toWatch; + ast.toWatch = [ast]; break; case AST.ArrayExpression: allConstants = true; @@ -704,8 +704,8 @@ function findConstantAndWatchExpressions(ast, $filter) { } function getInputs(body) { - if (!body.length) return; - var lastExpression = body[body.length - 1].expression; + 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; From b4a5e363cf59e80a25d8f2d037e20ee3dca9914d Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sun, 4 Jan 2015 12:55:53 +0100 Subject: [PATCH 15/20] fix($parse): use `locals` on assignments if `locals` has the property When there is an assignment in an expression and `locals` has a property that matches the name of the property that is being assigned to, then use `locals` --- src/ng/parse.js | 2 +- test/ng/parseSpec.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 3526c19c01e5..c03229ea8014 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1298,7 +1298,7 @@ ASTInterpreter.prototype = { case AST.Identifier: ensureSafeMemberName(ast.name); return function(scope, locals, assign, inputs) { - var base = locals && locals.hasOwnProperty(ast.name) ? locals : scope; + var base = locals && (ast.name in locals) ? locals : scope; if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { ensureSafeObject(value, self.expression); } diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index d71eaf0c0808..be0f25cb6f98 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3225,6 +3225,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() { From f08a8a35b0f0f92de1f66c785b374c2b99911645 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sun, 4 Jan 2015 13:39:00 +0100 Subject: [PATCH 16/20] fix($parse): mark empty expressions as `constant` and `literal` Mark empty expressions as `constant` and `literal` Closes ##7762 --- src/ng/parse.js | 8 ++++++-- test/ng/parseSpec.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index c03229ea8014..d0e2a0f82971 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -607,9 +607,12 @@ function findConstantAndWatchExpressions(ast, $filter) { 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; @@ -722,14 +725,15 @@ function assignableAST(ast) { } function isLiteral(ast) { - return ast.body.length === 1 && ( + 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.body.length === 1 && ast.body[0].expression.constant; + return ast.constant; } function ASTCompiler(astBuilder, $filter) { diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index be0f25cb6f98..147f1e7d5b48 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2804,6 +2804,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); @@ -3274,6 +3281,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); From 4a3a6e0c4a147ebdc8ad165c0048f3dac2db29c8 Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sat, 10 Jan 2015 13:33:49 +0100 Subject: [PATCH 17/20] refactor($parse): simplified code paths Simplified several code paths --- src/ng/parse.js | 333 +++++++++++++++++++++---------------------- test/ng/parseSpec.js | 11 ++ 2 files changed, 175 insertions(+), 169 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index d0e2a0f82971..21c2c6673cc7 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -793,7 +793,6 @@ ASTCompiler.prototype = { 'ensureSafeMemberName', 'ensureSafeObject', 'ensureSafeFunction', - 'isPossiblyDangerousMemberName', 'ifDefined', 'plus', 'text', @@ -802,7 +801,6 @@ ASTCompiler.prototype = { ensureSafeMemberName, ensureSafeObject, ensureSafeFunction, - isPossiblyDangerousMemberName, ifDefined, plusFn, expression); @@ -855,9 +853,17 @@ ASTCompiler.prototype = { return this.state[section].body.join(''); }, - recurse: function(ast, intoId, nameId, recursionFn, create) { + 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) { @@ -885,7 +891,7 @@ ASTCompiler.prototype = { this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); if (ast.operator === '+') { expression = this.plus(left, right); - } else if (ast.operator === '=') { + } else if (ast.operator === '-') { expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); } else { expression = '(' + left + ')' + ast.operator + '(' + right + ')'; @@ -895,23 +901,15 @@ ASTCompiler.prototype = { break; case AST.LogicalExpression: intoId = intoId || this.nextId(); - this.if(isDefined(ast.watchId) ? '!i' : true, function() { - self.recurse(ast.left, intoId); - self.if(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); - recursionFn(intoId); - }, function() { - self.assign(intoId, self.computedMember('i', ast.watchId)); - }); + 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(); - this.if(isDefined(ast.watchId) ? '!i' : true, function() { - self.recurse(ast.test, intoId); - self.if(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); - recursionFn(intoId); - }, function() { - self.assign(intoId, self.computedMember('i', ast.watchId)); - }); + 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(); @@ -920,112 +918,100 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - this.if(isDefined(ast.watchId) ? '!i' : true, function() { - 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); - }, function() { - self.assign(intoId, self.computedMember('i', ast.watchId)); - }); + 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(); - this.if(isDefined(ast.watchId) ? '!i' : true, function() { - 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; - } + 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), '{}')); } - recursionFn(intoId); - }); - }, !!create); - }, function() { - self.assign(intoId, self.computedMember('i', ast.watchId)); - }); + 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(); - this.if(isDefined(ast.watchId) ? '!i' : true, function() { - 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 (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); }); + 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); }); - } - }, function() { - self.assign(intoId, self.computedMember('i', ast.watchId)); - }); + }); + } break; case AST.AssignmentExpression: right = this.nextId(); @@ -1174,10 +1160,10 @@ ASTCompiler.prototype = { return 'ensureSafeFunction(' + item + ',text)'; }, - lazyRecurse: function(ast, intoId, nameId, recursionFn, create) { + lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { var self = this; return function() { - self.recurse(ast, intoId, nameId, recursionFn, create); + self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); }; }, @@ -1301,21 +1287,7 @@ ASTInterpreter.prototype = { ); case AST.Identifier: ensureSafeMemberName(ast.name); - return function(scope, locals, assign, inputs) { - var base = locals && (ast.name in locals) ? locals : scope; - if (self.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - ensureSafeObject(value, self.expression); - } - if (create && create !== 1 && base && !(ast.name in base)) { - base[ast.name] = {}; - } - var value = base ? base[ast.name] : undefined; - if (context) { - return {context: base, name: ast.name, value: value}; - } else { - return value; - } - }; + return self.identifier(ast.name, self.expensiveChecks, context, create, self.expression); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { @@ -1324,40 +1296,8 @@ ASTInterpreter.prototype = { } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? - 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, self.expression); - if (create && create !== 1 && lhs && !(rhs in lhs)) { - lhs[rhs] = {}; - } - value = lhs[rhs]; - ensureSafeObject(value, self.expression); - } - if (context) { - return {context: lhs, name: rhs, value: value}; - } else { - return value; - } - } : - 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 (self.expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, self.expression); - } - if (context) { - return {context: lhs, name: right, value: value}; - } else { - return value; - } - }; + 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) { @@ -1568,6 +1508,61 @@ ASTInterpreter.prototype = { 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 || isPossiblyDangerousMemberName(name)) { + 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]; @@ -1866,11 +1861,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) @@ -1885,7 +1880,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/test/ng/parseSpec.js b/test/ng/parseSpec.js index 147f1e7d5b48..135ae134bd15 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1898,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() { @@ -2982,6 +2992,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); From 9dac95ead5d707888bb6d05e75080d77e6b5169e Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Sat, 10 Jan 2015 20:54:24 +0100 Subject: [PATCH 18/20] refactor($parse): simplified a few expressions Simplified the RegEx for filtering string Compute `isPossiblyDangerousMemberName` only once for identifiers Compte if an expression is one-time binding only when it is not in the cache --- src/ng/parse.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 21c2c6673cc7..965beb358c23 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1174,7 +1174,7 @@ ASTCompiler.prototype = { }; }, - stringEscapeRegex: new RegExp('[^ a-zA-Z0-9]', 'g'), + stringEscapeRegex: /[^ a-zA-Z0-9]/g, stringEscapeFn: function(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); @@ -1287,7 +1287,9 @@ ASTInterpreter.prototype = { ); case AST.Identifier: ensureSafeMemberName(ast.name); - return self.identifier(ast.name, self.expensiveChecks, context, create, 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) { @@ -1515,7 +1517,7 @@ ASTInterpreter.prototype = { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(name)) { + if (expensiveChecks) { ensureSafeObject(value, expression); } if (context) { @@ -1702,12 +1704,12 @@ function $ParseProvider() { var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); From e598aa1aabb0a0e025f71449e2ef0a694cc7624a Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 16 Jan 2015 00:26:28 -0800 Subject: [PATCH 19/20] fix($parse): passing original input values to parse function instead of the valueOf --- src/ng/parse.js | 11 +++++++---- test/ng/parseSpec.js | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 965beb358c23..c81ee50f8fe6 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1764,21 +1764,23 @@ function $ParseProvider() { 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)) { + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); - oldInputValue = newInputValue && getValueOf(newInputValue); + oldInputValueOf = newInputValue && getValueOf(newInputValue); } return lastResult; }, 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) { @@ -1787,12 +1789,13 @@ 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, undefined, undefined, oldInputValueOfValues); + lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); } return lastResult; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 135ae134bd15..8837b158b90b 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3089,10 +3089,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; @@ -3115,10 +3116,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; From 22e9584328e1ec0705385bfa67a313caa6329b0a Mon Sep 17 00:00:00 2001 From: Lucas Galfaso Date: Thu, 22 Jan 2015 21:47:48 +0100 Subject: [PATCH 20/20] fix($parse): add the expression to the error in a few sandbox checks Add the expression to the error in two cases that were missing. Added a few tests for this, and more tests that define the behavior of expensive checks and assignments. --- src/ng/parse.js | 4 +-- test/ng/parseSpec.js | 60 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index c81ee50f8fe6..f500a32eddc0 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1286,7 +1286,7 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name); + ensureSafeMemberName(ast.name, self.expression); return self.identifier(ast.name, self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), context, create, self.expression); @@ -1336,7 +1336,7 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value); + ensureSafeObject(lhs.value, self.expression); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 8837b158b90b..c690bd61cfec 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2225,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( @@ -2308,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); @@ -2330,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'); })); }); }); @@ -2387,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() { @@ -2402,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() { @@ -2418,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) { @@ -2575,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');